Teil 3 · Explorative Datenanalyse und deskriptive Statistik
Lösungen
09 · Deskriptive Statistik und die Table 1
Vergleiche zuerst mit deinem eigenen Versuch. Es gibt oft mehrere richtige Wege.
Aufgabe 1 – Mittelwert oder Median?
.py schreiben und mit dem ▶-Knopf in VS Code ausführen – oder Zeile für Zeile in die Python-Konsole. Setzt die in Modul 02 eingerichtete Umgebung voraus.import pandas as pd k = pd.read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv") for col in ["crp_mg_l", "sofa_score", "verweildauer_tage"]: mw = k[col].mean() med = k[col].median() print(f"{col:25s} Mittelwert={mw:.1f} Median={med:.1f} Diff={abs(mw-med):.1f}")
library(readr) k <- read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv", show_col_types = FALSE) for (col in c("crp_mg_l", "sofa_score", "verweildauer_tage")) { cat(sprintf("%-25s Mittelwert=%.1f Median=%.1f\n", col, mean(k[[col]], na.rm=TRUE), median(k[[col]], na.rm=TRUE))) }
Befund: crp_mg_l und verweildauer_tage zeigen die größte Differenz
(rechtschiefe Verteilung durch wenige sehr hohe Werte). sofa_score ist
symmetrischer, Mittelwert und Median liegen nahe beieinander.
Empfehlung: CRP und Verweildauer → Median [IQR]; SOFA-Score → je nach
Journal Median [IQR] (konservativ) oder Mittelwert ± SD.
Aufgabe 2 – Häufigkeiten und Anteile
.py schreiben und mit dem ▶-Knopf in VS Code ausführen – oder Zeile für Zeile in die Python-Konsole. Setzt die in Modul 02 eingerichtete Umgebung voraus.k = pd.read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv") # a) Herzinsuffizienz n_hi = (k["aufnahmegrund"] == "Herzinsuffizienz").sum() print(f"Herzinsuffizienz: {n_hi} ({n_hi/len(k):.1%})") # b) Hypertonie print(f"Hypertonie: {k['hypertonie'].mean():.1%}") # c) Kreuztabelle import pandas as pd xt = pd.crosstab(k["raucherstatus"], k["diabetes"], margins=True, margins_name="Gesamt") print(xt) print(pd.crosstab(k["raucherstatus"], k["diabetes"], normalize="index").round(3) * 100)
library(readr) k <- read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv", show_col_types = FALSE) # a) n_hi <- sum(k$aufnahmegrund == "Herzinsuffizienz") cat(sprintf("Herzinsuffizienz: %d (%.1f %%)\n", n_hi, n_hi/nrow(k)*100)) # b) cat(sprintf("Hypertonie: %.1f %%\n", mean(k$hypertonie)*100)) # c) print(table(k$raucherstatus, k$diabetes)) print(round(prop.table(table(k$raucherstatus, k$diabetes), margin = 1) * 100, 1))
Aufgabe 3 – Gruppenvergleich manuell
.py schreiben und mit dem ▶-Knopf in VS Code ausführen – oder Zeile für Zeile in die Python-Konsole. Setzt die in Modul 02 eingerichtete Umgebung voraus.import pandas as pd k = pd.read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv") for col in ["alter", "sofa_score", "crp_mg_l", "verweildauer_tage"]: g = k.groupby("verstorben_30d")[col] q = g.quantile([0.25, 0.50, 0.75]).unstack() print(f"\n{col}:") for group, row in q.iterrows(): print(f" Gruppe {group}: Median={row[0.50]:.1f} [{row[0.25]:.1f}; {row[0.75]:.1f}]")
library(readr) k <- read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv", show_col_types = FALSE) for (col in c("alter", "sofa_score", "crp_mg_l", "verweildauer_tage")) { cat(sprintf("\n%s:\n", col)) k |> dplyr::group_by(verstorben_30d) |> dplyr::summarise( zusammenfassung = sprintf("%.1f [%.1f; %.1f]", median(.data[[col]], na.rm = TRUE), quantile(.data[[col]], 0.25, na.rm = TRUE), quantile(.data[[col]], 0.75, na.rm = TRUE)) ) |> print() }
Befund: alter und sofa_score zeigen die auffälligsten Unterschiede
(Verstorbene sind median ~7 Jahre älter, 69 vs. 62 Jahre; SOFA-Score fast
doppelt so hoch, 5,5 vs. 3,0). crp_mg_l und verweildauer_tage unterscheiden
sich zwischen den Gruppen dagegen kaum. Das passt zur eingebauten Wahrheit des
Datensatzes (Modul 12 prüft das formal).
Aufgabe 4 – Table 1 nach Aufnahmegrund
.py schreiben und mit dem ▶-Knopf in VS Code ausführen – oder Zeile für Zeile in die Python-Konsole. Setzt die in Modul 02 eingerichtete Umgebung voraus.import pandas as pd from tableone import TableOne k = pd.read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv") table1 = TableOne( k, columns=["alter", "geschlecht", "diabetes", "sofa_score", "crp_mg_l"], categorical=["geschlecht", "diabetes"], groupby="aufnahmegrund", pval=True, ) print(table1.tabulate(tablefmt="simple"))
library(readr) library(gtsummary) k <- read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv", show_col_types = FALSE) k |> dplyr::select(alter, geschlecht, diabetes, sofa_score, crp_mg_l, aufnahmegrund) |> dplyr::mutate(diabetes = factor(diabetes, levels=c(0,1), labels=c("nein","ja"))) |> tbl_summary(by = aufnahmegrund, statistic = list(all_continuous() ~ "{median} [{p25}; {p75}]", all_categorical() ~ "{n} ({p}%)")) |> add_p()
Beobachtung: Septische Patient:innen haben mit Abstand den höchsten SOFA-Score (Ø 6,1 vs. ~3 in den anderen Gruppen, p < 0,001), sie sind bei Aufnahme am schwersten krank. Bei CRP zeigen sich dagegen keine klaren Gruppenunterschiede nach Aufnahmegrund (Sepsis hat hier sogar den niedrigsten Mittelwert), und auch das Alter unterscheidet sich zwischen den Gruppen kaum (p = 0,42). SOFA-Score ist hier der klinisch aussagekräftigste Unterschied.
Aufgabe 5 – Fehlende Werte in Table 1
.py schreiben und mit dem ▶-Knopf in VS Code ausführen – oder Zeile für Zeile in die Python-Konsole. Setzt die in Modul 02 eingerichtete Umgebung voraus.import pandas as pd from tableone import TableOne labs = pd.read_csv("https://schradern.github.io/data-science-coach/data/labor.csv") df = pd.read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv").merge(labs, on="patient_id", how="left") table1 = TableOne(df, columns=["alter", "bmi", "laktat_mmol_l", "kreatinin_mg_dl"], groupby="verstorben_30d", pval=False, missing=True) print(table1.tabulate(tablefmt="simple"))
library(readr) library(gtsummary) cohort <- read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv", show_col_types = FALSE) labs <- read_csv("https://schradern.github.io/data-science-coach/data/labor.csv", show_col_types = FALSE) df <- dplyr::left_join(cohort, labs, by="patient_id") df |> dplyr::select(alter, bmi, laktat_mmol_l, kreatinin_mg_dl, verstorben_30d) |> tbl_summary(by=verstorben_30d, missing="ifany") |> add_overall()
Befund: bmi fehlt bei ~6 % (zufällig fehlend, MCAR), laktat_mmol_l bei
~17 % (MAR: die Fehlwahrscheinlichkeit hängt vom beobachteten
SOFA-Score ab, Laktat wird bei höherem SOFA-Score klinisch häufiger gemessen,
fehlt also seltener bei schwerer kranken Patient:innen, siehe data/README.md
und Modul 14 (Umgang mit fehlenden Werten)).
Bonus – IQR des Laktats je Aufnahmegrund
.py schreiben und mit dem ▶-Knopf in VS Code ausführen – oder Zeile für Zeile in die Python-Konsole. Setzt die in Modul 02 eingerichtete Umgebung voraus.import pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import seaborn as sns labs = pd.read_csv("https://schradern.github.io/data-science-coach/data/labor.csv") df = pd.read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv").merge(labs, on="patient_id", how="left") iqr_tbl = df.groupby("aufnahmegrund")["laktat_mmol_l"].agg( q25=lambda x: x.quantile(0.25), median="median", q75=lambda x: x.quantile(0.75), ) iqr_tbl["iqr"] = iqr_tbl["q75"] - iqr_tbl["q25"] print(iqr_tbl.sort_values("iqr", ascending=False).round(2)) # Boxplot for visual comparison order = iqr_tbl.sort_values("iqr", ascending=False).index.tolist() fig, ax = plt.subplots(figsize=(8, 4)) sns.boxplot(data=df.dropna(subset=["laktat_mmol_l"]), x="aufnahmegrund", y="laktat_mmol_l", order=order, palette="Set2", ax=ax) ax.set_title("Laktat [mmol/l] nach Aufnahmegrund (nach IQR sortiert)") ax.set_xlabel("Aufnahmegrund") ax.set_ylabel("Laktat (mmol/l)") fig.tight_layout() fig.savefig("laktat_iqr_nach_aufnahmegrund.png", dpi=120)
library(tidyverse) cohort <- read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv", show_col_types = FALSE) labs <- read_csv("https://schradern.github.io/data-science-coach/data/labor.csv", show_col_types = FALSE) df <- left_join(cohort, labs, by="patient_id") df |> group_by(aufnahmegrund) |> summarise( q25 = quantile(laktat_mmol_l, 0.25, na.rm=TRUE), median = median(laktat_mmol_l, na.rm=TRUE), q75 = quantile(laktat_mmol_l, 0.75, na.rm=TRUE), iqr = q75 - q25 ) |> arrange(desc(iqr))
Befund: Die Streuung ist zwischen den Aufnahmegründen ähnlich groß (IQR zwischen ~1,4 und ~2,0 mmol/l); in dieser Kohorte hat COPD-Exazerbation die größte, Pneumonie die kleinste Streuung. Sepsis liegt im Mittel (nicht im Median) am höchsten, aber die Unterschiede zwischen den Gruppen sind klein und könnten in einer anderen Stichprobe anders ausfallen, das Muster ist mit n = 55–105 je Gruppe nicht robust genug für eine starke Aussage.