Encounter to VISIT¶
Frequently the largest single piece of engineering in a FHIR to OMOP pipeline, and the one where two competent teams most often produce different answers from the same source.
Why it is hard¶
Encounter in FHIR is a resource representing an interaction with the health system. The specification does not prescribe granularity, and source systems have chosen wildly different granularities.
The same four-day hospital admission can arrive as:
- One Encounter covering the whole stay, with no internal structure
- A hierarchy: one parent for the admission, children for ward and ICU segments, linked by
partOf - Many flat Encounters: one per clinical service consultation, per department, or per day, with no linking at all
- A mixture, because the source system changed its practice in 2019 and both patterns are in the data
OMOP wants something specific: VISIT_OCCURRENCE representing a coherent care episode, and VISIT_DETAIL holding finer-grained segments underneath it. Getting from any of the four shapes above to that structure requires a rule, and the rule is a design decision rather than a lookup.
The target¶
VISIT_OCCURRENCE:
| Column | Source |
|---|---|
visit_occurrence_id |
Assigned |
person_id |
Resolved from Encounter.subject |
visit_concept_id |
From Encounter.class, mapped to the OMOP Visit domain |
visit_start_date, visit_start_datetime |
Encounter.period.start |
visit_end_date, visit_end_datetime |
Encounter.period.end |
visit_type_concept_id |
Provenance |
provider_id |
Encounter.participant |
care_site_id |
Encounter.serviceProvider or location |
visit_source_value, visit_source_concept_id |
Encounter.class and Encounter.type |
admitted_from_concept_id, admitted_from_source_value |
Encounter.hospitalization.admitSource |
discharged_to_concept_id, discharged_to_source_value |
Encounter.hospitalization.dischargeDisposition |
preceding_visit_occurrence_id |
Derived from sequence |
VISIT_DETAIL mirrors this with visit_detail_id, a required visit_occurrence_id, and parent_visit_detail_id for nesting.
visit_concept_id¶
Encounter.class uses the HL7 v3 ActCode value set: IMP inpatient, AMB ambulatory, EMER emergency, OBSENC observation, HH home health, VR virtual, and others.
The common OMOP Visit domain concepts:
| Concept | Meaning | Typical FHIR class |
|---|---|---|
| 9201 | Inpatient Visit | IMP |
| 9202 | Outpatient Visit | AMB |
| 9203 | Emergency Room Visit | EMER |
| 262 | Emergency Room and Inpatient Visit | derived when an ED stay leads to admission |
| 581379 | Inpatient Hospital Stay | some conventions |
| 42898160 | Long Term Care Visit | ACUTE, NONAC variants and SNF context |
| 5083 | Telehealth | VR |
Two subtleties.
Concept 262, emergency room and inpatient visit, has no FHIR equivalent. It represents the common pattern where a person presents to the emergency department and is admitted, and the whole thing is one care episode. Whether you construct it, and under what rule, is a design decision that affects utilization studies significantly.
Encounter.class is often less informative than Encounter.type, which carries a more specific local or SNOMED code. Consider both, and record the more informative one in visit_source_value.
The construction rules¶
Four patterns, matched to the four source shapes.
Pattern A: explicit hierarchy¶
The source uses partOf, as in R. Alvarez's bundle.
Rule: the root Encounter of each partOf tree becomes a VISIT_OCCURRENCE. Every descendant becomes a VISIT_DETAIL pointing at it, with parent_visit_detail_id reflecting deeper nesting.
This is the easy case and it is what the FHIR to OMOP Implementation Guide can most cleanly specify, because the source has already declared the structure.
R. Alvarez:
VISIT_OCCURRENCE
1 | person 1 | 9201 Inpatient | 2024-09-08 14:12 → 2024-09-12 11:30
VISIT_DETAIL
1 | visit 1 | 9201 | 2024-09-08 16:40 → 2024-09-10 08:15 | 7 West Medical
2 | visit 1 | 9201 | 2024-09-10 08:15 → 2024-09-12 11:30 | Medical ICU
Note that the parent starts at 14:12 and the first child starts at 16:40. The two and a half hours between admission and ward arrival are inside the visit and inside no visit detail. That is correct and it is also the kind of gap that breaks naive coverage assertions.
Pattern B: single encounter per stay¶
No hierarchy, one Encounter covering the episode.
Rule: one VISIT_OCCURRENCE, no VISIT_DETAIL. Straightforward, and VISIT_DETAIL stays empty for this source, which is acceptable.
The loss is real: ward and unit transfers are not recoverable. If ICU exposure is relevant to your research, note in your documentation that this source cannot support it.
Pattern C: many flat encounters¶
Multiple unlinked Encounters covering the same care episode. This is the hard one.
Rule: collapse by proximity and class. Group Encounters for the same person whose periods overlap or abut within a tolerance, where the class is compatible, into one VISIT_OCCURRENCE spanning the group. Each contributing Encounter becomes a VISIT_DETAIL.
Every parameter in that sentence is a decision:
- The tolerance. Zero hours means a one-minute gap splits a stay. Twenty-four hours means two genuinely separate outpatient visits on consecutive days merge into one. Common practice sits between one and twelve hours for inpatient collapse, and the right value depends on how your source records transitions.
- Class compatibility. Should an
EMERencounter merge with an immediately followingIMPencounter? If yes, is the result concept 262, or 9201, or 9203? This choice alone can move emergency department utilization counts by a large margin. - Same-day outpatient. Two clinic visits on the same day at different departments: one visit or two? Both answers appear in production instances.
Whatever you choose, write it down with the parameter values, and report how many source Encounters collapsed into how many visits. That ratio is one of the most useful single numbers for someone evaluating your instance.
Pattern D: mixture¶
Both patterns present, usually because the source system changed practice.
Rule: detect and branch. Apply Pattern A where partOf is present and Pattern C where it is not. Then check the boundary period where practice changed, because that is where the two rules produce inconsistent results for clinically identical situations.
Report visit counts by year. A discontinuity at the practice-change boundary is expected and needs to be documented so an analyst does not read it as a change in care delivery.
Attaching events to visits¶
Every clinical event that happened during a visit should carry visit_occurrence_id, and where applicable visit_detail_id.
FHIR usually gives you this directly. Observation.encounter, MedicationAdministration.context, Procedure.encounter. When the reference is present, resolve it to whichever visit or visit detail that Encounter became.
When it is absent, you have a choice. Attach by time, matching the event's date to a visit period, or leave the link null. Attaching by time is convenient and creates false links for events that genuinely happened outside a visit, such as an outside laboratory result received during an admission. Leaving it null is honest and reduces the usefulness of visit-based analyses.
A reasonable middle path: attach by time only when the match is unambiguous, meaning the event falls inside exactly one visit period, and leave it null otherwise. Report the proportion attached by reference versus by inference.
Note the mapping asymmetry: if an Encounter became a VISIT_DETAIL, events referencing it need both the detail id and the id of the visit that contains it.
Where this quietly breaks¶
Boundary timestamps that coincide. R. Alvarez's ICU detail ends at exactly the moment the visit ends. Interval logic using inclusive or exclusive comparisons inconsistently produces off-by-one errors, double-counted days, and zero-length intervals. Pick a convention, apply it everywhere, and test at boundaries specifically.
Open encounters. Encounter.period.end is absent for a stay in progress at export time. VISIT_OCCURRENCE.visit_end_date is required. Options are to use the export date, use the last associated event date, or exclude the visit. Each biases length-of-stay differently, and excluding drops currently-admitted people entirely, which biases toward completed and therefore shorter stays.
planned and cancelled encounters. Encounter.status includes values for interactions that did not happen. Loading them creates visits for care that was never delivered. Filter on status explicitly rather than accepting everything.
Encounters with no clinical content. Some sources emit administrative Encounters for scheduling, registration, or billing events. They inflate visit counts without representing care. Consider whether a visit with no associated clinical events should exist in your instance, and document the choice.
VISIT_DETAIL without VISIT_OCCURRENCE. The OMOP CDM requires every visit detail to reference a visit. An orphaned detail is a constraint violation that some loaders will accept silently.
Timezones. FHIR datetimes carry offsets. OMOP CDM columns typically do not. Converting everything to a single timezone is usually right, and inconsistent conversion produces events that appear to occur before the visit they belong to.
What to report¶
For any instance you build, these numbers should be available on request:
- Source Encounters in,
VISIT_OCCURRENCErows out, and the ratio VISIT_DETAILrows and the proportion of visits that have any- Visit concept distribution
- Count and handling of open encounters
- Count of clinical events attached by reference, by inference, and not at all
- Collapse parameters used, stated numerically