Initial commit

This commit is contained in:
Jonas Karneboge 2026-09-22 18:35:43 +02:00
commit 3cba772836
1341 changed files with 532924 additions and 0 deletions

BIN
SOMS2/.RData Normal file

Binary file not shown.

1
SOMS2/.Rprofile Normal file
View file

@ -0,0 +1 @@
source("renv/activate.R")

13
SOMS2/SOMS2.Rproj Normal file
View file

@ -0,0 +1,13 @@
Version: 1.0
RestoreWorkspace: Default
SaveWorkspace: Default
AlwaysSaveHistory: Default
EnableCodeIndexing: Yes
UseSpacesForTab: Yes
NumSpacesForTab: 2
Encoding: UTF-8
RnwWeave: Sweave
LaTeX: pdfLaTeX

999
SOMS2/app.R Normal file
View file

@ -0,0 +1,999 @@
# Präambel ####
AKZENT_FARBE = "#8B2635"
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_soms2.R" # liefert: daten_soms2
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R" # liefert: pseudo
PFAD_NORM_A1 = "normen/soms2_norm_a1_gesunde.csv"
PFAD_NORM_A2 = "normen/soms2_norm_a2_patienten.csv"
SOMS2_ROHWERT_MAX_A1 = 20
SOMS2_ROHWERT_MAX_A2 = 40
SOMS2_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person."
)
SOMS2_BESCHWERDEN_CUTOFF_HINWEIS = paste0(
"Ab einem Beschwerdenindex von mindestens 7 im SOMS-2 kann in der Regel von ",
"einem starken, beeintraechtigenden Somatisierungssyndrom ausgegangen werden."
)
library(shiny)
library(dplyr)
library(haven)
library(officer)
# Infrastruktur ####
APP_VERZEICHNIS = normalizePath(getwd())
absPath = function(pfad) {
if (grepl("^([A-Za-z]:[/\\\\]|/)", pfad)) return(pfad)
file.path(APP_VERZEICHNIS, pfad)
}
PFAD_DOWNLOAD_SKRIPT = normalizePath(absPath(PFAD_DOWNLOAD_SKRIPT), mustWork = FALSE)
PFAD_PSEUDONYM_SKRIPT = normalizePath(absPath(PFAD_PSEUDONYM_SKRIPT), mustWork = FALSE)
PFAD_NORM_A1 = normalizePath(absPath(PFAD_NORM_A1), mustWork = FALSE)
PFAD_NORM_A2 = normalizePath(absPath(PFAD_NORM_A2), mustWork = FALSE)
# Helper ####
# Generischer Label-Text-Lookup ueber das labels-Attribut der ORIGINAL-Spalte
# (vor Subsetting), damit die Zuordnung Wert -> Text immer aus den Daten selbst
# stammt und nie von der konkreten formr-Rohkodierung abhaengt.
soms2_get_label_text = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_character_)
lbl_attr = attr(original_col, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
pos = which(as.vector(lbl_attr) == as.numeric(wert[1]))
if (length(pos) > 0) return(trimws(names(lbl_attr)[pos[1]]))
}
NA_character_
}
# Ordinalrang (0-basiert) ueber die sortierte Position im labels-Attribut,
# unabhaengig von der tatsaechlichen formr-Kodierung. Genutzt fuer soms2_54
# (0 = "keinmal" ... 4 = "mehr als 12 mal") und soms2_63
# (0 = "unter 6 Monate" ... 3 = "ueber 2 Jahre").
soms2_ordinal_rang = function(original_col, wert) {
if (is.null(wert) || length(wert) == 0 || is.na(wert[1])) return(NA_integer_)
lbl_attr = attr(original_col, "labels")
if (!is.null(lbl_attr) && length(lbl_attr) > 0) {
lbl_sortiert = sort(as.vector(lbl_attr))
pos = which(lbl_sortiert == as.numeric(wert[1]))
if (length(pos) > 0) return(as.integer(pos[1]) - 1L)
}
NA_integer_
}
# TRUE nur bei eindeutigem "ja"-Label. NA/fehlend (u.a. showif-bedingtes NA bei
# geschlechtsspezifischen Items) wird als "nein" gewertet, nie als Fehler - das
# ist fuer die Summenscores korrekt, weil solche Items ohnehin ueber den
# dynamischen Maximalwert aus der Zaehlung herausgenommen werden.
soms2_ist_ja = function(original_col, wert) {
txt = soms2_get_label_text(original_col, wert)
if (is.na(txt)) return(FALSE)
if (grepl("nein", txt, ignore.case = TRUE)) return(FALSE)
grepl("ja", txt, ignore.case = TRUE)
}
# TRUE nur bei explizitem "nein"-Label. Anders als soms2_ist_ja() liefert dies
# bei NA FALSE zurueck (Kriterium "Antwort = nein" gilt bei fehlender Antwort
# als nicht erfuellt, nicht automatisch als erfuellt).
soms2_ist_nein = function(original_col, wert) {
txt = soms2_get_label_text(original_col, wert)
if (is.na(txt)) return(FALSE)
grepl("nein", txt, ignore.case = TRUE)
}
# Geschlecht ausschliesslich ueber das labels-Attribut/den Text abgleichen, nie
# ueber hartkodierte Zahlenwerte 1/2 (Kodierung kann je Setup variieren).
soms2_geschlecht_text = function(original_col, wert) {
txt = soms2_get_label_text(original_col, wert)
if (is.na(txt)) return(NA_character_)
txt_l = tolower(txt)
if (grepl("weib", txt_l)) return("weiblich")
if (grepl("männ|maenn", txt_l)) return("maennlich")
NA_character_
}
# Entfernt Markdown-Escapes ("1\. Text" -> "1. Text", so liegt das label-
# Attribut in der soms2.xlsx vor) und danach die fuehrende Itemnummer samt
# Trennzeichen (z.B. "1. " oder "01) ").
soms2_clean_item_text = function(text) {
if (is.null(text) || length(text) == 0 || is.na(text[1])) return(NA_character_)
txt = gsub("\\.", ".", trimws(as.character(text[1])), fixed = TRUE)
trimws(sub("^\\d+[.):]?\\s*", "", txt))
}
# Klartext-Fragetext eines Items, Fallback auf den Variablennamen, falls kein
# label-Attribut vorhanden ist (z.B. bei einem unvollstaendigen Test-Stub).
soms2_item_text = function(daten, item) {
txt = soms2_clean_item_text(attr(daten[[item]], "label"))
if (is.na(txt)) item else txt
}
# Baut einen Kriteriumsbestandteil: Fragetext, tatsaechlich gegebene Antwort
# (Klartext ueber das labels-Attribut) und geforderte Antwort/Kategorie.
soms2_kriterium_teil = function(zeile, daten, item, erforderlich) {
antwort = soms2_get_label_text(daten[[item]], zeile[[item]])
list(
text = soms2_item_text(daten, item),
antwort = if (is.na(antwort)) "k. A." else antwort,
erforderlich = erforderlich
)
}
# Zaehlt "ja"-Antworten ueber eine einfache Itemliste (DSM-IV, Beschwerdenindex).
soms2_score_items = function(zeile, daten, items) {
sum(vapply(items, function(it) soms2_ist_ja(daten[[it]], zeile[[it]]), logical(1)))
}
# Zaehlt Wertungseinheiten: jede Gruppe (ODER-Verknuepfung mehrerer Items)
# zaehlt maximal 1 Punkt, auch wenn mehrere Items darin "ja" sind
# (ICD-10-Somatisierungsindex, SAD-Index).
soms2_score_einheiten = function(zeile, daten, einheiten) {
sum(vapply(einheiten, function(gruppe) {
any(vapply(gruppe, function(it) soms2_ist_ja(daten[[it]], zeile[[it]]), logical(1)))
}, logical(1)))
}
# Prozentrang-Lookup: exakter Rohwert-Match; Rohwerte oberhalb des
# Tabellenmaximums erhalten Prozentrang 100 (alle Indizes erreichen dort
# bereits 100 in der jeweiligen Norm). Ohne exakten Treffer (sollte bei
# Ganzzahl-Scores nicht vorkommen) wird auf den naechstniedrigeren
# Tabelleneintrag geruendet.
soms2_prozentrang = function(rohwert, norm_tab, spalte, rohwert_max) {
if (is.null(rohwert) || length(rohwert) == 0 || is.na(rohwert)) return(NA_integer_)
if (rohwert > rohwert_max) return(100L)
treffer = norm_tab[norm_tab$rohwert == rohwert, ]
if (nrow(treffer) == 0) {
kandidaten = norm_tab[norm_tab$rohwert <= rohwert, ]
if (nrow(kandidaten) == 0) return(NA_integer_)
treffer = kandidaten[which.max(kandidaten$rohwert), ]
}
as.numeric(treffer[[spalte]][1])
}
# Ermittelt Prozentrang (+ optionale Gesamtnorm-Referenz bei A-1) fuer einen
# Index, abhaengig von der gewaehlten Vergleichsnorm und dem Geschlecht.
soms2_pr_ergebnis = function(index_key, rohwert, geschlecht, normwahl, norm_a1, norm_a2) {
if (identical(normwahl, "patienten")) {
spalte = SOMS2_NORM_SPALTEN_A2[[index_key]]
pr = soms2_prozentrang(rohwert, norm_a2, spalte, SOMS2_ROHWERT_MAX_A2)
return(list(pr = pr, pr_gesamt = NA_integer_))
}
spalten = SOMS2_NORM_SPALTEN_A1[[index_key]]
geschlecht_spalte = if (identical(geschlecht, "weiblich")) spalten$w
else if (identical(geschlecht, "maennlich")) spalten$m
else spalten$gesamt
pr = soms2_prozentrang(rohwert, norm_a1, geschlecht_spalte, SOMS2_ROHWERT_MAX_A1)
pr_gesamt = soms2_prozentrang(rohwert, norm_a1, spalten$gesamt, SOMS2_ROHWERT_MAX_A1)
list(pr = pr, pr_gesamt = pr_gesamt)
}
# Dynamische Maximalwerte (Abschnitt 4): unbekanntes/fehlendes Geschlecht faellt
# konservativ auf den vollen (nicht reduzierten) Maximalwert zurueck.
soms2_dsmiv_max = function(geschlecht) {
if (identical(geschlecht, "weiblich")) return(33L - 1L)
if (identical(geschlecht, "maennlich")) return(33L - 4L)
33L
}
soms2_icd10_max = function(geschlecht) {
if (identical(geschlecht, "maennlich")) return(13L)
14L
}
soms2_beschwerden_max = function(geschlecht) {
# Beschwerdenindex umfasst alle 53 Items (anders als DSM-IV, das soms2_52
# gar nicht enthaelt): bei Maennern sind soms2_48-52 (5 Items) nicht
# anwendbar, bei Frauen nur soms2_53 (1 Item).
if (identical(geschlecht, "weiblich")) return(53L - 1L)
if (identical(geschlecht, "maennlich")) return(53L - 5L)
53L
}
# Jedes Kriterium traegt seine Bestandteile (teile - i.d.R. 1, bei der
# ODER-Verknuepfung in Kriterium 1 des DSM-IV-Index 2) mit dem tatsaechlichen
# Fragetext + der gegebenen Antwort, damit UI und Word-Export die konkrete
# Item-Formulierung statt einer blossen Itemnummer anzeigen koennen.
soms2_dsmiv_kriterien = function(zeile, daten) {
rang54 = soms2_ordinal_rang(daten[["soms2_54"]], zeile[["soms2_54"]])
rang63 = soms2_ordinal_rang(daten[["soms2_63"]], zeile[["soms2_63"]])
list(
list(
teile = list(
soms2_kriterium_teil(zeile, daten, "soms2_54", "nicht 'keinmal'"),
soms2_kriterium_teil(zeile, daten, "soms2_58", "ja")
),
verknuepfung = "ODER",
erfuellt = (!is.na(rang54) && rang54 != 0L) ||
soms2_ist_ja(daten[["soms2_58"]], zeile[["soms2_58"]])
),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_55", "nein")),
verknuepfung = NULL,
erfuellt = soms2_ist_nein(daten[["soms2_55"]], zeile[["soms2_55"]])),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_62", "ja")),
verknuepfung = NULL,
erfuellt = soms2_ist_ja(daten[["soms2_62"]], zeile[["soms2_62"]])),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_63", "'ueber 2 Jahre'")),
verknuepfung = NULL,
erfuellt = !is.na(rang63) && rang63 == 3L)
)
}
soms2_icd10_kriterien = function(zeile, daten) {
rang54 = soms2_ordinal_rang(daten[["soms2_54"]], zeile[["soms2_54"]])
rang63 = soms2_ordinal_rang(daten[["soms2_63"]], zeile[["soms2_63"]])
list(
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_54", "mindestens '3 bis 6 mal'")),
verknuepfung = NULL, erfuellt = !is.na(rang54) && rang54 >= 2L),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_55", "nein")),
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_55"]], zeile[["soms2_55"]])),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_56", "nein")),
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_56"]], zeile[["soms2_56"]])),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_57", "ja")),
verknuepfung = NULL, erfuellt = soms2_ist_ja(daten[["soms2_57"]], zeile[["soms2_57"]])),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_61", "nein")),
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_61"]], zeile[["soms2_61"]])),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_63", "'ueber 2 Jahre'")),
verknuepfung = NULL, erfuellt = !is.na(rang63) && rang63 == 3L)
)
}
soms2_sad_kriterien = function(zeile, daten) {
list(
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_55", "nein")),
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_55"]], zeile[["soms2_55"]])),
list(teile = list(soms2_kriterium_teil(zeile, daten, "soms2_61", "nein")),
verknuepfung = NULL, erfuellt = soms2_ist_nein(daten[["soms2_61"]], zeile[["soms2_61"]]))
)
}
# Datenaufbereitung ####
if (!file.exists(PFAD_NORM_A1)) {
stop(paste0(
"Normtabelle (Gesunde) nicht gefunden: ", PFAD_NORM_A1,
". Bitte die Datei soms2_norm_a1_gesunde.csv in den Unterordner 'normen/' legen."
))
}
if (!file.exists(PFAD_NORM_A2)) {
stop(paste0(
"Normtabelle (Patienten) nicht gefunden: ", PFAD_NORM_A2,
". Bitte die Datei soms2_norm_a2_patienten.csv in den Unterordner 'normen/' legen."
))
}
soms2_norm_a1 = read.csv(PFAD_NORM_A1, stringsAsFactors = FALSE)
soms2_norm_a2 = read.csv(PFAD_NORM_A2, stringsAsFactors = FALSE)
# Somatisierungsindex DSM-IV: 33 Items, soms2_53 nur bei Frauen erhoben,
# soms2_48-51 nur bei Maennern erhoben (showif in formr).
SOMS2_DSMIV_ITEMS = sprintf("soms2_%02d", c(
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 16, 20, 32, 34, 35,
36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 53
))
# Somatisierungsindex ICD-10: 14 Wertungseinheiten (ODER-Gruppen), Einheit 14
# (soms2_52) nur bei Frauen anwendbar.
SOMS2_ICD10_EINHEITEN = list(
"soms2_02",
c("soms2_04", "soms2_05"),
"soms2_06",
c("soms2_09", "soms2_22", "soms2_38"),
"soms2_10",
"soms2_11",
c("soms2_13", "soms2_14"),
"soms2_18",
c("soms2_20", "soms2_21"),
"soms2_28",
"soms2_31",
"soms2_33",
c("soms2_40", "soms2_41"),
"soms2_52"
)
# SAD-Index ICD-10: 12 Wertungseinheiten, keine Geschlechtertrennung.
SOMS2_SAD_EINHEITEN = list(
c("soms2_06", "soms2_25"),
"soms2_11",
"soms2_12",
"soms2_15",
"soms2_19",
c("soms2_09", "soms2_22", "soms2_38"),
"soms2_23",
"soms2_24",
"soms2_26",
"soms2_27",
c("soms2_28", "soms2_29"),
"soms2_30"
)
# Beschwerdenindex Somatisierung: alle Items soms2_01-soms2_53, keine Auswahl.
SOMS2_BESCHWERDEN_ITEMS = sprintf("soms2_%02d", 1:53)
# Zusatz-Screeningitems 64-68: 65/67 sind Folgefragen zu 64/66, 68 steht
# alleine. Reine Einzelitem-Anzeige, kein Score.
SOMS2_ZUSATZ_ITEMS = list(
list(haupt = "soms2_64", folge = "soms2_65"),
list(haupt = "soms2_66", folge = "soms2_67"),
list(haupt = "soms2_68", folge = NULL)
)
# Spaltenzuordnung fuer die Prozentrang-Lookups je Normtabelle.
SOMS2_NORM_SPALTEN_A1 = list(
dsmiv = list(gesamt = "dsmiv_gesamt", m = "dsmiv_m", w = "dsmiv_w"),
icd10 = list(gesamt = "icd10_gesamt", m = "icd10_m", w = "icd10_w"),
sad = list(gesamt = "sad_gesamt", m = "sad_m", w = "sad_w"),
beschwerden = list(gesamt = "beschwerdenindex_gesamt", m = "beschwerdenindex_m", w = "beschwerdenindex_w")
)
SOMS2_NORM_SPALTEN_A2 = list(
dsmiv = "dsmiv",
icd10 = "icd10",
sad = "sad",
beschwerden = "beschwerdenindex_gesamt"
)
# UI ####
app_css = "
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f5f5; }
.app-header {
background: #8B2635; color: white; padding: 18px 24px 14px;
margin-bottom: 20px; border-radius: 0 0 6px 6px;
}
.app-header h2 { margin: 0; font-size: 1.5rem; font-weight: 600; }
.app-header p { margin: 4px 0 0; opacity: 0.85; font-size: 0.9rem; }
.input-panel {
background: white; border-radius: 6px; padding: 16px 20px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap;
}
.input-panel .form-group { margin-bottom: 0; }
.input-panel label { font-weight: 600; color: #333; }
.btn-laden {
background: #8B2635 !important; color: white !important;
border: none !important; border-radius: 4px !important;
padding: 8px 20px !important; font-weight: 600 !important; cursor: pointer;
}
.btn-laden:hover { background: #6d1e29 !important; }
.alert-fehler {
background: #FFEBEE; border-left: 5px solid #C62828;
padding: 12px 16px; border-radius: 4px; color: #B71C1C;
margin-bottom: 12px; font-weight: 500;
}
.alert-warnung {
background: #FFF3E0; border-left: 5px solid #E65100;
padding: 10px 16px; border-radius: 4px; color: #BF360C;
margin-bottom: 12px; font-size: 0.93em; font-weight: 500;
}
.abschnitt-karte {
background: white; border-radius: 6px; padding: 20px 24px;
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
.abschnitt-titel {
color: #8B2635; font-size: 1.15rem; font-weight: 700;
border-bottom: 2px solid #8B2635; padding-bottom: 8px; margin-bottom: 14px;
}
.meta-block { margin-bottom: 10px; color: #555; font-size: 0.95em; }
.meta-block strong { color: #222; }
.item-zeile {
display: flex; align-items: flex-start; gap: 10px;
padding: 7px 0; border-bottom: 1px solid #F0F0F0;
}
.item-zeile.item-unterpunkt { padding-left: 28px; border-bottom: none; }
.item-nr { font-weight: 600; color: #8B2635; min-width: 34px; flex-shrink: 0; }
.item-text { flex: 1; color: #333; font-size: 0.92em; }
.kriterien-liste { margin: 10px 0; padding: 0; list-style: none; }
.kriterium-zeile {
display: flex; align-items: flex-start; gap: 10px; padding: 8px 0;
border-bottom: 1px solid #F5F5F5; font-size: 0.92em; color: #444;
}
.kriterium-status { font-weight: 700; min-width: 100px; flex-shrink: 0; }
.kriterium-erfuellt { color: #2E7D32; }
.kriterium-nicht-erfuellt { color: #B71C1C; }
.kriterium-inhalt { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.kriterium-frage { color: #333; }
.kriterium-antwort { color: #777; font-size: 0.85em; }
.kriterium-verknuepfung {
font-size: 0.78em; color: #999; font-style: italic; margin: 2px 0;
}
.gesamtstatus-box {
border-radius: 6px; padding: 12px 16px; margin-top: 10px;
font-weight: 700; border-left: 5px solid;
}
.gesamtstatus-erfuellt { background: #E8F5E9; border-color: #2E7D32; color: #2E7D32; }
.gesamtstatus-nicht-erfuellt { background: #F5F5F5; border-color: #9E9E9E; color: #616161; }
.score-zahl { font-size: 2.2rem; font-weight: 800; color: #8B2635; }
.score-label { color: #555; font-size: 0.9em; }
.pr-info { color: #555; font-size: 0.9em; margin-top: 4px; }
.cutoff-hinweis {
background: #FFF3E0; border-left: 5px solid #E65100; color: #BF360C;
padding: 10px 14px; border-radius: 4px; margin-top: 10px; font-size: 0.9em;
}
.normwahl-hinweis { color: #777; font-size: 0.82em; font-style: italic; margin-top: 6px; }
.start-hinweis { text-align: center; color: #bbb; padding: 30px 0; font-style: italic; }
"
app_css = gsub("#8B2635", AKZENT_FARBE, app_css, fixed = TRUE)
ui = fluidPage(
tags$head(
tags$meta(charset = "UTF-8"),
tags$style(HTML(app_css))
),
div(class = "app-header",
tags$h2("SOMS-2 - Screening fuer somatoforme Stoerungen (2-Jahres-Version)"),
tags$p("Rief, Hiller & Heuser 1997 | Vier parallele Auswertungskennwerte")
),
div(class = "container-fluid",
div(class = "input-panel",
div(style = "min-width: 360px; white-space: nowrap;",
textInput("pseudonym",
label = tagList(
"Pseudonym",
tags$span(style = "font-weight: normal; font-style: italic; font-size: 0.78em; color: #888; margin-left: 4px; white-space: nowrap;",
"optional, hat Vorrang vor Chiffre")
),
placeholder = "optional", width = "340px")
),
div(style = "min-width: 200px;",
textInput("chiffre", label = "Patientenchiffre",
placeholder = "z.B. P000123", width = "100%")
),
actionButton("btn_suchen", "Auswerten", class = "btn btn-primary btn-laden"),
div(style = "margin-left: auto;",
downloadButton("download_word", "Word-Export (.docx)")
)
),
div(class = "input-panel",
div(
tags$label("Vergleichsnorm",
style = "font-weight:600; color:#333; display:block; margin-bottom:4px;"),
radioButtons("normwahl", label = NULL,
choices = c("Gesunde" = "gesunde", "psychosomatische Patienten" = "patienten"),
selected = "gesunde", inline = TRUE)
),
uiOutput("normwahl_hinweis_ui")
),
uiOutput("ergebnis_ui")
)
)
# Word-Export ####
erstelle_soms2_docx = function(erg) {
doc = read_docx()
fp_titel = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18)
fp_abschnitt = fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 13)
fp_label = fp_text(bold = TRUE, font.size = 11)
fp_normal = fp_text(font.size = 11)
fp_erfuellt = fp_text(font.size = 10, bold = TRUE, color = "#2E7D32")
fp_nicht = fp_text(font.size = 10, bold = TRUE, color = "#B71C1C")
fp_gesamt_ok = fp_text(font.size = 11, bold = TRUE, color = "#2E7D32")
fp_gesamt_nok = fp_text(font.size = 11, bold = TRUE, color = "#616161")
fp_cutoff = fp_text(font.size = 10, italic = TRUE, color = "#BF360C")
fp_warnung = fp_text(font.size = 10, italic = TRUE, color = "#8a6d00")
fp_normhinweis = fp_text(font.size = 9.5, italic = TRUE, color = "#555555")
fp_antwort = fp_text(font.size = 9.5, color = "#777777")
fp_verknuepfung = fp_text(font.size = 9, italic = TRUE, color = "#999999")
fp_disclaimer = fp_text(font.size = 9, italic = TRUE, color = "#777777")
doc = body_add_fpar(doc, fpar(ftext("SOMS-2 - Auswertung", fp_titel)))
doc = body_add_fpar(doc, fpar(
ftext("Chiffre: ", fp_label),
ftext(erg$chiffre, fp_normal),
ftext(" Ausfuelldatum: ", fp_label),
ftext(erg$datum_str, fp_normal),
ftext(" Geschlecht: ", fp_label),
ftext(if (is.na(erg$geschlecht_text)) "k. A." else erg$geschlecht_text, fp_normal)
))
if (!is.null(erg$warnung_daten)) {
doc = body_add_fpar(doc, fpar(ftext(erg$warnung_daten, fp_warnung)))
}
doc = body_add_fpar(doc, fpar(ftext(
if (identical(erg$normwahl, "patienten"))
"Vergleichsnorm: psychosomatische Patienten (nicht nach Geschlecht getrennt)"
else
"Vergleichsnorm: Gesunde",
fp_normhinweis
)))
doc = body_add_par(doc, "", style = "Normal")
for (idx in erg$indizes) {
doc = body_add_fpar(doc, fpar(ftext(idx$titel, fp_abschnitt)))
doc = body_add_fpar(doc, fpar(
ftext("Rohwert: ", fp_label),
ftext(paste0(idx$rohwert, " / ", idx$max), fp_normal),
ftext(" Prozentrang: ", fp_label),
ftext(if (is.na(idx$pr)) "k. A." else paste0(idx$pr), fp_normal)
))
if (!is.null(idx$kriterien)) {
for (k in idx$kriterien) {
status_txt = if (isTRUE(k$erfuellt)) "[erfuellt] " else "[nicht erfuellt] "
status_fp = if (isTRUE(k$erfuellt)) fp_erfuellt else fp_nicht
for (i in seq_along(k$teile)) {
teil = k$teile[[i]]
if (i == 1) {
doc = body_add_fpar(doc, fpar(ftext(status_txt, status_fp), ftext(teil$text, fp_normal)))
} else {
doc = body_add_fpar(doc, fpar(
ftext(paste0(" ", k$verknuepfung, " "), fp_verknuepfung),
ftext(teil$text, fp_normal)
))
}
doc = body_add_fpar(doc, fpar(ftext(
paste0(" Antwort: ", teil$antwort, " (erforderlich: ", teil$erforderlich, ")"),
fp_antwort
)))
}
}
doc = body_add_fpar(doc, fpar(ftext(
if (isTRUE(idx$gesamtstatus)) "Kriterien fuer Verdachtsdiagnose erfuellt"
else "Kriterien fuer Verdachtsdiagnose nicht (vollstaendig) erfuellt",
if (isTRUE(idx$gesamtstatus)) fp_gesamt_ok else fp_gesamt_nok
)))
}
if (!is.null(idx$cutoff_hinweis)) {
doc = body_add_fpar(doc, fpar(ftext(idx$cutoff_hinweis, fp_cutoff)))
}
if (identical(idx$key, "beschwerden")) {
if (length(erg$beschwerden_ja_items) > 0) {
doc = body_add_fpar(doc, fpar(ftext("Angegebene Beschwerden (mit 'ja' beantwortet):", fp_label)))
for (b in erg$beschwerden_ja_items) {
doc = body_add_fpar(doc, fpar(ftext(paste0(b$nr, ". ", b$text), fp_normal)))
}
} else {
doc = body_add_fpar(doc, fpar(ftext("Keine der 53 Beschwerden wurde mit 'ja' beantwortet.", fp_normal)))
}
}
doc = body_add_par(doc, "", style = "Normal")
}
if (length(erg$screening_hinweise) > 0) {
doc = body_add_fpar(doc, fpar(ftext("Weitere Screening-Hinweise", fp_abschnitt)))
for (h in erg$screening_hinweise) {
doc = body_add_fpar(doc, fpar(ftext(paste0(h$nr, ". ", h$text), fp_normal)))
if (!is.null(h$folge_text)) {
doc = body_add_fpar(doc, fpar(ftext(paste0(" -> ", h$folge_text), fp_normal)))
}
}
doc = body_add_par(doc, "", style = "Normal")
}
doc = body_add_fpar(doc, fpar(ftext(SOMS2_DISCLAIMER, fp_disclaimer)))
doc
}
# Server ####
server = function(input, output, session) {
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$pseudonym) && nchar(trimws(query$pseudonym)) > 0) {
updateTextInput(session, "pseudonym", value = trimws(query$pseudonym))
}
})
observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$chiffre) && nchar(trimws(query$chiffre)) > 0) {
updateTextInput(session, "chiffre", value = toupper(trimws(query$chiffre)))
}
})
rohdaten_r = eventReactive(input$btn_suchen, {
chiffre = toupper(trimws(input$chiffre))
if (nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0) {
return(list(typ = "leere_eingabe", meldung = "Bitte Chiffre oder Pseudonym eingeben."))
}
if (nchar(trimws(input$pseudonym)) == 0 && !grepl("^[A-Z][0-9]{6}$", chiffre)) {
return(list(typ = "format_fehler", chiffre = chiffre))
}
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) {
return(list(typ = "skript_fehler",
meldung = paste0("Download-Skript nicht gefunden: ", PFAD_DOWNLOAD_SKRIPT)))
}
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
return(list(typ = "skript_fehler",
meldung = paste0("Pseudonym-Skript nicht gefunden: ", PFAD_PSEUDONYM_SKRIPT)))
}
ok = tryCatch({
source(PFAD_DOWNLOAD_SKRIPT, local = FALSE)
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok$ok) return(list(typ = "skript_fehler", meldung = ok$msg))
db_ordner = local({
ordner = dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT, mustWork = FALSE))
gefunden = NULL
for (i in 1:5) {
if (file.exists(file.path(ordner, "pseudonyme.db"))) {
gefunden = ordner
break
}
elternteil = dirname(ordner)
if (elternteil == ordner) break
ordner = elternteil
}
gefunden
})
if (is.null(db_ordner)) {
return(list(typ = "skript_fehler",
meldung = "pseudonyme.db wurde ausgehend vom Pseudonym-Skript-Ordner bis zu 5 Ebenen nach oben nicht gefunden."))
}
alter_wd = getwd()
on.exit(setwd(alter_wd), add = TRUE)
setwd(db_ordner)
ok_ps = tryCatch({
source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok_ps$ok) return(list(typ = "skript_fehler", meldung = ok_ps$msg))
if (!exists("daten_soms2", envir = .GlobalEnv)) {
return(list(typ = "skript_fehler",
meldung = "Objekt 'daten_soms2' wurde nach dem Sourcen des Download-Skripts nicht gefunden."))
}
if (!exists("pseudo", envir = .GlobalEnv)) {
return(list(typ = "skript_fehler",
meldung = "Objekt 'pseudo' wurde nach dem Sourcen des Pseudonym-Skripts nicht gefunden."))
}
daten_soms2 = get("daten_soms2", envir = .GlobalEnv)
pseudo = get("pseudo", envir = .GlobalEnv)
if (nchar(trimws(input$pseudonym)) > 0) {
pw_treffer = pseudo[pseudo$pseudonym == trimws(input$pseudonym), ]
if (nrow(pw_treffer) > 0) chiffre = toupper(trimws(pw_treffer$chiffre[1]))
}
treffer_ps = pseudo[pseudo$chiffre == chiffre, ]
if (nrow(treffer_ps) == 0) {
return(list(typ = "chiffre_nicht_gefunden", chiffre = chiffre))
}
alle_session_ids = unique(treffer_ps$pseudonym)
if (nchar(trimws(input$pseudonym)) > 0) alle_session_ids = trimws(input$pseudonym)
treffer_dat = daten_soms2[daten_soms2$session %in% alle_session_ids, ]
if (nrow(treffer_dat) == 0) {
return(list(typ = "session_nicht_gefunden", chiffre = chiffre))
}
warnung_daten = NULL
if (nrow(treffer_dat) > 1) {
n = nrow(treffer_dat)
treffer_dat = treffer_dat[order(treffer_dat$created, decreasing = TRUE), ]
datum_neu = tryCatch(
format(as.POSIXct(treffer_dat$created[1]), "%d.%m.%Y %H:%M"),
error = function(e) "unbekanntes Datum"
)
warnung_daten = paste0(
"Mehrere Ausfuellungen gefunden (", n, " Eintraege). ",
"Angezeigt wird die neueste vom ", datum_neu, "."
)
treffer_dat = treffer_dat[1, , drop = FALSE]
}
zeile = treffer_dat[1, , drop = FALSE]
datum_str = tryCatch(
format(as.POSIXct(zeile[["created"]][1]), "%d.%m.%Y"),
error = function(e) format(Sys.Date(), "%d.%m.%Y")
)
geschlecht_text = soms2_geschlecht_text(daten_soms2[["soms2_geschlecht"]], zeile[["soms2_geschlecht"]])
dsmiv_rohwert = soms2_score_items(zeile, daten_soms2, SOMS2_DSMIV_ITEMS)
dsmiv_max = soms2_dsmiv_max(geschlecht_text)
dsmiv_krit = soms2_dsmiv_kriterien(zeile, daten_soms2)
icd10_rohwert = soms2_score_einheiten(zeile, daten_soms2, SOMS2_ICD10_EINHEITEN)
icd10_max = soms2_icd10_max(geschlecht_text)
icd10_krit = soms2_icd10_kriterien(zeile, daten_soms2)
sad_rohwert = soms2_score_einheiten(zeile, daten_soms2, SOMS2_SAD_EINHEITEN)
sad_max = 12L
sad_krit = soms2_sad_kriterien(zeile, daten_soms2)
beschwerden_rohwert = soms2_score_items(zeile, daten_soms2, SOMS2_BESCHWERDEN_ITEMS)
beschwerden_max = soms2_beschwerden_max(geschlecht_text)
# Liste der einzelnen mit "ja" beantworteten Beschwerden (Items 1-53) fuer
# die Anzeige unter dem Beschwerdenindex.
beschwerden_ja_items = list()
for (it in SOMS2_BESCHWERDEN_ITEMS) {
if (isTRUE(soms2_ist_ja(daten_soms2[[it]], zeile[[it]]))) {
beschwerden_ja_items[[length(beschwerden_ja_items) + 1]] = list(
nr = as.integer(sub("^soms2_", "", it)),
text = soms2_item_text(daten_soms2, it)
)
}
}
screening_hinweise = list()
for (paar in SOMS2_ZUSATZ_ITEMS) {
if (isTRUE(soms2_ist_ja(daten_soms2[[paar$haupt]], zeile[[paar$haupt]]))) {
folge_text = NULL
if (!is.null(paar$folge) && isTRUE(soms2_ist_ja(daten_soms2[[paar$folge]], zeile[[paar$folge]]))) {
folge_text = soms2_item_text(daten_soms2, paar$folge)
}
screening_hinweise[[length(screening_hinweise) + 1]] = list(
nr = as.integer(sub("^soms2_", "", paar$haupt)),
text = soms2_item_text(daten_soms2, paar$haupt),
folge_text = folge_text
)
}
}
list(
typ = "ergebnis",
chiffre = chiffre,
datum_str = datum_str,
warnung_daten = warnung_daten,
geschlecht_text = geschlecht_text,
dsmiv_rohwert = dsmiv_rohwert, dsmiv_max = dsmiv_max, dsmiv_krit = dsmiv_krit,
icd10_rohwert = icd10_rohwert, icd10_max = icd10_max, icd10_krit = icd10_krit,
sad_rohwert = sad_rohwert, sad_max = sad_max, sad_krit = sad_krit,
beschwerden_rohwert = beschwerden_rohwert, beschwerden_max = beschwerden_max,
beschwerden_ja_items = beschwerden_ja_items,
screening_hinweise = screening_hinweise
)
})
# Leichtgewichtiges reactive() statt eventReactive: die Prozentraenge sollen
# sich sofort aktualisieren, wenn die Vergleichsnorm umgeschaltet wird, ohne
# dass erneut "Auswerten" geklickt werden muss (Rohdaten/Kriterien bleiben
# dabei unveraendert, nur der Normtabellen-Lookup wird neu berechnet).
ergebnis_r = reactive({
d = rohdaten_r()
if (d$typ != "ergebnis") return(d)
normwahl = input$normwahl
if (is.null(normwahl)) normwahl = "gesunde"
dsmiv_pr = soms2_pr_ergebnis("dsmiv", d$dsmiv_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
icd10_pr = soms2_pr_ergebnis("icd10", d$icd10_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
sad_pr = soms2_pr_ergebnis("sad", d$sad_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
besch_pr = soms2_pr_ergebnis("beschwerden", d$beschwerden_rohwert, d$geschlecht_text, normwahl, soms2_norm_a1, soms2_norm_a2)
indizes = list(
list(
key = "dsmiv", titel = "Somatisierungsindex DSM-IV",
rohwert = d$dsmiv_rohwert, max = d$dsmiv_max,
pr = dsmiv_pr$pr, pr_gesamt = dsmiv_pr$pr_gesamt,
kriterien = d$dsmiv_krit,
gesamtstatus = all(vapply(d$dsmiv_krit, function(k) isTRUE(k$erfuellt), logical(1))),
cutoff_hinweis = NULL
),
list(
key = "icd10", titel = "Somatisierungsindex ICD-10",
rohwert = d$icd10_rohwert, max = d$icd10_max,
pr = icd10_pr$pr, pr_gesamt = icd10_pr$pr_gesamt,
kriterien = d$icd10_krit,
gesamtstatus = all(vapply(d$icd10_krit, function(k) isTRUE(k$erfuellt), logical(1))),
cutoff_hinweis = NULL
),
list(
key = "sad", titel = "SAD-Index ICD-10",
rohwert = d$sad_rohwert, max = d$sad_max,
pr = sad_pr$pr, pr_gesamt = sad_pr$pr_gesamt,
kriterien = d$sad_krit,
gesamtstatus = all(vapply(d$sad_krit, function(k) isTRUE(k$erfuellt), logical(1))),
cutoff_hinweis = NULL
),
list(
key = "beschwerden", titel = "Beschwerdenindex Somatisierung",
rohwert = d$beschwerden_rohwert, max = d$beschwerden_max,
pr = besch_pr$pr, pr_gesamt = besch_pr$pr_gesamt,
kriterien = NULL,
gesamtstatus = NA,
cutoff_hinweis = if (isTRUE(d$beschwerden_rohwert >= 7)) SOMS2_BESCHWERDEN_CUTOFF_HINWEIS else NULL
)
)
c(d, list(normwahl = normwahl, indizes = indizes))
})
output$normwahl_hinweis_ui = renderUI({
if (identical(input$normwahl, "patienten")) {
div(class = "normwahl-hinweis",
"Die Patientennorm liegt nicht nach Geschlecht getrennt vor (eine Vergleichsgruppe fuer alle).")
} else {
NULL
}
})
output$ergebnis_ui = renderUI({
if (input$btn_suchen == 0) {
return(div(class = "abschnitt-karte start-hinweis",
"Bitte Chiffre oder Pseudonym eingeben und auf 'Auswerten' klicken."
))
}
erg = ergebnis_r()
if (erg$typ == "leere_eingabe") {
return(div(class = "alert-warnung", erg$meldung))
}
if (erg$typ == "format_fehler") {
return(div(class = "alert-warnung",
paste0("Ungueltige Chiffre '", erg$chiffre, "'. Erwartet: ein Grossbuchstabe gefolgt von 6 Ziffern (z. B. P000123).")))
}
if (erg$typ == "skript_fehler") {
return(div(class = "alert-fehler", tags$pre(style = "white-space:pre-wrap; margin:0;", erg$meldung)))
}
if (erg$typ == "chiffre_nicht_gefunden") {
return(div(class = "alert-fehler",
paste0("Chiffre '", erg$chiffre, "' wurde in der Pseudonym-Datenbank nicht gefunden.")))
}
if (erg$typ == "session_nicht_gefunden") {
return(div(class = "alert-fehler",
paste0("Kein SOMS-2-Datensatz fuer Chiffre '", erg$chiffre, "' gefunden.")))
}
meta_block = div(class = "meta-block",
tags$strong("Chiffre: "), erg$chiffre, " ",
tags$strong("Ausfuelldatum: "), erg$datum_str, " ",
tags$strong("Geschlecht: "), if (is.na(erg$geschlecht_text)) "k. A." else erg$geschlecht_text
)
kopf_karte = div(class = "abschnitt-karte",
meta_block,
if (!is.null(erg$warnung_daten)) div(class = "alert-warnung", erg$warnung_daten) else NULL
)
index_karten = lapply(erg$indizes, function(idx) {
pr_text = if (is.na(idx$pr)) "k. A." else paste0(idx$pr)
pr_zusatz = if (!identical(erg$normwahl, "patienten") &&
!is.na(erg$geschlecht_text) &&
!is.na(idx$pr_gesamt))
paste0(" (Gesamtnorm: ", idx$pr_gesamt, ")") else ""
kriterien_ui = if (!is.null(idx$kriterien)) {
tagList(
div(class = "kriterien-liste",
lapply(idx$kriterien, function(k) {
teile_ui = lapply(seq_along(k$teile), function(i) {
teil = k$teile[[i]]
tagList(
if (i > 1) div(class = "kriterium-verknuepfung", k$verknuepfung) else NULL,
div(class = "kriterium-frage", teil$text),
div(class = "kriterium-antwort",
paste0("Antwort: ", teil$antwort, " (erforderlich: ", teil$erforderlich, ")"))
)
})
div(class = "kriterium-zeile",
span(class = paste0("kriterium-status ",
if (isTRUE(k$erfuellt)) "kriterium-erfuellt" else "kriterium-nicht-erfuellt"),
if (isTRUE(k$erfuellt)) "erfuellt" else "nicht erfuellt"),
div(class = "kriterium-inhalt", teile_ui)
)
})
),
div(class = paste0("gesamtstatus-box ",
if (isTRUE(idx$gesamtstatus)) "gesamtstatus-erfuellt" else "gesamtstatus-nicht-erfuellt"),
if (isTRUE(idx$gesamtstatus)) "Kriterien fuer Verdachtsdiagnose erfuellt"
else "Kriterien fuer Verdachtsdiagnose nicht (vollstaendig) erfuellt")
)
} else NULL
cutoff_ui = if (!is.null(idx$cutoff_hinweis)) div(class = "cutoff-hinweis", idx$cutoff_hinweis) else NULL
beschwerden_liste_ui = if (identical(idx$key, "beschwerden")) {
if (length(erg$beschwerden_ja_items) > 0) {
tagList(
tags$h5("Angegebene Beschwerden (mit 'ja' beantwortet)",
style = "margin-top:14px; margin-bottom:6px; color:#555; font-size:0.95em;"),
lapply(erg$beschwerden_ja_items, function(b) {
div(class = "item-zeile",
div(class = "item-nr", paste0(b$nr, ".")),
div(class = "item-text", b$text)
)
})
)
} else {
div(class = "normwahl-hinweis",
"Keine der 53 Beschwerden wurde mit 'ja' beantwortet.")
}
} else NULL
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", idx$titel),
div(
span(class = "score-zahl", idx$rohwert),
span(class = "score-label", paste0(" / ", idx$max))
),
div(class = "pr-info", paste0("Prozentrang: ", pr_text, pr_zusatz)),
kriterien_ui,
cutoff_ui,
beschwerden_liste_ui
)
})
hinweise_karte = if (length(erg$screening_hinweise) > 0) {
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Weitere Screening-Hinweise"),
lapply(erg$screening_hinweise, function(h) {
tagList(
div(class = "item-zeile",
div(class = "item-nr", paste0(h$nr, ".")),
div(class = "item-text", h$text)
),
if (!is.null(h$folge_text))
div(class = "item-zeile item-unterpunkt",
div(class = "item-text", h$folge_text))
else NULL
)
})
)
} else NULL
tagList(kopf_karte, index_karten, hinweise_karte)
})
output$download_word = downloadHandler(
filename = function() {
erg = tryCatch(ergebnis_r(), error = function(e) NULL)
if (is.null(erg) || erg$typ != "ergebnis") return("SOMS2_Export.docx")
chiffre_esc = gsub("[^A-Za-z0-9_-]", "_", erg$chiffre)
ausfuelldatum_fn = tryCatch(
format(as.Date(erg$datum_str, "%d.%m.%Y"), "%Y%m%d"),
error = function(e) format(Sys.Date(), "%Y%m%d")
)
paste0("SOMS2_", chiffre_esc, "_", ausfuelldatum_fn, ".docx")
},
content = function(file) {
erg = tryCatch(ergebnis_r(), error = function(e) NULL)
if (is.null(erg) || erg$typ != "ergebnis") {
doc = read_docx()
doc = body_add_par(doc,
"Kein Datensatz geladen. Bitte zuerst Chiffre oder Pseudonym eingeben und 'Auswerten' klicken.",
style = "Normal")
print(doc, target = file)
return()
}
doc = tryCatch(
erstelle_soms2_docx(erg),
error = function(e) {
err_doc = read_docx()
body_add_par(err_doc,
paste0("Fehler beim Erstellen des Word-Dokuments: ", e$message),
style = "Normal")
}
)
print(doc, target = file)
}
)
}
# Start ####
shinyApp(ui, server)

View file

@ -0,0 +1,22 @@
rohwert,dsmiv_gesamt,dsmiv_m,dsmiv_w,icd10_gesamt,icd10_m,icd10_w,sad_gesamt,sad_m,sad_w,beschwerdenindex_gesamt,beschwerdenindex_m,beschwerdenindex_w
0,25,31,20,34,33,34,41,52,32,20,29,14
1,43,50,37,55,60,51,61,71,54,33,38,29
2,50,60,42,68,71,66,72,86,63,43,52,36
3,57,67,51,76,81,73,80,86,76,51,60,44
4,68,74,64,85,88,83,90,93,88,55,64,48
5,81,88,76,91,93,90,96,98,95,61,74,53
6,85,93,80,95,95,95,98,100,97,67,81,58
7,89,95,85,96,98,95,99,100,98,72,81,66
8,90,95,86,99,100,98,100,100,100,79,88,73
9,93,95,92,100,100,100,100,100,100,83,88,80
10,95,98,93,100,100,100,100,100,100,83,88,80
11,100,100,100,100,100,100,100,100,100,84,88,81
12,100,100,100,100,100,100,100,100,100,88,93,85
13,100,100,100,100,100,100,100,100,100,90,95,86
14,100,100,100,100,100,100,100,100,100,93,98,90
15,100,100,100,100,100,100,100,100,100,95,98,93
16,100,100,100,100,100,100,100,100,100,98,100,97
17,100,100,100,100,100,100,100,100,100,99,100,98
18,100,100,100,100,100,100,100,100,100,100,100,100
19,100,100,100,100,100,100,100,100,100,100,100,100
20,100,100,100,100,100,100,100,100,100,100,100,100
1 rohwert dsmiv_gesamt dsmiv_m dsmiv_w icd10_gesamt icd10_m icd10_w sad_gesamt sad_m sad_w beschwerdenindex_gesamt beschwerdenindex_m beschwerdenindex_w
2 0 25 31 20 34 33 34 41 52 32 20 29 14
3 1 43 50 37 55 60 51 61 71 54 33 38 29
4 2 50 60 42 68 71 66 72 86 63 43 52 36
5 3 57 67 51 76 81 73 80 86 76 51 60 44
6 4 68 74 64 85 88 83 90 93 88 55 64 48
7 5 81 88 76 91 93 90 96 98 95 61 74 53
8 6 85 93 80 95 95 95 98 100 97 67 81 58
9 7 89 95 85 96 98 95 99 100 98 72 81 66
10 8 90 95 86 99 100 98 100 100 100 79 88 73
11 9 93 95 92 100 100 100 100 100 100 83 88 80
12 10 95 98 93 100 100 100 100 100 100 83 88 80
13 11 100 100 100 100 100 100 100 100 100 84 88 81
14 12 100 100 100 100 100 100 100 100 100 88 93 85
15 13 100 100 100 100 100 100 100 100 100 90 95 86
16 14 100 100 100 100 100 100 100 100 100 93 98 90
17 15 100 100 100 100 100 100 100 100 100 95 98 93
18 16 100 100 100 100 100 100 100 100 100 98 100 97
19 17 100 100 100 100 100 100 100 100 100 99 100 98
20 18 100 100 100 100 100 100 100 100 100 100 100 100
21 19 100 100 100 100 100 100 100 100 100 100 100 100
22 20 100 100 100 100 100 100 100 100 100 100 100 100

View file

@ -0,0 +1,42 @@
rohwert,dsmiv,icd10,sad,beschwerdenindex_gesamt
0,2,4,4,1
1,5,10,9,2
2,8,22,17,3
3,13,35,27,5
4,22,46,39,7
5,30,56,50,9
6,37,67,63,13
7,46,78,73,17
8,56,84,80,23
9,65,90,89,27
10,72,96,94,30
11,77,98,99,35
12,81,99,100,41
13,86,100,100,47
14,90,100,100,51
15,92,100,100,56
16,95,100,100,63
17,96,100,100,66
18,98,100,100,69
19,98,100,100,74
20,99,100,100,77
21,99,100,100,80
22,99,100,100,82
23,99,100,100,84
24,100,100,100,87
25,100,100,100,88
26,100,100,100,90
27,100,100,100,92
28,100,100,100,94
29,100,100,100,95
30,100,100,100,96
31,100,100,100,97
32,100,100,100,98
33,100,100,100,98
34,100,100,100,98
35,100,100,100,99
36,100,100,100,99
37,100,100,100,99
38,100,100,100,99
39,100,100,100,99
40,100,100,100,100
1 rohwert dsmiv icd10 sad beschwerdenindex_gesamt
2 0 2 4 4 1
3 1 5 10 9 2
4 2 8 22 17 3
5 3 13 35 27 5
6 4 22 46 39 7
7 5 30 56 50 9
8 6 37 67 63 13
9 7 46 78 73 17
10 8 56 84 80 23
11 9 65 90 89 27
12 10 72 96 94 30
13 11 77 98 99 35
14 12 81 99 100 41
15 13 86 100 100 47
16 14 90 100 100 51
17 15 92 100 100 56
18 16 95 100 100 63
19 17 96 100 100 66
20 18 98 100 100 69
21 19 98 100 100 74
22 20 99 100 100 77
23 21 99 100 100 80
24 22 99 100 100 82
25 23 99 100 100 84
26 24 100 100 100 87
27 25 100 100 100 88
28 26 100 100 100 90
29 27 100 100 100 92
30 28 100 100 100 94
31 29 100 100 100 95
32 30 100 100 100 96
33 31 100 100 100 97
34 32 100 100 100 98
35 33 100 100 100 98
36 34 100 100 100 98
37 35 100 100 100 99
38 36 100 100 100 99
39 37 100 100 100 99
40 38 100 100 100 99
41 39 100 100 100 99
42 40 100 100 100 100

2552
SOMS2/renv.lock Normal file

File diff suppressed because it is too large Load diff

17
SOMS2/setup_renv.R Normal file
View file

@ -0,0 +1,17 @@
# Einmalig ausfuehren, bevor die App zum ersten Mal gestartet wird.
# Initialisiert renv und installiert alle benoetigten Pakete.
#
# formr wird hier installiert, weil das extern gesourcte Download-Skript
# (get_data_soms2.R) es benoetigt - die App selbst laedt formr nicht per
# library() und spricht nie direkt mit der formr-API.
# DBI und RSQLite werden vom gesourcten Pseudonym-Skript benoetigt,
# nicht direkt von der App selbst.
renv::init()
pkgs = c("shiny", "dplyr", "ggplot2", "haven", "officer", "DBI", "RSQLite", "formr")
install.packages(pkgs)
renv::snapshot()
message("Setup abgeschlossen. App starten mit: shiny::runApp()")