Skip to content

Patient to PERSON

The easiest table and the one with the most quietly consequential decisions. PERSON is small, so the mistakes are subtle and they propagate into every study that stratifies by age, sex, race, or ethnicity.


The target

PERSON in OMOP CDM v5.4:

Column Required Source in FHIR
person_id yes Assigned, not taken from source
gender_concept_id yes Patient.gender
year_of_birth yes Patient.birthDate
month_of_birth no Patient.birthDate
day_of_birth no Patient.birthDate
birth_datetime no Patient.birthDate
race_concept_id yes US Core race extension
ethnicity_concept_id yes US Core ethnicity extension
location_id no Patient.address
provider_id no Patient.generalPractitioner
care_site_id no Organization context
person_source_value no Patient.identifier
gender_source_value, gender_source_concept_id no Patient.gender
race_source_value, race_source_concept_id no Race extension
ethnicity_source_value, ethnicity_source_concept_id no Ethnicity extension

One row per person. PERSON holds only attributes that are stable enough to treat as fixed. Anything that changes over time belongs in OBSERVATION as a dated fact.


person_id

person_id is an integer you assign. It is not the MRN, not the FHIR resource id, and not any identifier that appears in the source.

Two reasons. First, the OMOP CDM uses integer keys throughout for join performance at scale. Second, and more importantly, a surrogate key is a de-identification boundary. person_source_value holds the link back to source identity, and that column can be omitted, hashed, or held in a restricted mapping table depending on your governance posture.

Keep the mapping from person_source_value to person_id somewhere durable and access-controlled. Incremental loads need it, and losing it means you cannot append new data to existing people.

Identity resolution happens before this step

If your FHIR source draws from multiple systems, one human being may appear as several Patient resources with different ids. Deciding they are the same person is a distinct problem from the OMOP CDM transformation, and it needs its own design, its own review, and its own error rate estimate. Do not let it hide inside the ETL.


Birth date and the precision problem

Patient.birthDate is a FHIR date, which permits YYYY, YYYY-MM, or YYYY-MM-DD. R. Alvarez has "1962".

The OMOP CDM wants year_of_birth required, month_of_birth and day_of_birth optional, and birth_datetime optional.

The correct handling of "1962":

year_of_birth  = 1962
month_of_birth = NULL
day_of_birth   = NULL
birth_datetime = NULL

That is honest. You know the year and nothing more, and the optional columns exist precisely so you can say so.

The common wrong handling is to fill in January first, producing 1962-01-01. This is attractive because it makes birth_datetime non-null and simplifies downstream age arithmetic. It is also fabrication, and it has two specific consequences.

First, age at any event is biased. For a population with year-only birth dates, defaulting to January first makes everyone look on average six months older than they are. In adult chronic disease research the effect is small. In pediatric research, in neonatal work, and in any analysis with narrow age bands it is not.

Second, the fabrication is undetectable downstream. A January first birth date looks exactly like a real January first birth date. An analyst cannot filter it out, cannot estimate its prevalence, and has no signal that it happened. Nulls are visible. Fabricated values are not.

If a downstream requirement genuinely forces a complete date, populate birth_datetime with the imputed value and leave month_of_birth and day_of_birth null, so the imputation is at least detectable by comparing the columns. Then document it in CDM_SOURCE and in your ETL specification.


Gender

Patient.gender uses the administrative-gender value set: male, female, other, unknown.

PERSON.gender_concept_id expects a concept from the OMOP Gender domain. The two common standard concepts are 8507 for male and 8532 for female. Values that do not map to those go to 0, with the original preserved in gender_source_value.

The mapping is mechanically trivial and conceptually deserves a pause. The FHIR specification is explicit that administrative-gender is an administrative property, not a statement about clinical sex, biological sex, or gender identity. OMOP's Gender domain is used in practice as a proxy for sex in most analyses. These are not the same concept, and the transformation quietly equates them.

For most population-scale research this is an accepted approximation and everyone in the field knows it. Where it stops being acceptable is in research where sex assigned at birth, current gender identity, or organ inventory carry clinical meaning. If your source has richer information, US Core and related guides define elements for gender identity and sex parameters that can go into OBSERVATION as dated facts, and that is where the nuance survives. Note in your ETL documentation what gender_concept_id in your instance actually represents.


Race and ethnicity

This is the field pair most likely to be handled badly, and the reason is structural rather than careless.

OMOP separates race and ethnicity into two columns, following the OMB categories where ethnicity has essentially two values: Hispanic or Latino, and Not Hispanic or Latino. US Core does the same, with separate us-core-race and us-core-ethnicity extensions.

When the source populates both extensions properly, as R. Alvarez's does, the mapping is direct:

race extension  ombCategory 2106-3 White
  → race_concept_id = 8527 (White)
  → race_source_value = '2106-3'

ethnicity extension ombCategory 2135-2 Hispanic or Latino
  → ethnicity_concept_id = 38003563 (Hispanic or Latino)
  → ethnicity_source_value = '2135-2'

The problem is that many source systems do not maintain the separation. A single field containing values like "Hispanic", "White", "Black", "Asian", "Other" collapses two axes into one. When that field says "Hispanic", it usually tells you the ethnicity and tells you nothing about race, even though race is a required column.

Options when the source is collapsed:

Populate ethnicity, set race to 0. Honest. race_concept_id = 0 means "not determined from source", which is true. Preserve the original in race_source_value.

Populate both from the single field where the value is unambiguously racial. "White" with no ethnicity information gives you race and leaves ethnicity at 0.

Infer. Do not. Inferring ethnicity from race or race from ethnicity produces data that looks complete and is invented, on a variable that carries real weight in health equity research.

The US Core extensions also support detailed codings alongside ombCategory, and a text element. The detailed codings can carry granularity the OMB categories lose. If your source populates them, consider retaining that granularity as an OBSERVATION row even though PERSON cannot hold it.

Why this deserves the extra care

Race and ethnicity in health data are already recognized as inconsistently collected and frequently missing. A transformation that silently improves apparent completeness by imputation makes a known data limitation invisible, which is the opposite of what a research data set should do. Preserving the gap honestly is the more useful outcome.


Location

Patient.address populates a LOCATION row, and PERSON.location_id points at it.

Two considerations.

PERSON.location_id holds a single location. A person who moved has more addresses than the column can hold. The usual convention is the most recent known address, with the earlier ones either dropped or retained as OBSERVATION rows if residential history is relevant to your research.

Geographic identifiers carry re-identification risk. A five-digit postal code plus a date of birth plus a sex is a well-known re-identification vector. Whether you populate full postal codes, truncate to three digits, or omit geography is a governance decision, and it belongs in your data use agreement rather than in the transformation code.

OMOP CDM v5.4's LOCATION table includes latitude and longitude columns. Populating them from a source address makes a person's residence precisely locatable, and that should be a deliberate decision with a documented justification.


Where this quietly breaks

Silent January first imputation. Covered above, and the single most common issue in this table.

Reusing FHIR resource ids as person_id. They are strings, they are not stable across systems, and they leak source structure into the analytical layer.

Loading each Patient resource as a person without identity resolution. Produces duplicate people, splits their event histories, and deflates every per-person count in ways that look like a data availability problem rather than a modeling error.

Filling race_concept_id with a general concept to avoid zeros. A zero is informative. A wrong value is not.

Dropping person_source_value for de-identification. Understandable, but it makes incremental loading impossible. Better to keep the mapping in a restricted table with its own access controls than to destroy it.

Forgetting that PERSON has no time dimension. Anything that changes belongs elsewhere. If you find yourself wanting a "current" value in PERSON, that is a signal the fact belongs in OBSERVATION.


Practice

R. Alvarez's PERSON row, filled in:

person_id               = 1                      (assigned)
gender_concept_id       = 8532                   (Female)
year_of_birth           = 1962
month_of_birth          = NULL
day_of_birth            = NULL
birth_datetime          = NULL
race_concept_id         = 8527                   (White)
ethnicity_concept_id    = 38003563               (Hispanic or Latino)
location_id             = 1                      (19002, PA, US)
provider_id             = NULL
care_site_id            = NULL
person_source_value     = 'MRN-4471902'
gender_source_value     = 'female'
race_source_value       = '2106-3'
ethnicity_source_value  = '2135-2'

Three nulls where a careless pipeline would have written January first, 1962. That is the chapter.


Next