Pipeline Tutorial: HL7 v2 → OMOP CDM¶
The supported route is two explicit hops — HL7 v2 → FHIR R4 → OMOP — plus offline concept normalization of the carried diagnosis codes. This mirrors real-world practice (v2 feeds usually land as FHIR first) and keeps each hop auditable.
Runnable notebook: 25_pipeline_hl7v2_to_omop.ipynb.
The route¶
ADT feed ──parse──► segments ──hop 1──► FHIR resources ──hop 2──► OMOP rows ──► concept map
hl7v2 → fhir_r4 fhir_r4 → omop (Stage 3, offline)
Portiere ships hl7v2 → fhir_r4 and fhir_r4 → omop crossmaps; there is
deliberately no direct hl7v2 → omop map — chaining the two shipped maps is
the supported route.
Hop 1 — HL7 v2 → FHIR¶
Identical to the HL7 v2 → FHIR tutorial: parse
ER7 into segment dicts, map_record per segment, inject resourceType from
the entity map. The notebook adds a batch splitter for multi-message feeds
(split on MSH lines).
Hop 2 — FHIR → OMOP¶
fhir_to_omop = CrossStandardMapper("fhir_r4", "omop_cdm_v5.4")
fhir_to_omop.get_entity_map()
# {'Patient': 'person', 'Encounter': 'visit_occurrence',
# 'Condition': 'condition_occurrence', 'MedicationRequest': 'drug_exposure',
# 'Observation': 'measurement', ...}
omop_rows = {}
for r in fhir_resources:
table = fhir_to_omop.get_entity_map()[r["resourceType"]]
omop_rows.setdefault(table, []).append(
fhir_to_omop.map_record(r["resourceType"], r))
Write each table list to CSV (the notebook uses polars) and you have
structurally-OMOP data: person, visit_occurrence, condition_occurrence.
Concept normalization — the step chaining can't do¶
Crossmaps move structure; the diagnosis codes ride through as source
values. Landing standard condition_concept_ids is Stage-3 work:
concept_map = project.map_concepts(codes=[
{"code": "I10", "description": "Essential (primary) hypertension", "count": 1},
{"code": "E11.9", "description": "Type 2 diabetes mellitus …", "count": 1},
])
The notebook runs this against the bundled offline vocabulary (BM25 +
offline=True); production uses your full Athena export with SNOMED so ICD-10
codes resolve to SNOMED standard concepts via Maps to. Join the resulting
source_code → target_concept_id lookup onto condition_occurrence.
Production notes¶
- Persist the intermediate FHIR. Hop-1 output is a legitimate artifact — auditable, re-playable into other targets, and the natural place for profile validation.
- Determinism: both crossmaps are pure YAML transforms — same input, same output; version the crossmap files with your ETL release.
- person de-duplication: an ADT feed re-states PID per message; dedupe
personrows on the source MRN before loading (the notebook's demo feed has distinct patients, so it elides this).