Skip to content

Medications to DRUG_EXPOSURE

Four FHIR resources, one OMOP table. The table is the easy part. The interesting part is that the four resources represent four different strengths of evidence, and if the transformation flattens that distinction, drug safety and utilization research loses most of what it needs.


The four resources

Resource What it asserts Evidence strength
MedicationRequest A prescriber ordered this Intent. The person may never have filled it
MedicationDispense A pharmacy dispensed this The medication left the pharmacy. The person may not have taken it
MedicationAdministration A dose was given Strongest. Typically inpatient, with a specific time and route
MedicationStatement Someone reports this is being taken Weakest and often uniquely valuable, because it captures over-the-counter and outside-system medications

All four become rows in DRUG_EXPOSURE. The distinction survives in drug_type_concept_id, and that column is the entire point of this chapter.


The type concept

drug_type_concept_id records provenance. Type concepts live in the Type Concept vocabulary, and the ones relevant here distinguish prescriptions written, dispensing records, medication administration records, and patient-reported medication.

A researcher studying adherence needs prescriptions and dispensings and needs to tell them apart, because the gap between them is the finding. A researcher studying inpatient dosing needs administrations specifically. A researcher studying over-the-counter aspirin exposure needs patient-reported statements, which are the only place that exposure appears at all.

A pipeline that assigns one type concept to everything has produced a DRUG_EXPOSURE table where none of those studies is possible. The rows are all there. The distinction is gone, and it cannot be recovered without going back to source.

The single most common defect in this table

Uniform type concepts. It passes every structural quality check, produces correct row counts, and silently removes the ability to answer most drug questions well.


The drug concept

OMOP standardizes drugs on RxNorm and RxNorm Extension. US Core binds medication codes to RxNorm, so the alignment is usually good, which is a genuine convenience.

The complication is granularity. RxNorm has multiple term types, and the level you map to determines what analyses are possible.

RxNorm level Example What it supports
Ingredient metformin Broad exposure questions
Clinical Drug Form metformin oral tablet Route-aware questions
Clinical Drug metformin hydrochloride 500 MG oral tablet Dose-aware questions
Branded Drug Glucophage 500 MG oral tablet Brand-specific questions

Map to the most specific level the source supports. Ancestry through CONCEPT_ANCESTOR means a specific concept is still found by an ingredient-level concept set, so specificity costs nothing and generality loses information permanently.

R. Alvarez's metformin arrives as RxNorm 860975, a Clinical Drug. Map it there, not up to the ingredient.

NDC codes are common in dispensing data and are non-standard in OMOP. They map to RxNorm through Maps to, and the mapping is imperfect because NDC is a packaging identifier rather than a clinical one. Preserve the NDC in drug_source_value and drug_source_concept_id.

Local formulary codes need Usagi work like any other local vocabulary.


Dates, and the end date problem

DRUG_EXPOSURE requires both drug_exposure_start_date and drug_exposure_end_date. FHIR frequently gives you only a start.

Resource Start End
MedicationRequest authoredOn Derive from dispenseRequest duration or dosage instruction
MedicationDispense whenHandedOver, else whenPrepared Start plus daysSupply
MedicationAdministration effectiveDateTime or effectivePeriod.start effectivePeriod.end, else same as start for a single dose
MedicationStatement effectivePeriod.start or effectiveDateTime effectivePeriod.end, often absent

The derivation rules, in order of preference:

  1. An explicit end in the source
  2. Start plus daysSupply, for dispensings
  3. Start plus expectedSupplyDuration, for requests
  4. Start plus a duration computed from quantity and dosage instruction
  5. Start plus a documented default
  6. Start equals end, for single administrations

Rules five and six deserve care. A default duration applied to a chronic medication produces exposure periods that are uniformly wrong, and every persistence, adherence, and time-at-risk analysis inherits the error. If you must use a default, use one that is defensible for the drug class rather than a single global number, and report how many rows received it.

R. Alvarez's aspirin MedicationStatement has effectivePeriod.start of "2019" and no end. Year-only start, open-ended. Populating start as 2019-01-01 and end as the export date asserts a continuous six-year exposure that the source did not claim. Populating end as equal to start asserts a single day. Both are inventions, and the honest handling is to pick one, document it, and flag these rows so an analyst can exclude them.


Quantity, dose, and route

OMOP CDM column FHIR source
quantity dispenseRequest.quantity, MedicationDispense.quantity
days_supply daysSupply, expectedSupplyDuration
refills dispenseRequest.numberOfRepeatsAllowed
sig dosageInstruction.text
route_concept_id dosage.route, dosageInstruction.route
lot_number Rarely present
dose_unit_source_value dosage.dose.unit
drug_source_value, drug_source_concept_id The original coding
stop_reason statusReason where meaningful

sig holds the dosing instruction as free text. It is unglamorous and it is frequently the only place the actual regimen survives, since structured dosage elements are inconsistently populated. Keep it.

route_concept_id carries more weight than it looks. The same drug given intravenously and orally are different exposures with different pharmacokinetics, and the route is often the difference between an inpatient and an outpatient exposure pattern.


The Medication resource

Medications can be coded inline in medicationCodeableConcept or referenced through medicationReference pointing at a Medication resource, which may be external or contained inside the parent.

A parser that handles only medicationCodeableConcept will silently produce rows with no drug concept for every referenced medication. A parser that handles external references but not contained resources will do the same for the contained ones.

Handle all three paths and count how many of each you encountered. If the counts surprise you, the parser is wrong.

Medication resources can also carry ingredient detail and form, which occasionally lets you resolve a more specific RxNorm concept than the top-level code supports.


Immunization

Immunization is a separate resource and its content belongs in DRUG_EXPOSURE, because vaccines are drug exposures in the OMOP model.

Vaccine codes commonly arrive as CVX, which is non-standard in OMOP and maps to RxNorm. Preserve the CVX in the source columns. Use a type concept that distinguishes immunization records, since vaccination data has different completeness characteristics from other medication data.


Where this quietly breaks

Uniform type concepts. Stated three times in this chapter, deliberately.

Loading requests as though they were exposures without distinguishing them. A prescription written is not a drug taken. In some populations the gap is very large.

Mapping to ingredient level unnecessarily. Loses dose and form permanently for no benefit.

Silent default durations. Produces uniformly wrong exposure windows.

Dropping contained Medication resources. Produces zero-concept rows for a subset of medications, often a systematic subset such as compounded or non-formulary drugs.

Ignoring status. MedicationRequest.status includes cancelled, draft, and entered-in-error. MedicationAdministration.status includes not-done, which means the dose was specifically not given. Loading a not-done administration as an exposure asserts the opposite of what the source said.

Deduplicating across resource types. A prescription, its dispensing, and its administration describe one clinical intention through three stages. They are three rows with three type concepts, not one row. DRUG_ERA does the collapsing afterward, and it does it correctly because it works from the loaded rows.


Practice

R. Alvarez's three medications:

DRUG_EXPOSURE  1
  drug_concept_id           = 1503327     metformin hydrochloride 500 MG oral tablet
  drug_exposure_start_date  = 2024-09-12
  drug_exposure_end_date    = 2024-10-12  start + expectedSupplyDuration 30 d
  drug_type_concept_id      = prescription written
  quantity                  = 60
  days_supply               = 30
  drug_source_value         = '860975'

DRUG_EXPOSURE  2
  drug_concept_id           = 1596977     insulin regular human 100 UNT/ML injectable
  drug_exposure_start_date  = 2024-09-10
  drug_exposure_end_date    = 2024-09-10  single administration
  drug_type_concept_id      = medication administered
  route_concept_id          = 4171047     intravenous
  visit_occurrence_id       = 1
  visit_detail_id           = 2           the ICU segment
  drug_source_value         = '311041'

DRUG_EXPOSURE  3
  drug_concept_id           = 19059056    aspirin 81 MG oral tablet
  drug_exposure_start_date  = 2019-01-01  year-only start, imputed, flagged
  drug_exposure_end_date    = 2019-01-01  no end in source, see policy
  drug_type_concept_id      = patient reported
  drug_source_value         = '243670'

Three type concepts, because three different things are being claimed. That is the chapter.


Next