Data Science · Klinik Klinische Datenanalyse & Machine Learning
Ansicht
Lerntiefe
Codeansicht
Farbschema

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?

Python
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}")
R
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

Python
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)
R
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

Python
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}]")
R
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

Python
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"))
R
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

Python
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"))
R
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

Python
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)
R
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.