Teil 4 – Statistische Inferenz und medizinische Statistik
22 · Reproduzierbare Berichte mit Quarto
- Verstehen, warum Reproduzierbarkeit in der klinischen Forschung keine Kür, sondern eine wissenschaftliche Pflicht ist.
- Ein Analyseprojekt als Git-Repository anlegen und nachvollziehbar strukturieren.
- Datenschutz-Hygiene im Versionskontrollsystem einhalten: was nie in ein Repository darf.
- Den Analyse-Code reproduzierbar schreiben: Seed, relative Pfade, Paketversionen.
- Analyse und Bericht aus einer einzigen Quelle erzeugen (Quarto).
Auf dieser Seite
- Klinischer Aufhänger
- 1 Reproduzierbarkeit: was & warum
- 2 Das Projekt: Struktur und Repository
- 3 Datenschutz-Hygiene: was nie ins Repository darf
- 4 Der Analyse-Code in Python
- 5 Der Analyse-Code in R
- 6 Der Bericht: Quarto — aus einer Quelle
- Ein Quarto-Projekt anlegen
- Aufbau einer .qmd-Datei
- Einrichten (einmalig)
- Rendern
- Fallstricke und Merksätze
- Selbstcheck
Klinischer Aufhänger
Deine Analyse ist fertig, und sechs Monate später fragt ein Gutachter: „Kannst du zeigen, wie genau Tabelle 2 entstanden ist?" Ohne reproduzierbaren Workflow lautet die ehrliche Antwort oft: „Nicht mehr genau." Nicht nachvollziehbare Ergebnisse gefährden Folgepublikationen, Peer-Review-Entscheidungen und letztlich klinische Schlussfolgerungen, die auf deiner Arbeit beruhen.
Dieses Modul baut ein reproduzierbares Projekt von Grund auf: erst die Struktur und das Repository, dann die Datenschutz-Regeln, dann der Analyse-Code und zuletzt der Bericht, der alles zusammenführt.
1 Reproduzierbarkeit: was & warum
Was bedeutet reproduzierbar? Dieselbe Analyse, von einer anderen Person oder von dir selbst in zwei Jahren, auf demselben Datensatz, liefert bitgenau dasselbe Ergebnis.
Warum scheitert das so häufig?
| Ursache | Konsequenz |
|---|---|
| Zufallszahlen ohne Seed | Ergebnisse ändern sich bei jedem Lauf |
Absolute Pfade (C:\Users\…) |
Skript läuft nur auf einem Rechner |
| Paketversionen nicht dokumentiert | Update bricht die Analyse still |
| Manuelle Zwischenschritte (Excel-Kopieren) | Nicht rekonstruierbar |
| PHI im Repository | Datenschutzverletzung, rechtliche Konsequenzen |
Wie lösen wir es? Mit drei Prinzipien, die sich durch dieses Modul ziehen:
- Alles Code, kein Klicken, kein Kopieren.
- Seed setzen, Zufall muss deterministisch sein.
- Umgebung dokumentieren, welche Pakete in welcher Version.
Der häufigste Fehler ist nicht das vergessene Seed, sondern der selektive Workflow: Daten erst in Excel bereinigt, dann in Python geladen. Dieser Excel-Schritt ist unsichtbar und nicht reproduzierbar, er bricht die gesamte Kette. Sobald eine manuelle Editierung existiert, ist die Analyse nicht mehr vollständig nachvollziehbar.
Für FortgeschritteneVertiefung
Containerisierung (Docker, Singularity) sichert die gesamte Softwareumgebung inklusive Betriebssystem-Bibliotheken. Für Publikationen in Hochrisiko-Journals ist das zunehmend gefordert; für die meisten klinischen Projekte reicht eine virtuelle Python-Umgebung mit dokumentierten Versionen.
2 Das Projekt: Struktur und Repository
Ein Repository ist ein mit Git versioniertes Projektverzeichnis: Git speichert bei jedem git commit einen Snapshot, sodass jeder Stand später abrufbar bleibt. Modul 02 hat das eingerichtet (git init, git add, git commit) — hier geht es darum, was in dieses Repository gehört und wie es geordnet ist. Reproduzierbarkeit beginnt damit, dass alles zur Analyse Gehörende in einer klaren Struktur an einem Ort liegt.
Eine bewährte Struktur für ein klinisches Analyseprojekt:
mein-projekt/
├── README.md # Kurzbeschreibung, Ansprechpersonen, Laufanleitung
├── .gitignore # was Git ignorieren soll (s. Abschnitt 3)
├── _quarto.yml # Quarto-Projektkonfiguration (s. Abschnitt 6)
├── data/
│ ├── roh/ # unveränderliche Rohdaten — nie committen!
│ └── aufbereitet/ # durch Skripte erzeugt, ggf. committen
├── code/
│ ├── 01_bereinigung.py
│ ├── 02_analyse.py
│ └── 03_modell.py
├── berichte/
│ └── bericht.qmd # Quarto-Dokument (Text + Code)
└── ausgaben/
├── tabellen/
└── figures/
Die nummerierten Skripte in code/ machen die Ausführungsreihenfolge sofort erkennbar; der Bericht in berichte/ fügt ihre Ergebnisse zu einem Dokument zusammen. Wer vier Monate nach Abgabe zurückkommt, weiß sofort, wo er anfangen muss.
Versionierung durch Dateinamen (analyse_FINAL_v3_neu.py) ist kein Ersatz für Git. Sobald zwei Personen am Projekt arbeiten, sind Namenskollisionen und verlorene Änderungen unvermeidlich. Git ist kein Luxus, sondern Grundlage kollaborativer Reproduzierbarkeit.
Für FortgeschritteneVertiefung
Für komplexere Projekte lohnt ein Makefile oder dvc (Data Version Control), das die Abhängigkeiten zwischen Skripten explizit macht und nur die tatsächlich veränderten Schritte neu ausführt, besonders nützlich bei langen Preprocessing-Läufen.
3 Datenschutz-Hygiene: was nie ins Repository darf
Die wichtigste Regel: Keine Patientendaten in Git.
Erstelle eine .gitignore-Datei im Projektstamm, vor dem ersten Commit:
# Rohdaten und Dateien mit potenziellen PHI (Protected Health Information) - # nur der Rohdaten-Ordner, NICHT alle CSVs im Projekt (data/aufbereitet/ # darf laut Projektstruktur oben committet werden) data/roh/** data/roh/*.csv data/roh/*.xlsx data/roh/*.xls data/roh/*.sas7bdat data/roh/*.dta # Jupyter-Notebook-Ausgaben können Datenfragmente enthalten .ipynb_checkpoints/ # Betriebssystem-Metadaten .DS_Store Thumbs.db
Eine pauschale Regel wie *.csv (statt data/roh/*.csv) würde jede CSV im Projekt ignorieren, auch die aufbereiteten Ergebnisse in data/aufbereitet/, die laut Projektstruktur oben bewusst committet werden sollen. Ein späteres !data/aufbereitet/*.csv hilft hier nicht zuverlässig: Ist ein übergeordneter Ordner bereits ignoriert, greifen Negationen in .gitignore nicht mehr. Am robustesten: von Anfang an gezielt nur data/roh/ ausschließen, nicht pauschal nach Dateityp.
Zusätzliche Schutzmaßnahmen:
- Rohdaten nur auf genehmigten Servern oder verschlüsselten Laufwerken speichern.
- Pseudonymisierung vor jeder Weitergabe, auch intern.
- Aggregierte Ergebnisse (Tabellen, Grafiken ohne Einzelfälle) können in der Regel geteilt werden; prüfe aber deine Einrichtungsregeln und das Ethikvotum.
Git verfolgt die gesamte Geschichte aller Dateien. Eine einmal committete Datei lässt sich aus dem aktuellen Stand löschen, bleibt aber in git log --all sichtbar. Vollständiges Entfernen erfordert git filter-repo und ein erzwungenes Re-Push auf allen Klonen, erheblicher Aufwand mit Risiken für alle Mitarbeitenden. Besser: von Anfang an konsequent mit .gitignore arbeiten.
Für FortgeschritteneVertiefung
Automatisierte Pre-Commit-Hooks (z. B. detect-secrets oder git-secrets) scannen jeden Commit nach Mustern, die wie PHI oder API-Schlüssel aussehen, und blockieren ihn, bevor der Schaden entsteht. In Teams sind solche Hooks eine sinnvolle Pflicht.
4 Der Analyse-Code in Python
Der eigentliche Analyse-Code lebt als Skript in code/. Damit seine Zahlen morgen und auf einem anderen Rechner identisch sind, muss er die drei Prinzipien aus Abschnitt 1 einlösen: fester Seed, relative Pfade (nie C:\Users\…) und dokumentierte Paketversionen. code/python.py zeigt alle drei an der Kurs-Kohorte.
.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.SEED = 42 import numpy as np import pandas as pd np.random.seed(SEED) # Pflicht vor jeder Zufallsoperation cohort = pd.read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv") sample = cohort.sample(n=50, random_state=SEED) # eigener Seed, unabhaengig von numpy print("Mittleres Alter:", sample["alter"].mean().round(1)) import importlib.metadata # Paketversionen fuers Manifest for pkg in ["numpy", "pandas", "scikit-learn"]: print(pkg, importlib.metadata.version(pkg))
random_state in pandas und scikit-learn ist unabhängig von np.random.seed(), beide müssen gesetzt werden, wenn beide genutzt werden. Das vollständige code/python.py lädt die Daten über lib.helpers (relative Pfade, läuft vom Projektstamm aus) und schreibt die Versionsliste in ein Manifest (manifest.md), das du neben der Analyse committest — so ist die Umgebung dokumentiert, ohne dass jemand sie aus dem Gedächtnis rekonstruieren muss.
np.random.seed() beeinflusst nur den globalen NumPy-Zufallsgenerator. Bei parallelen Ausführungen oder verschachtelten Zufallsgeneratoren kann das zu unerwarteten Interaktionen führen. Robuster ist der neuere Ansatz: rng = np.random.default_rng(SEED) erzeugen und explizit weitergeben.
Für FortgeschritteneVertiefung
pip freeze > anforderungen.txt oder pip-compile (pip-tools) erzeugt eine vollständige, nachvollziehbare Liste aller installierten Versionen. Zusammen mit einer virtuellen Umgebung ist das die einfachste Form von Environments-as-Code, der erste Schritt zu vollständiger Reproduzierbarkeit.
5 Der Analyse-Code in R
Dieselben drei Garantien in R: set.seed() statt np.random.seed(), relative Pfade über die lib/-Helfer und sessionInfo() bzw. packageVersion() für die Versionsdokumentation. code/r.R zeigt sie an derselben Kohorte.
SEED <- 42 library(tidyverse) set.seed(SEED) # vor jeder Zufallsoperation cohort <- read_csv("https://schradern.github.io/data-science-coach/data/kohorte.csv", show_col_types = FALSE) sample <- cohort |> slice_sample(n = 50) cat("Mittleres Alter:", mean(sample$alter), "\n") si <- sessionInfo() # Versionen fuer den Bericht cat("R:", si$R.version$version.string, "\n") cat("tidyverse:", as.character(packageVersion("tidyverse")), "\n")
Das vollständige code/r.R lädt die Daten über lib/helpers.R (relative Pfade) und listet die Versionen der zentralen Pakete auf, das R-Gegenstück zum Python-Manifest.
set.seed() setzt den Zufallsgenerator global. Ein späterer set.seed()-Aufruf innerhalb einer Funktion kann den äußeren State überschreiben, das passiert leicht, wenn Hilfsfunktionen aus Paketen intern Seeds setzen. Robuster: withr::with_seed(SEED, expr) für lokal kontrollierte Reproduzierbarkeit.
Für FortgeschritteneVertiefung
renv ist das R-Äquivalent zu Pythons venv mit pip-tools: Es hält alle Paketversionen in einer renv.lock-Datei fest, die committet wird. Wer das Projekt klont, ruft renv::restore() auf und erhält exakt dieselbe Paketumgebung, ohne manuelle Installation.
6 Der Bericht: Quarto — aus einer Quelle
Skript und Bericht getrennt zu pflegen heißt: die Zahlen aus dem Skript per Copy-Paste in ein Word-Dokument übertragen — genau die manuelle Kette, die Reproduzierbarkeit bricht. Quarto (.qmd) legt Fließtext (Markdown) und Analyse-Code in eine Datei. quarto render führt jeden Code-Chunk aus und bettet Tabelle, Abbildung und Zahl direkt an Ort und Stelle ein.
Warum das besser ist als „Kopieren aus dem Skript":
- Tabelle und der Code, der sie erzeugt, stehen am selben Ort.
- Datensatz geändert? Einfach neu rendern, die Tabelle aktualisiert sich automatisch.
- Gutachter erhalten den Bericht inklusive Code; manuelle Übertragungsfehler entfallen.
Ein Quarto-Projekt anlegen
quarto create project default mein-bericht legt ein Projektverzeichnis mit einer _quarto.yml an. Diese Datei bündelt die Einstellungen, die für alle .qmd-Dateien des Projekts gelten — Ausgabeformat, Inhaltsverzeichnis, Ausgabeverzeichnis —, sodass einzelne Berichte nur noch ihren Titel und Inhalt tragen:
# _quarto.yml — gemeinsame Einstellungen fuer alle .qmd im Projekt project: type: default format: html: toc: true code-fold: true # Code vorhanden, aber standardmaessig eingeklappt
In der Projektstruktur aus Abschnitt 2 liegt der Bericht dann unter berichte/. Ein einzelnes .qmd funktioniert aber auch ohne Projekt und ohne _quarto.yml — dann stehen alle Einstellungen im Kopf des Dokuments selbst.
Aufbau einer .qmd-Datei
Jedes Dokument beginnt mit einem YAML-Header zwischen zwei ----Zeilen; er steuert Titel, Format und Ausführung:
title: "Intensivkohorte — Kurzauswertung" author: "Dr. med. Muster" date: today format: html execute: echo: true # Code im Bericht sichtbar (Transparenz!)
Darunter folgt der Bericht: normaler Markdown-Text, unterbrochen von Code-Chunks — Codeblöcken, deren Sprache in geschweiften Klammern hinter dem Code-Zaun steht ({python} oder {r}). Beim Rendern ersetzt Quarto jeden Chunk durch seine Ausgabe (Tabelle, Zahl, Abbildung). Ein vollständiges, lauffähiges Beispiel steht in code/bericht_beispiel.qmd — es lädt die Kohorte, rechnet eine Table 1 und rendert sie zu HTML. (Im Kurs liegt das Beispiel unter code/, weil jedes Modul seinen Code dort bündelt; in einem eigenen Projekt gehört der Bericht nach berichte/.)
Einrichten (einmalig)
- Quarto-CLI installieren über quarto.org/docs/get-started — macOS:
brew install quarto, Windows:winget install quartooder der Installer. Prüfen mitquarto --version. - Python-Kernel bereitstellen:
pip install jupyterin der Kursumgebung aus Modul 02. Quarto führt die{python}-Chunks über Jupyter aus (für{r}-Chunks stattdessen das R-Paketrmarkdown). - Nur für PDF: eine LaTeX-Distribution, am einfachsten
quarto install tinytex.
Rendern
quarto render code/bericht_beispiel.qmd erzeugt HTML (das im YAML-Header gesetzte format). Mit --to pdf oder --to docx entstehen die anderen Formate aus derselben Quelle. quarto preview code/bericht_beispiel.qmd rendert bei jedem Speichern neu und zeigt den Bericht live im Browser — praktisch beim Schreiben.
Enthält der YAML-Header echo: false, bleibt der Code im gerenderten Bericht unsichtbar, Gutachter können die Analyse nicht nachvollziehen. Für Publikationen immer echo: true setzen. code-fold: true ist ein guter Kompromiss: Der Code ist vorhanden, aber standardmäßig eingeklappt.
Für FortgeschritteneVertiefung
Mit freeze: true cached Quarto die Chunk-Ausgaben zwischen Renders. Das spart Zeit bei langen Berechnungen, ist aber gefährlich, wenn die Daten sich geändert haben. Im Zweifel: freeze: false und den Render sauber von vorne starten.
Fallstricke und Merksätze
- Seed vergessen ist der häufigste Fehler. Setze ihn ganz oben, bevor irgendein Zufallsgenerator aufgerufen wird.
random_statein scikit-learn undnp.random.seed()sind unabhängig, beide müssen gesetzt werden, wenn beide genutzt werden.- Relative Pfade über die
lib/-Helfer statt absoluter Pfade sind Grundvoraussetzung, dass Analysen auf anderen Rechnern laufen. - Paketversionen ändern sich still. Dokumentiere sie mit
pip freezeodersessionInfo(); noch besser: virtuelle Umgebungen mit gesperrten Versionen. - Manuelle Schritte sind reproduzierbarer Tod. Jede Excel-Editierung, die nicht im Code steht, kann nicht nachvollzogen werden.
echo: trueim Quarto-YAML, so sieht der Leser den Code. Für Publikationsanhänge ist das Gold wert.
Selbstcheck
git filter-repo und ein erzwungenes Re-Push auf alle Klone, aufwändig, fehleranfällig und nicht garantiert vollständig bei bereits weitergezogenen Klonen.quarto render führt alle Chunks aus und bettet Ergebnisse direkt ein, manuelle Übertragungsfehler zwischen Skript und Bericht entfallen vollständig.