DiagnostikApps/HZI/app.R
2026-09-22 18:35:43 +02:00

970 lines
33 KiB
R
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Präambel ####
library(shiny)
library(dplyr)
library(ggplot2)
library(haven)
library(readr)
library(officer)
library(DBI)
library(RSQLite)
AKZENT_FARBE = "#8B2635"
SKALEN_NAMEN = c(
A = "Kontrollieren, Wiederholen, Denken nach einer Handlung",
B = "Waschen, Reinigen",
C = "Ordnen",
D = "Zählen, Berühren, Sprechen",
E = "Denken von Worten, Bildern, Gedankenketten",
F = "Gedanken, sich selbst/anderen ein Leid zuzufügen"
)
KENNWERT_NAMEN = c(
A = "Skala A - Kontrollieren, Wiederholen, Denken nach einer Handlung",
B = "Skala B - Waschen, Reinigen",
C = "Skala C - Ordnen",
D = "Skala D - Zählen, Berühren, Sprechen",
E = "Skala E - Denken von Worten, Bildern, Gedankenketten",
F = "Skala F - Gedanken, sich selbst/anderen ein Leid zuzufügen",
G = "Gesamtskala",
P1 = "Prüfskala P1",
P2 = "Prüfskala P2",
P3 = "Prüfskala P3",
P4 = "Prüfskala P4"
)
TABELLE_KONFIDENZINTERVALLE = data.frame(
skala = c("A","B","C","D","E","F","G","P1","P2","P3","P4"),
r_tt = c(.88,.96,.94,.95,.86,.78,.93,.86,.90,.90,.91),
s_e = c(0.69,0.40,0.49,0.45,0.75,0.94,0.53,0.75,0.63,0.63,0.60),
cl_5proz = c(1.35,0.78,0.96,0.87,1.47,1.84,1.03,1.47,1.23,1.23,1.17),
cl_1proz = c(1.78,1.03,1.26,1.15,1.93,2.42,1.36,1.93,1.62,1.62,1.54),
stringsAsFactors = FALSE
)
PRUEFSKALEN_D_CRIT_5PROZ = 3
PRUEFSKALEN_D_CRIT_1PROZ = 4
PRUEFSKALEN_D_CRIT_01PROZ = 5
HZI_DISCLAIMER = paste0(
"Diese Auswertung ist ein Hilfsmittel fuer klinisches Fachpersonal und ersetzt ",
"keine klinische Diagnose. Die Interpretation obliegt der behandelnden Person. ",
"Angaben zu Normal- und Extrembereich sowie die 54%-Markierung des Originalprofilbogens ",
"sind in dieser digitalen Auswertung nicht abgebildet, da hierfuer keine im Manual ",
"textuell belegten Zahlenwerte vorliegen."
)
PRUEFSKALA_ERKLAERUNG = paste0(
"Die Prüfskalen P1P4 sind keine inhaltlichen Symptomskalen wie AF, sondern fassen die Items ",
"aller sechs Skalen auf derselben Schwierigkeitsstufe zusammen (z. B. P1 = alle Stufe-1-Items ",
"über alle Skalen hinweg). Sie dienen als Kontrollwert für die Konsistenz des Antwortverhaltens: ",
"weichen die vier Prüfskalen stark voneinander ab, deutet dies auf ein untypisches Antwortmuster ",
"hin (HZI-Non-Skalen-Typ), nicht auf ein bestimmtes Symptombild."
)
#### Infrastruktur ####
PFAD_DOWNLOAD_SKRIPT = "../API/get_data_hzi.R"
PFAD_PSEUDONYM_SKRIPT = "../get_pseudo.R"
PFAD_NORMTABELLEN_ORDNER = "./normtabellen"
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_NORMTABELLEN_ORDNER = normalizePath(absPath(PFAD_NORMTABELLEN_ORDNER), mustWork = FALSE)
app_css = "
.input-panel {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
padding: 16px;
margin-bottom: 20px;
background: #f5f5f5;
border-radius: 6px;
}
.btn-laden {
background-color: #8B2635;
border-color: #8B2635;
color: #fff;
}
.btn-laden:hover, .btn-laden:focus {
background-color: #6f1e2a;
border-color: #6f1e2a;
color: #fff;
}
.abschnitt-karte {
padding: 16px 20px;
margin-bottom: 18px;
border: 1px solid #ddd;
border-radius: 6px;
background: #fff;
}
.abschnitt-titel {
font-size: 1.15em;
font-weight: 700;
color: #8B2635;
margin-bottom: 12px;
}
.alert-fehler {
padding: 12px 16px;
margin-bottom: 14px;
background: #f8d7da;
border: 1px solid #c0392b;
border-radius: 5px;
color: #58151c;
}
.alert-warnung {
padding: 12px 16px;
margin-bottom: 14px;
background: #fff3cd;
border: 1px solid #b8860b;
border-radius: 5px;
color: #6b5100;
}
.item-zeile {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 4px 0;
border-bottom: 1px solid #eee;
}
.item-nr {
font-weight: 600;
min-width: 60px;
color: #8B2635;
flex-shrink: 0;
}
.item-text {
flex: 1;
}
.badge {
display: inline-block;
align-self: flex-start;
flex-shrink: 0;
padding: 2px 10px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
}
.badge-positiv {
background: #8B2635;
color: #fff;
}
.badge-negativ {
background: #e0e0e0;
color: #444;
}
.badge-fehlend {
background: #fff3cd;
color: #6b5100;
border: 1px solid #b8860b;
}
table.tabelle-werte {
width: 100%;
border-collapse: collapse;
}
table.tabelle-werte th, table.tabelle-werte td {
padding: 6px 10px;
border-bottom: 1px solid #ddd;
text-align: left;
}
table.tabelle-werte th {
background: #8B2635;
color: #fff;
}
"
app_css = gsub("#8B2635", AKZENT_FARBE, app_css, fixed = TRUE)
#### Helper ####
validiere_chiffre = function(chiffre_roh) {
chiffre = toupper(trimws(chiffre_roh))
if (!grepl("^[A-Z][0-9]{6}$", chiffre)) {
return(list(ok = FALSE, typ = "format_fehler", chiffre = chiffre))
}
list(ok = TRUE, chiffre = chiffre)
}
extrahiere_item_zuordnung = function(spaltennamen) {
treffer = regmatches(spaltennamen, regexec("^hzi_(\\d{3})_([a-f])([1-4])$", spaltennamen))
gefunden = vapply(treffer, function(x) length(x) == 4, logical(1))
if (sum(gefunden) == 0) {
stop("Keine Item-Spalten im Muster 'hzi_NNN_[a-f][1-4]' in daten_hzi gefunden.")
}
passende = treffer[gefunden]
data.frame(
spalte = spaltennamen[gefunden],
item_nr = vapply(passende, function(x) x[2], character(1)),
skala = toupper(vapply(passende, function(x) x[3], character(1))),
stufe = as.integer(vapply(passende, function(x) x[4], character(1))),
stringsAsFactors = FALSE
)
}
ermittle_stimmt_code = function(daten_hzi, spalte) {
labels = attr(daten_hzi[[spalte]], "labels")
if (is.null(labels)) {
stop(sprintf("Item-Spalte '%s': keine Kodierungs-Labels (labelled-Attribut 'labels') gefunden.", spalte))
}
namen = trimws(names(labels))
treffer = which(namen == "stimmt")
if (length(treffer) == 0) {
stop(sprintf("Item-Spalte '%s': keine Antwortoption exakt 'stimmt' in den Labels gefunden.", spalte))
}
if (length(treffer) > 1) {
stop(sprintf("Item-Spalte '%s': mehrdeutige Kodierung, mehrere Labels 'stimmt' gefunden.", spalte))
}
unname(labels[treffer])
}
bereinige_markdown = function(text) {
if (is.na(text)) return(text)
t = text
# Fuehrende Itemnummerierung entfernen, z. B. "6\. " oder "12. " (redundant zur Itemnummer-Badge)
t = sub(r"(^\s*\d+\\?[.\)]\s*)", "", t, perl = TRUE)
# Escapte Markdown-Sonderzeichen entschaerfen, z. B. "\." -> "."
t = gsub(r"(\\([[:punct:]]))", "\\1", t, perl = TRUE)
trimws(t)
}
ermittle_item_text = function(daten_hzi, spalte) {
text = attr(daten_hzi[[spalte]], "label", exact = TRUE)
if (is.null(text) || length(text) != 1 || is.na(text) || trimws(text) == "") {
return(NA_character_)
}
bereinige_markdown(trimws(text))
}
baue_item_info = function(daten_hzi) {
item_zuordnung = extrahiere_item_zuordnung(colnames(daten_hzi))
item_zuordnung$stimmt_code = vapply(
item_zuordnung$spalte,
function(sp) ermittle_stimmt_code(daten_hzi, sp),
numeric(1)
)
item_zuordnung$item_text = vapply(
item_zuordnung$spalte,
function(sp) ermittle_item_text(daten_hzi, sp),
character(1)
)
item_zuordnung
}
berechne_itemwerte = function(zeile, item_info) {
werte = vapply(seq_len(nrow(item_info)), function(i) {
spalte = item_info$spalte[i]
stimmt_code = item_info$stimmt_code[i]
roh = zeile[[spalte]][1]
val = suppressWarnings(as.numeric(roh))
if (is.na(val)) return(NA_integer_)
if (val == stimmt_code) 1L else 0L
}, integer(1))
item_info$wert = werte
item_info
}
berechne_rohwerte = function(item_info_mit_werten) {
zellwerte = item_info_mit_werten %>%
group_by(skala, stufe) %>%
summarise(rohwert = sum(wert, na.rm = TRUE), n_fehlend = sum(is.na(wert)), .groups = "drop")
skalenrohwerte = zellwerte %>%
group_by(skala) %>%
summarise(rohwert = sum(rohwert), n_fehlend = sum(n_fehlend), .groups = "drop")
gesamtrohwert = sum(skalenrohwerte$rohwert)
pruefskalen = zellwerte %>%
group_by(stufe) %>%
summarise(rohwert = sum(rohwert), n_fehlend = sum(n_fehlend), .groups = "drop")
rohwerte = c(setNames(skalenrohwerte$rohwert, skalenrohwerte$skala), G = gesamtrohwert)
rohwerte = c(rohwerte, setNames(pruefskalen$rohwert, paste0("P", pruefskalen$stufe)))
n_fehlend_gesamt = sum(item_info_mit_werten$wert %>% is.na())
list(
zellwerte = zellwerte,
skalenrohwerte = skalenrohwerte,
pruefskalen = pruefskalen,
rohwerte = rohwerte,
gesamtrohwert = gesamtrohwert,
n_fehlend = n_fehlend_gesamt
)
}
rohwert_zu_stanine = function(tabelle, rohwert) {
zeile = tabelle[tabelle$rohwert == rohwert, ]
if (nrow(zeile) == 0) {
stop(sprintf(
"Rohwert %s liegt außerhalb des gültigen Bereichs der Normtabelle (%s%s). Dies deutet auf einen Rechenfehler hin.",
rohwert, min(tabelle$rohwert, na.rm = TRUE), max(tabelle$rohwert, na.rm = TRUE)
))
}
zeile$stanine[1]
}
NORM_KEY_ZUORDNUNG = c(
A = "a", B = "b", C = "c", D = "d", E = "e", F = "f",
G = "gesamt", P1 = "p1", P2 = "p2", P3 = "p3", P4 = "p4"
)
berechne_stanine_alle = function(rohwerte, normtabellen) {
namen = names(rohwerte)
stanine = vapply(namen, function(n) {
key = NORM_KEY_ZUORDNUNG[[n]]
tabelle = normtabellen[[key]]
stanine_wert = rohwert_zu_stanine(tabelle, rohwerte[[n]])
if (is.na(stanine_wert)) NA_real_ else as.numeric(stanine_wert)
}, numeric(1))
names(stanine) = namen
stanine
}
pruefe_skalenpaar_differenzen = function(stanine_werte, differenzen_tabelle) {
ergebnisse = lapply(seq_len(nrow(differenzen_tabelle)), function(i) {
s1 = differenzen_tabelle$skala_1[i]
s2 = differenzen_tabelle$skala_2[i]
st1 = stanine_werte[[s1]]
st2 = stanine_werte[[s2]]
if (is.null(st1) || is.null(st2) || is.na(st1) || is.na(st2)) return(NULL)
d = abs(st1 - st2)
crit5 = differenzen_tabelle$d_crit_5proz[i]
crit1 = differenzen_tabelle$d_crit_1proz[i]
if (d < crit5) return(NULL)
signifikanz_1proz = d >= crit1
data.frame(
skala_1 = s1, skala_2 = s2, differenz = d,
signifikant_1proz = signifikanz_1proz,
stringsAsFactors = FALSE
)
})
ergebnisse = ergebnisse[!vapply(ergebnisse, is.null, logical(1))]
if (length(ergebnisse) == 0) return(data.frame())
do.call(rbind, ergebnisse)
}
formatiere_skalenpaar_text = function(zeile) {
basis = sprintf(
"Skala %s unterscheidet sich bedeutsam von Skala %s (Differenz %s, überschreitet kritische Differenz bei p<.05",
zeile$skala_1, zeile$skala_2, zeile$differenz
)
if (isTRUE(zeile$signifikant_1proz)) {
paste0(basis, ", auch p<.01).")
} else {
paste0(basis, ").")
}
}
pruefskalen_streuung_hinweis = function(stanine_werte) {
p_werte = stanine_werte[c("P1", "P2", "P3", "P4")]
p_werte = p_werte[!is.na(p_werte)]
if (length(p_werte) < 2) return(NULL)
spanne = max(p_werte) - min(p_werte)
schwelle = NULL
if (spanne >= PRUEFSKALEN_D_CRIT_01PROZ) {
schwelle = list(text = "p<.001", grenze = PRUEFSKALEN_D_CRIT_01PROZ)
} else if (spanne >= PRUEFSKALEN_D_CRIT_1PROZ) {
schwelle = list(text = "p<.01", grenze = PRUEFSKALEN_D_CRIT_1PROZ)
} else if (spanne >= PRUEFSKALEN_D_CRIT_5PROZ) {
schwelle = list(text = "p<.05", grenze = PRUEFSKALEN_D_CRIT_5PROZ)
}
if (is.null(schwelle)) return(NULL)
list(
spanne = spanne,
text = sprintf(
"Streuung der Prüfskalen: %s Stanine-Punkte, überschreitet die kritische Differenz bei %s — Hinweis auf möglicherweise untypisches Antwortmuster (HZI-Non-Skalen-Typ).",
spanne, schwelle$text
)
)
}
dissimulation_hinweis = function(gesamtrohwert) {
if (gesamtrohwert < 8 || gesamtrohwert > 145) {
return(paste0(
"Gesamtrohwert liegt außerhalb des in der Validierungsstichprobe beobachteten Wertebereichs (8145) — ",
"möglicher Hinweis auf verzerrtes Antwortverhalten (Unter- oder Übertreibung)."
))
}
NULL
}
formatiere_ci_text = function(stanine, cl_5proz) {
if (is.na(stanine)) return("")
sprintf("%s ± %s", stanine, cl_5proz)
}
item_status_text = function(wert) {
if (is.na(wert)) return("fehlend")
if (wert == 1) "stimmt" else "stimmt nicht"
}
item_status_klasse = function(wert) {
if (is.na(wert)) return("badge-fehlend")
if (wert == 1) "badge-positiv" else "badge-negativ"
}
sortiere_items_pro_skala = function(item_info) {
item_info$prioritaet = ifelse(is.na(item_info$wert), 3L, ifelse(item_info$wert == 1L, 1L, 2L))
item_info[order(item_info$skala, item_info$prioritaet, item_info$item_nr), ]
}
sichere_datumsparse = function(text) {
formate = c("%Y-%m-%d", "%d.%m.%Y", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S")
for (fmt in formate) {
d = suppressWarnings(as.Date(text, format = fmt))
if (!is.na(d)) return(d)
}
NA
}
#### Datenaufbereitung ####
ERWARTETE_NORMTABELLEN = list(
a = "skala_a_kontrollieren.csv",
b = "skala_b_waschen.csv",
c = "skala_c_ordnen.csv",
d = "skala_d_zaehlen.csv",
e = "skala_e_denken.csv",
f = "skala_f_selbstfremdschaedigung.csv",
gesamt = "gesamtskala.csv",
p1 = "pruefskala_p1.csv",
p2 = "pruefskala_p2.csv",
p3 = "pruefskala_p3.csv",
p4 = "pruefskala_p4.csv"
)
if (!dir.exists(PFAD_NORMTABELLEN_ORDNER)) {
stop(sprintf(
"Normtabellen-Ordner nicht gefunden. Geprüfter Pfad: '%s'. Bitte die 11 Normtabellen-CSVs dort ablegen.",
PFAD_NORMTABELLEN_ORDNER
))
}
normtabellen = list()
for (schluessel in names(ERWARTETE_NORMTABELLEN)) {
dateiname = ERWARTETE_NORMTABELLEN[[schluessel]]
dateipfad = file.path(PFAD_NORMTABELLEN_ORDNER, dateiname)
if (!file.exists(dateipfad)) {
stop(sprintf(
"Normtabelle fehlt: '%s'. Erwarteter Pfad: '%s'.",
dateiname, dateipfad
))
}
normtabellen[[schluessel]] = read_csv(
dateipfad,
col_types = cols(rohwert = col_integer(), stanine = col_integer()),
show_col_types = FALSE
)
}
pfad_differenzen = file.path(PFAD_NORMTABELLEN_ORDNER, "kritische_differenzen_skalenpaare.csv")
if (!file.exists(pfad_differenzen)) {
stop(sprintf(
"Normtabelle fehlt: 'kritische_differenzen_skalenpaare.csv'. Erwarteter Pfad: '%s'.",
pfad_differenzen
))
}
normtabellen$differenzen = read_csv(
pfad_differenzen,
col_types = cols(
skala_1 = col_character(),
skala_2 = col_character(),
d_crit_5proz = col_double(),
d_crit_1proz = col_double()
),
show_col_types = FALSE
)
#### UI ####
ui = fluidPage(
tags$head(tags$style(HTML(app_css))),
titlePanel("HZI (Langform) — Auswertung"),
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)")
)
),
uiOutput("ergebnis_ui")
)
#### Word-Export ####
erstelle_hzi_docx = function(erg) {
doc = read_docx()
doc = doc %>% body_add_fpar(fpar(
ftext(sprintf("HZI (Langform) — Auswertung — Chiffre %s — Ausfülldatum: %s",
erg$chiffre, erg$ausfuelldatum_anzeige),
fp_text(color = AKZENT_FARBE, bold = TRUE, font.size = 18))
))
if (!is.null(erg$ausfuelldatum_hinweis)) {
doc = doc %>% body_add_fpar(fpar(ftext(erg$ausfuelldatum_hinweis, fp_text(italic = TRUE, font.size = 10))))
}
if (length(erg$warnungen) > 0) {
doc = doc %>% body_add_fpar(fpar(ftext("Hinweise", fp_text(bold = TRUE, font.size = 14))))
for (w in erg$warnungen) {
doc = doc %>% body_add_fpar(fpar(ftext(w, fp_text(color = "#B8860B", font.size = 11))))
}
}
if (!is.null(erg$mehrfach_hinweis)) {
doc = doc %>% body_add_fpar(fpar(ftext(erg$mehrfach_hinweis, fp_text(color = "#B8860B", font.size = 11))))
}
bild_pfad_profil = tempfile(fileext = ".png")
ggsave(bild_pfad_profil, plot = erg$plot_profil, width = 7, height = 3.5, dpi = 150)
doc = doc %>% body_add_img(src = bild_pfad_profil, width = 6, height = 3)
doc = doc %>% body_add_fpar(fpar(ftext(PRUEFSKALA_ERKLAERUNG, fp_text(italic = TRUE, font.size = 9))))
bild_pfad_pruef = tempfile(fileext = ".png")
ggsave(bild_pfad_pruef, plot = erg$plot_pruefskalen, width = 7, height = 3.5, dpi = 150)
doc = doc %>% body_add_img(src = bild_pfad_pruef, width = 6, height = 3)
doc = doc %>% body_add_fpar(fpar(ftext("Werteübersicht", fp_text(bold = TRUE, font.size = 14))))
for (i in seq_len(nrow(erg$tabelle_werte))) {
zeile = erg$tabelle_werte[i, ]
doc = doc %>% body_add_fpar(fpar(ftext(sprintf(
"%s — Rohwert: %s, Stanine: %s, ± CL(5%%): %s",
zeile$name, zeile$rohwert, zeile$stanine_anzeige, zeile$ci_text
), fp_text(font.size = 11))))
}
doc = doc %>% body_add_fpar(fpar(ftext("Items pro Skala", fp_text(bold = TRUE, font.size = 14))))
items_sortiert = sortiere_items_pro_skala(erg$item_info)
status_farbe = c(stimmt = AKZENT_FARBE, "stimmt nicht" = "#666666", fehlend = "#B8860B")
for (sk in names(SKALEN_NAMEN)) {
doc = doc %>% body_add_fpar(fpar(ftext(
sprintf("Skala %s %s", sk, SKALEN_NAMEN[[sk]]), fp_text(bold = TRUE, font.size = 12)
)))
items_sk = items_sortiert[items_sortiert$skala == sk, ]
for (i in seq_len(nrow(items_sk))) {
zeile = items_sk[i, ]
text_anzeige = if (is.na(zeile$item_text)) {
sprintf("Item %s (kein Itemtext im Export vorhanden)", zeile$item_nr)
} else {
zeile$item_text
}
status = item_status_text(zeile$wert)
doc = doc %>% body_add_fpar(fpar(
ftext(sprintf("%s — %s — ", zeile$item_nr, text_anzeige), fp_text(font.size = 10)),
ftext(status, fp_text(bold = TRUE, font.size = 10, color = status_farbe[[status]]))
))
}
}
doc = doc %>% body_add_fpar(fpar(ftext(HZI_DISCLAIMER, fp_text(italic = TRUE, font.size = 9))))
doc
}
#### Server ####
server = function(input, output, session) {
# --- pseudonym-support-injection v1 ---
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)))
}
})
ergebnis_r = eventReactive(input$btn_suchen, {
chiffre_roh = input$chiffre
if (is.null(chiffre_roh) || (nchar(trimws(input$pseudonym)) == 0 && trimws(chiffre_roh) == "")) {
return(list(ok = FALSE, meldung = "Bitte eine Patientenchiffre eingeben."))
}
validierung = validiere_chiffre(chiffre_roh)
if (!validierung$ok) {
return(list(ok = FALSE, meldung = sprintf(
"Chiffre '%s' hat kein gültiges Format (erwartet: ein Buchstabe gefolgt von 6 Ziffern, z.B. P000123).",
validierung$chiffre
)))
}
chiffre = validierung$chiffre
if (!file.exists(PFAD_DOWNLOAD_SKRIPT)) {
return(list(ok = FALSE, meldung = sprintf(
"Download-Skript nicht gefunden. Geprüfter Pfad: '%s'.", PFAD_DOWNLOAD_SKRIPT
)))
}
if (!file.exists(PFAD_PSEUDONYM_SKRIPT)) {
return(list(ok = FALSE, meldung = sprintf(
"Pseudonym-Skript nicht gefunden. Geprüfter Pfad: '%s'.", 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(ok = FALSE, meldung = sprintf("Fehler beim Ausführen des Download-Skripts: %s", ok$msg)))
}
db_ordner = NULL
kandidat = dirname(normalizePath(PFAD_PSEUDONYM_SKRIPT))
for (i in 1:5) {
if (file.exists(file.path(kandidat, "pseudonyme.db"))) {
db_ordner = kandidat
break
}
neuer_kandidat = dirname(kandidat)
if (neuer_kandidat == kandidat) break
kandidat = neuer_kandidat
}
if (is.null(db_ordner)) {
return(list(ok = FALSE, meldung = "Datei 'pseudonyme.db' konnte in den übergeordneten Verzeichnissen des Pseudonym-Skripts nicht gefunden werden."))
}
alter_wd = getwd()
on.exit(setwd(alter_wd), add = TRUE)
setwd(db_ordner)
ok2 = tryCatch({
source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)
if (nchar(trimws(input$pseudonym)) > 0) {
.pw_wert = trimws(input$pseudonym)
.pw_tab = get("pseudo", envir = .GlobalEnv)
.pw_treffer = .pw_tab[.pw_tab$pseudonym == .pw_wert, ]
if (nrow(.pw_treffer) > 0) chiffre = toupper(trimws(.pw_treffer$chiffre[1]))
}
list(ok = TRUE)
}, error = function(e) list(ok = FALSE, msg = e$message))
if (!ok2$ok) {
return(list(ok = FALSE, meldung = sprintf("Fehler beim Ausführen des Pseudonym-Skripts: %s", ok2$msg)))
}
if (!exists("daten_hzi", envir = .GlobalEnv)) {
return(list(ok = FALSE, meldung = "Objekt 'daten_hzi' wurde nach dem Sourcen des Download-Skripts nicht gefunden."))
}
if (!exists("pseudo", envir = .GlobalEnv)) {
return(list(ok = FALSE, meldung = "Objekt 'pseudo' wurde nach dem Sourcen des Pseudonym-Skripts nicht gefunden."))
}
daten_hzi = get("daten_hzi", envir = .GlobalEnv)
pseudo = get("pseudo", envir = .GlobalEnv)
treffer_pseudo = pseudo[toupper(trimws(pseudo$chiffre)) == chiffre, ]
if (nrow(treffer_pseudo) == 0) {
return(list(ok = FALSE, meldung = sprintf("Keine Zuordnung für Chiffre '%s' in der Pseudonym-Tabelle gefunden.", chiffre)))
}
session_id = treffer_pseudo$pseudonym[1]
if (nchar(trimws(input$pseudonym)) > 0) session_id = trimws(input$pseudonym)
treffer_daten = daten_hzi[daten_hzi$session == session_id |
(("pseudonym" %in% colnames(daten_hzi)) && daten_hzi$pseudonym == session_id), ]
if (nrow(treffer_daten) == 0 && "pseudonym" %in% colnames(daten_hzi)) {
treffer_daten = daten_hzi[daten_hzi$pseudonym == session_id, ]
}
if (nrow(treffer_daten) == 0 && "session" %in% colnames(daten_hzi)) {
treffer_daten = daten_hzi[daten_hzi$session == session_id, ]
}
if (nrow(treffer_daten) == 0) {
return(list(ok = FALSE, meldung = sprintf("Keine HZI-Antworten für Chiffre '%s' (Session '%s') in den Exportdaten gefunden.", chiffre, session_id)))
}
mehrfach_hinweis = NULL
if (nrow(treffer_daten) > 1) {
if ("created" %in% colnames(treffer_daten)) {
treffer_daten = treffer_daten[order(treffer_daten$created, decreasing = TRUE), ]
treffer_daten = treffer_daten[1, ]
mehrfach_hinweis = "Hinweis: Der Bogen wurde mehrfach ausgefüllt. Es wurde die Auswertung mit dem neuesten Ausfülldatum verwendet."
} else {
treffer_daten = treffer_daten[1, ]
mehrfach_hinweis = "Hinweis: Der Bogen wurde mehrfach ausgefüllt. Es konnte kein Ausfülldatum zur Auswahl der neuesten Version ermittelt werden, es wurde der erste Treffer verwendet."
}
}
ausfuelldatum_hinweis = NULL
if ("created" %in% colnames(treffer_daten)) {
ausfuelldatum = sichere_datumsparse(as.character(treffer_daten$created[1]))
if (is.na(ausfuelldatum)) {
ausfuelldatum = Sys.Date()
ausfuelldatum_hinweis = "Ausfülldatum nicht in Exportdaten gefunden, Downloaddatum verwendet."
}
} else {
ausfuelldatum = Sys.Date()
ausfuelldatum_hinweis = "Ausfülldatum nicht in Exportdaten gefunden, Downloaddatum verwendet."
}
item_info_roh = tryCatch(
baue_item_info(daten_hzi),
error = function(e) e
)
if (inherits(item_info_roh, "error")) {
return(list(ok = FALSE, meldung = sprintf("Fehler bei der Item-Zuordnung: %s", conditionMessage(item_info_roh))))
}
item_info = berechne_itemwerte(treffer_daten, item_info_roh)
n_fehlend = sum(is.na(item_info$wert))
ergebnis_rohwerte = berechne_rohwerte(item_info)
stanine_werte = tryCatch(
berechne_stanine_alle(ergebnis_rohwerte$rohwerte, normtabellen),
error = function(e) e
)
if (inherits(stanine_werte, "error")) {
return(list(ok = FALSE, meldung = sprintf("Fehler bei der Stanine-Umrechnung: %s", conditionMessage(stanine_werte))))
}
skalenpaar_differenzen = pruefe_skalenpaar_differenzen(stanine_werte, normtabellen$differenzen)
streuung_hinweis = pruefskalen_streuung_hinweis(stanine_werte)
dissim_hinweis = dissimulation_hinweis(ergebnis_rohwerte$gesamtrohwert)
warnungen = c()
if (!is.null(dissim_hinweis)) warnungen = c(warnungen, dissim_hinweis)
if (!is.null(streuung_hinweis)) warnungen = c(warnungen, streuung_hinweis$text)
if (nrow(skalenpaar_differenzen) > 0) {
for (i in seq_len(nrow(skalenpaar_differenzen))) {
warnungen = c(warnungen, formatiere_skalenpaar_text(skalenpaar_differenzen[i, ]))
}
}
if (n_fehlend > 0) {
warnungen = c(warnungen, sprintf(
"%s Item(s) wurden nicht beantwortet (fehlende Werte) und wurden bei der Rohwertberechnung nicht mitgezählt.",
n_fehlend
))
}
tabelle_werte = data.frame(
kennwert = names(stanine_werte),
stringsAsFactors = FALSE
)
tabelle_werte$name = KENNWERT_NAMEN[tabelle_werte$kennwert]
tabelle_werte$rohwert = ergebnis_rohwerte$rohwerte[tabelle_werte$kennwert]
tabelle_werte$stanine_num = stanine_werte[tabelle_werte$kennwert]
tabelle_werte$stanine_anzeige = ifelse(is.na(tabelle_werte$stanine_num), "", as.character(tabelle_werte$stanine_num))
tabelle_werte = merge(tabelle_werte, TABELLE_KONFIDENZINTERVALLE, by.x = "kennwert", by.y = "skala", sort = FALSE)
tabelle_werte$ci_text = mapply(formatiere_ci_text, tabelle_werte$stanine_num, tabelle_werte$cl_5proz)
reihenfolge = c("A","B","C","D","E","F","G","P1","P2","P3","P4")
tabelle_werte = tabelle_werte[match(reihenfolge, tabelle_werte$kennwert), ]
list(
ok = TRUE,
chiffre = chiffre,
ausfuelldatum = ausfuelldatum,
ausfuelldatum_anzeige = format(ausfuelldatum, "%d.%m.%Y"),
ausfuelldatum_hinweis = ausfuelldatum_hinweis,
mehrfach_hinweis = mehrfach_hinweis,
rohwerte = ergebnis_rohwerte$rohwerte,
stanine_werte = stanine_werte,
tabelle_werte = tabelle_werte,
warnungen = warnungen,
item_info = item_info
)
})
baue_profil_plot = function(erg) {
daten = data.frame(
skala = factor(names(SKALEN_NAMEN), levels = names(SKALEN_NAMEN)),
stanine = as.numeric(erg$stanine_werte[names(SKALEN_NAMEN)])
)
ggplot(daten, aes(x = skala, y = stanine)) +
geom_hline(yintercept = 5, linetype = "dashed", color = "grey40") +
annotate("text", x = -Inf, y = 5.3, label = "Mittelwert Normstichprobe", hjust = -0.05, size = 3, color = "grey40") +
geom_col(fill = AKZENT_FARBE, width = 0.5, na.rm = TRUE) +
scale_y_continuous(limits = c(0, 9), breaks = 1:9) +
labs(x = "Skala", y = "Stanine", title = "Profil Skalen AF") +
theme_minimal(base_size = 12)
}
baue_pruefskalen_plot = function(erg) {
daten = data.frame(
skala = factor(c("P1","P2","P3","P4"), levels = c("P1","P2","P3","P4")),
stanine = as.numeric(erg$stanine_werte[c("P1","P2","P3","P4")])
)
ggplot(daten, aes(x = skala, y = stanine)) +
geom_hline(yintercept = 5, linetype = "dashed", color = "grey40") +
annotate("text", x = -Inf, y = 5.3, label = "Mittelwert Normstichprobe", hjust = -0.05, size = 3, color = "grey40") +
geom_col(fill = AKZENT_FARBE, width = 0.5, na.rm = TRUE) +
scale_y_continuous(limits = c(0, 9), breaks = 1:9) +
labs(x = "Prüfskala", y = "Stanine", title = "Profil Prüfskalen P1P4") +
theme_minimal(base_size = 12)
}
baue_gesamt_plot = function(erg) {
stanine_g = as.numeric(erg$stanine_werte["G"])
daten = data.frame(x = 1:9, y = 1)
ggplot(daten, aes(x = x, y = y)) +
geom_col(fill = "grey85", width = 1, color = "white") +
geom_col(data = data.frame(x = stanine_g, y = 1), aes(x = x, y = y), fill = AKZENT_FARBE, width = 1) +
scale_x_continuous(breaks = 1:9, limits = c(0.5, 9.5)) +
labs(x = "Stanine", y = NULL, title = "Gesamtskala G") +
theme_minimal(base_size = 12) +
theme(axis.text.y = element_blank(), axis.ticks.y = element_blank())
}
output$ergebnis_ui = renderUI({
erg = ergebnis_r()
if (is.null(erg)) return(NULL)
if (!isTRUE(erg$ok)) {
return(div(class = "alert-fehler", erg$meldung))
}
blocks = list()
if (!is.null(erg$mehrfach_hinweis)) {
blocks = c(blocks, list(div(class = "alert-warnung", erg$mehrfach_hinweis)))
}
if (!is.null(erg$ausfuelldatum_hinweis)) {
blocks = c(blocks, list(div(class = "alert-warnung", erg$ausfuelldatum_hinweis)))
}
if (length(erg$warnungen) > 0) {
blocks = c(blocks, list(
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Hinweise"),
tags$ul(lapply(erg$warnungen, function(w) tags$li(class = "alert-warnung", w)))
)
))
}
items_sortiert = sortiere_items_pro_skala(erg$item_info)
blocks = c(blocks, list(
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Profil Skalen AF"),
plotOutput("plot_profil", height = "320px")
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Gesamtskala"),
plotOutput("plot_gesamt", height = "180px"),
p(sprintf("Rohwert: %s, Stanine: %s, ± CL(5%%): %s",
erg$rohwerte[["G"]],
ifelse(is.na(erg$stanine_werte[["G"]]), "", erg$stanine_werte[["G"]]),
erg$tabelle_werte[erg$tabelle_werte$kennwert == "G", "ci_text"]))
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Prüfskalen P1P4"),
p(style = "font-style: italic; color: #555; font-size: 0.9em;", PRUEFSKALA_ERKLAERUNG),
tags$table(class = "tabelle-werte",
tags$thead(tags$tr(
tags$th("Prüfskala"), tags$th("Rohwert"), tags$th("Stanine"), tags$th("± CL (5%)")
)),
tags$tbody(
lapply(c("P1", "P2", "P3", "P4"), function(k) {
zeile = erg$tabelle_werte[erg$tabelle_werte$kennwert == k, ]
tags$tr(
tags$td(zeile$name), tags$td(zeile$rohwert),
tags$td(zeile$stanine_anzeige), tags$td(zeile$ci_text)
)
})
)
),
plotOutput("plot_pruefskalen", height = "320px")
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Werteübersicht"),
tableOutput("tabelle_werte")
),
div(class = "abschnitt-karte",
div(class = "abschnitt-titel", "Items pro Skala"),
lapply(names(SKALEN_NAMEN), function(sk) {
items_sk = items_sortiert[items_sortiert$skala == sk, ]
tags$details(
tags$summary(sprintf("Skala %s %s (Rohwert: %s)", sk, SKALEN_NAMEN[[sk]], erg$rohwerte[[sk]])),
lapply(seq_len(nrow(items_sk)), function(i) {
zeile = items_sk[i, ]
text_anzeige = if (is.na(zeile$item_text)) {
sprintf("Item %s (kein Itemtext im Export vorhanden)", zeile$item_nr)
} else {
zeile$item_text
}
div(class = "item-zeile",
span(class = "item-nr", zeile$item_nr),
span(class = "item-text", text_anzeige),
span(class = paste("badge", item_status_klasse(zeile$wert)), item_status_text(zeile$wert))
)
})
)
})
)
))
div(blocks)
})
output$plot_profil = renderPlot({
erg = ergebnis_r()
req(erg$ok)
baue_profil_plot(erg)
})
output$plot_pruefskalen = renderPlot({
erg = ergebnis_r()
req(erg$ok)
baue_pruefskalen_plot(erg)
})
output$plot_gesamt = renderPlot({
erg = ergebnis_r()
req(erg$ok)
baue_gesamt_plot(erg)
})
output$tabelle_werte = renderTable({
erg = ergebnis_r()
req(erg$ok)
anzeige = erg$tabelle_werte[, c("name", "rohwert", "stanine_anzeige", "ci_text")]
colnames(anzeige) = c("Skala/Kennwert", "Rohwert", "Stanine", "± CL (5%)")
anzeige
}, striped = TRUE, bordered = TRUE)
output$download_word = downloadHandler(
filename = function() {
erg = ergebnis_r()
if (is.null(erg) || !isTRUE(erg$ok)) return("HZI_Auswertung.docx")
datum_fn = format(erg$ausfuelldatum, "%Y%m%d")
chiffre_esc = gsub("[^A-Za-z0-9]", "", erg$chiffre)
paste0("HZI_", chiffre_esc, "_", datum_fn, ".docx")
},
content = function(file) {
erg = ergebnis_r()
req(erg$ok)
erg$plot_profil = baue_profil_plot(erg)
erg$plot_pruefskalen = baue_pruefskalen_plot(erg)
doc = erstelle_hzi_docx(erg)
print(doc, target = file)
}
)
}
#### Start ####
shinyApp(ui = ui, server = server)