Teil 2 – Datenimport, Datenbereinigung und Datenmanagement
07 · Patientendaten-Extraktion via FHIR und OMOP
- Das Kernkonzept von FHIR (Fast Healthcare Interoperability Resources) und seine wichtigsten Ressourcen (Patient, Observation) erklären.
- FHIR-Ressourcen über eine REST-API abfragen und JSON-Antworten in Python/R parsen.
- Das OMOP Common Data Model (CDM) und seine Rolle bei multizentrischen Studien verstehen.
- Einfache SQL-Abfragen auf OMOP-Standardtabellen (person, measurement) schreiben.
Auf dieser Seite
Klinischer Aufhänger
Deine Arbeitsgruppe plant eine retrospektive Studie zu Laktatverläufen. Anstatt Excel-Listen manuell auszufüllen, willst du die Daten direkt aus der Patient:innenakte (KAS) extrahieren. Das KAS deines Klinikums bietet eine FHIR-Schnittstelle und speichert Forschungsdaten im OMOP-Format. Hier lernst du, wie du eine Abfrage schreibst, die Patient:innenstammdaten und Laborwerte automatisiert zusammenführt.
1 Das FHIR-Konzept
FHIR strukturiert medizinische Daten in Ressourcen. Jede Ressource repräsentiert eine klinische Entität:
- Patient: Demografische Daten (Alter, Geschlecht).
- Observation: Messwerte (Laborwerte wie Laktat, Vitalparameter wie Blutdruck).
- Encounter: Ein Aufenthalt (z. B. Notaufnahme, Intensivstation).
FHIR nutzt das REST-Prinzip: Daten werden über URLs abgerufen.
GET https://fhir-server.de/fhir/Patient/123liefert den/die Patient:in mit ID 123 als JSON oder XML.GET https://fhir-server.de/fhir/Observation?code=2524-7liefert alle Laktat-Messwerte (LOINC-Code2524-7).
LOINC-Codes sehen alle ähnlich aus (fünfstellige Zahl, Bindestrich, Prüfziffer) — ein falscher Code liefert keinen Fehler, sondern lautlos die falschen Werte. 2571-8 ist zum Beispiel nicht Laktat, sondern Triglyzeride. Prüfe jeden Code, den du in eine echte Abfrage einbaust, gegen loinc.org oder die OMOP-Vokabeltabelle — verlasse dich nie auf den Namen einer Variablen aus einem Tutorial.
2 FHIR-Daten abfragen in Python
Ein echter FHIR-Server liefert ein Bundle: ein JSON-Objekt mit einer Liste entry, in der Patient- und Observation-Ressourcen gemischt vorkommen. Damit dieses Modul offline und reproduzierbar läuft, nutzen wir ein Beispiel-Bundle (https://schradern.github.io/data-science-coach/data/fhir_bundle.json) mit genau dieser Struktur, statt live gegen einen öffentlichen Testserver zu fragen — das Parsing ist identisch zu dem, was ein echter Server zurückgibt.
.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 json import urllib.request import pandas as pd # 1. Bundle laden (die https://schradern.github.io/data-science-coach/data/-URL liefert dasselbe JSON, das ein FHIR-Server # fuer GET .../Observation?code=... zurueckgeben wuerde). Wir nutzen urllib # aus der Standardbibliothek -- 'requests' ist nicht in der Kurs-Grundausstattung. with urllib.request.urlopen("https://schradern.github.io/data-science-coach/data/fhir_bundle.json") as response: bundle = json.load(response) # 2. Bundle in eine flache Tabelle entpacken: ein JSON-Objekt pro Ressource flat = pd.json_normalize(bundle["entry"], sep="_") # 3. Patient- und Observation-Ressourcen trennen patients = ( flat[flat["resource_resourceType"] == "Patient"] [["resource_id", "resource_gender", "resource_birthDate"]] .rename(columns={"resource_id": "patient_id", "resource_gender": "geschlecht", "resource_birthDate": "geburtsdatum"}) ) print(patients)
🗣 Code-Verbalisierung:
pd.json_normalize(bundle["entry"], sep="_")entpackt jede verschachtelte Ressource (resource.code.text,resource.valueQuantity.value, ...) in eine eigene, flache Spalte -- genau der Schritt, der aus tief verschachteltem FHIR-JSON eine Tabelle macht, die sich filtern und mergen lässt.- Patient- und Observation-Ressourcen liegen in derselben
entry-Liste, unterschieden nur durchresourceType-- deshalb filtern wir zweimal auf denselben DataFrame. code/python.pyführt diesen Schritt vollständig aus und fügt Patient:innen- und Observation-Tabelle zu einer Analysetabelle zusammen (den ganzen Weg bis zum OMOP-Modell zeigt die Abbildung in Abschnitt 4).
3 FHIR-Daten in R
In R kann das spezialisierte Paket fhircrackr komplexe FHIR-Strukturen direkt von einem Live-Server "cracken". Für dieses Beispiel-Bundle reicht das in R vorinstallierte jsonlite -- das Prinzip (verschachteltes JSON in eine flache Tabelle entpacken) ist dasselbe.
library(jsonlite) # 1. Bundle laden (lokale Datei oder URL) bundle <- fromJSON("https://schradern.github.io/data-science-coach/data/fhir_bundle.json", simplifyVector = FALSE) # 2. Patient-Ressourcen in eine Tabelle sammeln patient_rows <- list() for (entry in bundle$entry) { res <- entry$resource if (res$resourceType == "Patient") { patient_rows[[length(patient_rows) + 1]] <- data.frame( patient_id = res$id, geschlecht = res$gender, geburtsdatum = res$birthDate ) } } patients <- do.call(rbind, patient_rows) print(patients)
Für FortgeschritteneVertiefung
fhircrackr::fhir_crack() übernimmt für sehr komplexe, tief verschachtelte Bundles (z. B. mit wiederholten Unter-Ressourcen) automatisch, was hier von Hand mit einer Schleife passiert -- für ein Produktionssystem mit vielen Ressourcentypen ist es die robustere Wahl.
4 Das OMOP Common Data Model
Während FHIR für den Datenaustausch optimiert ist, dient das OMOP Common Data Model (CDM) der standardisierten Analyse. Krankenhäuser transformieren ihre lokalen Datenbanken in das OMOP-Format, damit derselbe Analysecode an Hunderten Kliniken weltweit laufen kann.

Kernelemente des OMOP-Modells:
- PERSON: Enthält eindeutige demografische Daten (Geschlecht, Geburtsjahr).
- MEASUREMENT: Enthält Messwerte und Laborergebnisse (verknüpft über Concept-IDs mit LOINC/SNOMED).
OMOP-SQL-Abfrage: Laktatwerte extrahieren
SELECT p.person_id, p.year_of_birth, p.gender_concept_id, m.measurement_date, m.value_as_number, m.unit_source_value FROM person p JOIN measurement m ON p.person_id = m.person_id WHERE m.measurement_concept_id = 3004327 -- Concept ID fuer Laktat (Beispiel) ORDER BY m.measurement_date DESC;
Die konkrete measurement_concept_id ist kein feststehender, auswendig lernbarer Wert -- sie hängt vom OMOP-Vokabular-Release deines CDM ab. Schlage die aktuelle Standard-Concept-ID für den gewünschten LOINC-Code immer in der OHDSI-Athena-Vokabeltabelle deiner Instanz nach, statt eine Zahl aus einem Tutorial ungeprüft zu übernehmen — derselbe Fehler wie beim LOINC-Code oben, nur eine Schicht tiefer im Modell.
Fallstricke und Merksätze
- FHIR-JSONs sind tief verschachtelt. Listen in Listen machen direktes Einlesen unmöglich. Du musst Daten explizit parsen oder Hilfsbibliotheken nutzen.
- Concepts statt Rohwerte. OMOP nutzt standardisierte Concept-IDs. Statt nach
"Laktat"zu suchen, musst du nach der spezifischen ID (3004327) filtern. - Datenschutz. FHIR-REST-APIs können sensible Patient:innendaten unverschlüsselt übertragen. Nutze im Kliniknetzwerk ausschließlich verschlüsselte HTTPS-Verbindungen.
Selbstcheck
measurement_concept_id in der Tabelle measurement, unter Verwendung der entsprechenden standardisierten Vokabular-ID (z. B. LOINC/SNOMED).pd.json_normalize(bundle["entry"], sep="_") flacht alle Ressourcen in eine Tabelle ab. Danach trennt man sie über resource_resourceType und verknüpft die Observations per resource_subject_reference mit der Patient-ID.2571-8 liefert Triglyzeride statt Laktat — lautlos. Codes gehören gegen loinc.org bzw. die Athena-Vokabeltabelle geprüft, nie aus einem Tutorial übernommen.