Skip to content

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 EMER encounter merge with an immediately following IMP encounter? 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_OCCURRENCE rows out, and the ratio
  • VISIT_DETAIL rows 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

Next