Synthmas — clinical events¶
visit_occurrence, visit_detail, condition_occurrence, and
procedure_occurrence all live in the OMOP CDM as records of
something that happened to a patient at a point in time. They share a
small handful of patterns:
- A fresh integer primary key from
AutoGenId. - A
*_type_concept_idset withSetValue. - Foreign keys (
person_id,visit_occurrence_id,visit_detail_id,provider_id) resolved withGetCDMTableId. - Vocabulary-coded concepts mapped via
VocabMap+VocabSourceId.
Plus one pattern that's specific to visit_occurrence:
Selector used as a
type-based dispatcher.
The universal *_type_concept_id¶
Every clinical-event table has a _type_concept_id field that
records how the row got there (EHR, claim, registry, derived…).
For Synthmas, every event came from EHR-equivalent data, so they all
get the same hard-coded value:
logic: SetValue(32810) # "EHR" — visit, visit_detail, procedure, observation, …
logic: SetValue(32867) # "EHR Chief complaint" — condition_occurrence
logic: SetValue(32817) # "EHR prescription" — drug_exposure
The exact value comes from Athena — pick the concept id that best describes the provenance of your source data.
Cross-table foreign keys¶
Every clinical-event table has a person_id pointing back to
person. The pattern is always the same — look up the source UUID in
the already-loaded person table and demand a hit:
GetCDMTableId(@person) defaults to looking up
person_source_value and returning person_id. The
AssertNot(NULL) makes
the missing-person case a logged failure rather than a silent NULL
foreign key.
visit_occurrence_id and visit_detail_id follow the same pattern,
but with a NULL default rather than a hard fail (a procedure may
be unattached to a visit):
For provider_id on a child table, an explicit key/value form points
the lookup at the right column:
visit_occurrence — type-based dispatch with Selector¶
Synthmas's encounters.csv carries an encounterclass column that
is sometimes an integer concept id and sometimes a class string
(ambulatory, emergency, …). Two CDM fields draw from it
differently:
visit_concept_id:
logic: Selector(:encounterclass, "=", 1, :code, :encounterclass)
visit_source_value:
logic: Selector(:encounterclass, "=", 0, :code, :encounterclass)
logic: AsString()
Read the Selector calls as: "if :encounterclass equals 1, return
:code; otherwise return :encounterclass". So the engine picks
between the source code and encounterclass columns depending on
which side of the source data this row came from. The
AsString() after the second Selector flattens the chosen value
to a string for the source-value field.
Vocabulary-coded concepts (SNOMED, RxNorm, LOINC)¶
condition_occurrence, procedure_occurrence, and the other
event-y tables all carry SNOMED-coded code columns in the source.
The CDM wants both a standard concept id and a source concept id —
the engine has rules for each:
condition_concept_id:
logic: AsString()
logic: VocabMap("SNOMED")
condition_source_concept_id:
logic: AsString()
logic: VocabSourceId("SNOMED")
VocabMap returns the standard concept id mapped from a
vocabulary code (cross-walked through OMOP's mapping tables).
VocabSourceId returns the OMOP concept id of the source code
itself, without crosswalk.
For codes that aren't in the standard vocabulary — Synthea
occasionally emits SNOMED codes that have been retired or replaced —
a small inline Map ahead of VocabMap re-routes them:
logic: AsString()
logic: Map([
logic: ["425525006", "782555009"],
logic: ["300916003", "419412007"]
logic: ])
logic: VocabMap("SNOMED")
Map only fires for keys it recognises; everything else passes
through unchanged to VocabMap. See
Useful patterns → pre-map then vocabulary
for variations on this theme.
Recap¶
The four event tables share:
AutoGenIdfor the primary key.SetValue(<concept_id>)for*_type_concept_id.GetCDMTableId(@person) + AssertNot(NULL)for the mandatory FK.GetCDMTableId(@visit_occurrence, NULL)for optional FKs.AsString() + VocabMap("SNOMED") + VocabSourceId("SNOMED")for vocabulary lookups, with an optional pre-Mapfor out-of-vocabulary codes.
visit_occurrence adds the Selector-as-dispatcher pattern, which
becomes a recurring trick once you start handling source data with
multi-format columns.
Next: Values and dates — observations, measurements, and the date-arithmetic patterns for drug and device exposures.