← Methods repository
CONCEPTINTERMEDIATEPYTHON · R · SAS4 citations

OMOP CONDITION_OCCURRENCE and CONDITION_ERA

The two OMOP CDM tables that hold diagnoses (CONDITION_OCCURRENCE, one row per recorded condition event) and their derived persistence-window collapse (CONDITION_ERA), used to operationalize disease onset, recurrence, and chronic-disease history into cohort entry, comorbidity, and outcome variables.

Outcome Measureomop-cdmcondition-occurrencecondition-erapersistence-windowphenotypecomorbidity-ascertainmentdiagnosis-coding
On this page
Methods reference only. Use primary source citations and local policy before applying this in a study protocol, regulatory submission, payer dossier, or clinical decision.
In plain language

In a standardized health database built on the OMOP model, every time a patient receives a diagnosis — from a doctor visit, a hospital stay, or a problem list entry — that event lands as one row in a table called CONDITION_OCCURRENCE. Because the same chronic disease generates many separate visits over months or years, the database also builds a second table called CONDITION_ERA, which stitches those scattered diagnosis events into a single continuous time span whenever the gap between consecutive events is no wider than 30 days. Use CONDITION_OCCURRENCE when you want to find the first time a disease was recorded or count specific events; use CONDITION_ERA when you want to ask whether a patient had a disease present during a particular period.

When to use it
Chronic, well-coded comorbidities where any contact implies presence and non-differential misclassification is tolerable.
Descriptive baseline comorbidity covariates; never as a substitute for a validated outcome/eligibility definition.
Multi-database/network studies; audit the mapping when a single algorithm must exactly match a published ICD-based definition.
Watch out for
Era bakes in the persistence gap and discards diagnosis position/status, so position-aware or rule-out-aware algorithms cannot be expressed on the era.
Poor PPV for analytic outcomes; cannot encode confirmatory-code or position requirements that validated algorithms rely on.
Source-to-SNOMED mapping is lossy and ETL/vintage-dependent; counts can shift between CDM versions.

In the OMOP Common Data Model (CDM), almost every diagnosis-based variable in a real-world study is built from two tables. CONDITION_OCCURRENCE stores one row per recorded condition event: `person_id`, a standard `condition_concept_id` (SNOMED), `condition_start_date`/`condition_end_date`, the source code (`condition_source_value`), the `condition_type_concept_id` that records how the diagnosis was captured (inpatient primary, inpatient secondary, outpatient/professional claim, EHR problem list, EHR encounter diagnosis), and `condition_status_concept_id` (admitting, final, rule-out).

CONDITION_ERA

is a derived table: OHDSI's standard ETL collapses consecutive CONDITION_OCCURRENCE rows of the same condition into a single span (`condition_era_start_date`, `condition_era_end_date`, `condition_occurrence_count`) whenever the gap between events is no greater than a persistence window (OHDSI default 30 days). The occurrence table is the raw evidence; the era table is one opinionated summary of it.

Core conceptual distinction

. The two tables answer different questions and fail in different ways. CONDITION_OCCURRENCE is the unit for event questions — first-ever diagnosis (incident outcome), date of an acute event, count of encounters carrying a code — and it preserves the provenance you need to filter (an inpatient primary MI is not a rule-out MI coded on a chest-pain visit). CONDITION_ERA is the unit for state questions — "did this person have chronic condition X during the baseline window?" — because it has already stitched fragmented encounters into a presence/absence span.

The catch is that the era is only as good as the persistence window: with a 30-day gap, two heart-failure visits 45 days apart are two eras (looks intermittent), while a 180-day gap makes them one continuous era (looks chronic). The window is an analytic choice that silently rewrites prevalence and "chronic" status, so the estimand-relevant decision — what counts as having the condition, and over what calendar window — must be pre-specified and sensitivity-tested, not inherited from the ETL default.

Neither table validates a phenotype; both are the substrate on top of which a condition era / occurrence-based algorithm (e.g., 1 inpatient OR 2 outpatient codes ≥30 days apart) is layered.

Pros, cons, and trade-offs

. - CONDITION_ERA vs raw CONDITION_OCCURRENCE for chronic-disease ascertainment: the era table gives you a ready-made, de-duplicated presence span and avoids hand-rolling gap logic; it is the right primitive for comorbidity flags and "prevalent disease at index."

Cost: it bakes in the 30-day gap and discards `condition_type_concept_id`, so you can no longer require inpatient-primary position or exclude rule-out codes from inside the era. Prefer the era for chronic, well-coded conditions where any contact implies presence; prefer raw occurrence whenever code position, diagnosis status, or a custom 2-codes-in-N-days rule matters (most validated phenotypes).

  • vs a bespoke phenotype/cohort algorithm (ATLAS/Capr cohort, or the 1ip-2op rule): the eras/occurrences are inputs to those algorithms, not substitutes. A naive `≥1 condition_concept_id` flag over CONDITION_OCCURRENCE is fast but has poor PPV for most outcomes; a validated algorithm trades sensitivity for specificity. Prefer the validated algorithm for any analytic outcome or eligibility criterion; reserve the raw-flag-from-era shortcut for descriptive comorbidity adjustment where misclassification is non-differential and tolerable.
  • vs source-vocabulary (ICD-9/10) logic in native claims: working in standardized `condition_concept_id` makes code lists portable across data sources and networks (the OHDSI advantage).

Cost: mapping from source codes to SNOMED is lossy and ETL-dependent; a concept that is split or deprecated upstream can change cohort counts between CDM vintages. Prefer standardized concepts for multi-database/network studies; audit the source-to-standard mapping (and keep `condition_source_value`) when a single high-stakes algorithm must match a published ICD-based definition exactly.

When NOT to use — and when it is actively misleading or dangerous

.

  • Do not use CONDITION_ERA when code position or rule-out status is part of the definition. The era has already discarded `condition_type_concept_id`/`condition_status_concept_id`, so a "rule-out cancer" code on an imaging visit gets folded into a "cancer" era. For outcomes like cancer, MI, or stroke this inflates incidence and biases comparative estimates if coding intensity differs by arm.
  • Do not treat the 30-day default as physiology. Reporting "chronic disease" prevalence off the default gap without a sensitivity analysis (30/60/180 days) is indefensible — the number is an artifact of the ETL knob.
  • Do not read the first CONDITION_OCCURRENCE row as disease onset without an adequate clean/washout window inside an observed OBSERVATION_PERIOD. The first recorded date is when the patient entered the data with the code, not when disease began (left censoring / prevalent-as-incident misclassification).
  • Do not pool diagnosis events across data sources with different `condition_type_concept_id` mixes. A database that is mostly outpatient EHR problem-list entries and one that is mostly inpatient claims will produce non-comparable era structures even for the same persistence window.
  • Do not assume CONDITION_ERA exists or is current. It is a derived table; some CDM instances skip it, build it with a non-default gap, or fail to rebuild it after an incremental load. Verify the ETL spec and the era-build date before relying on it.

Data-source operational depth

. - Administrative claims: `condition_type_concept_id` distinguishes inpatient principal/secondary from professional/ outpatient claims — this is the backbone of validated 1-inpatient-OR-2-outpatient rules.

Failure modes: rule-out coding (a diagnosis placed on a lab/imaging encounter to justify the test, not to assert disease) inflates occurrence counts and, once collapsed, contaminates eras; require diagnosis position and/or a second confirmatory code. Claims adjudication lag means recent person-time looks disease-free; freeze data with a run-out buffer.

Medicare Advantage-only person-time lacks fee-for-service claims, so absence of a CONDITION_OCCURRENCE row is missingness, not true absence — restrict to A/B/D FFS person-time when ascertaining baseline conditions or incident outcomes. - EHR: the same condition can appear as a problem-list entry (carried forward indefinitely, no natural end date), an encounter diagnosis, or a resolved condition; `condition_status_concept_id` that would disambiguate them is frequently null.

Problem-list carry-forward makes CONDITION_ERA end dates unreliable and can make a resolved condition look chronic. Visit-driven capture means a patient who gets care outside the system has phantom "gaps" that fragment eras; reconcile with the observed OBSERVATION_PERIOD before interpreting era discontinuity.

  • Registry: strongest for adjudicated, well-defined conditions (e.g., cancer with stage) but typically narrow in scope and weak for incidental comorbidities; map registry diagnoses into CONDITION_OCCURRENCE carefully and do not expect a complete comorbidity profile from registry-derived eras alone.
  • Linked claims–EHR(–registry): the richest substrate, but the same condition is now captured by multiple feeds with different dates and positions; de-duplicate before the era ETL or eras double-count, and reconcile differential capture so that exposure arms are not compared on systematically different coding density.

Worked claims/CDM example — chronic HF comorbidity + incident HF outcome

Question: among new initiators of drug A vs drug B (index date = first qualifying fill), (i) flag baseline chronic heart failure as a confounder and (ii) ascertain incident HF as the outcome, in an OMOP-mapped commercial + Medicare FFS claims database. Define the HF concept set as a standard `condition_concept_id` descendant list.

Baseline comorbidity (state question → CONDITION_ERA)

a person has baseline HF if they have ≥1 HF CONDITION_ERA overlapping the 365-day pre-index window. Because eras drop diagnosis position, pre-specify and report the sensitivity to the persistence gap (rebuild eras or apply a custom gap of 30/60/180 days) and cross-check against the position-aware rule below.

Incident outcome (event question → CONDITION_OCCURRENCE with a validated algorithm)

a qualifying HF event = either one HF code with `condition_type_concept_id` = inpatient principal, OR two outpatient/professional HF codes ≥30 days apart; the outcome date is the first such qualifying date after index. Apply a clean window — no HF CONDITION_OCCURRENCE row in the 365 days before index — so first-recorded is plausibly incident, and require the entire baseline and at-risk period to fall inside a continuous OBSERVATION_PERIOD on FFS (A/B/D) coverage, excluding MA-only person-time where claims are absent.

Censor at observation-period end, death, or end of data. Sensitivity analyses: era persistence gap (30/60/180), the 1-inpatient-vs-2-outpatient rule, and a negative-control condition to detect differential coding intensity between arms.

Interpreting the output

. Patient 2001 has three heart failure CONDITION_OCCURRENCE rows: February 1, February 29 (gap = 28 days from the February 1 discharge), and April 14 (gap = 45 days from the February 29 discharge). With a 30-day persistence window, the February 1 and February 29 occurrences are within the window and collapse into a single CONDITION_ERA spanning February 1 through February 29; the April 14 occurrence exceeds the 30-day gap and opens a second, distinct CONDITION_ERA on April 14.

Formal interpretation: the same three diagnosis codes yield two CONDITION_ERAs under a 30-day persistence window but would yield one era under a 45-day window and three separate eras under a 7-day window. The era count is a construction artifact, not a clinical truth — the persistence window encodes an assumption about what gap between coded encounters represents a new disease episode versus continued care for the same episode.

This construction choice propagates to any analysis that uses CONDITION_ERA to define prevalent disease exposure, disease duration, or episode count. Analysts who use the OMOP default 30-day window without checking whether it is appropriate for the target condition and coding environment are importing an assumption without examination.

Practical interpretation: always report the persistence gap used and run a sensitivity analysis at alternative gaps (30, 60, 180 days) for chronic conditions where the clinically meaningful episode boundary is ambiguous. For acute conditions — where any recurrence after full recovery is a distinct event — prefer the CONDITION_OCCURRENCE-based incident-event algorithm over condition era, and document that choice in the SAP.

Decision diagram

flowchart TD
  Src[Source diagnosis codes<br/>ICD-9/10, EHR problem list] --> CO[CONDITION_OCCURRENCE<br/>1 row per event<br/>concept_id + start_date + type_concept_id]
  CO -->|"ETL collapse, gap <= persistence window (default 30d)"| CE[CONDITION_ERA<br/>presence span + occurrence_count]
  CE --> State["STATE question:<br/>chronic / prevalent comorbidity<br/>era overlaps baseline window"]
  CO --> Event["EVENT question:<br/>incident outcome<br/>validated rule + clean window"]
  State --> Cohort[Baseline covariate block]
  Event --> Cohort
  Cohort --> Sens["Sensitivity:<br/>era gap 30/60/180d,<br/>1-inpatient vs 2-outpatient rule"]
How the two tables relate. CONDITION_OCCURRENCE is the raw event substrate; CONDITION_ERA is a persistence-window collapse of it. State questions (chronic comorbidity) use the era; event questions (incident outcome) use the occurrence table with a validated, position-aware rule.
gantt
  title Persistence window rewrites "chronic" status (same three HF codes)
  dateFormat YYYY-MM-DD
  axisFormat %b
  section Raw occurrences
  Code 1 :milestone, c1, 2024-01-10, 0d
  Code 2 (45d later) :milestone, c2, 2024-02-24, 0d
  Code 3 (40d later) :milestone, c3, 2024-04-04, 0d
  section Era with 30-day gap
  Era 1 (single code) :a1, 2024-01-10, 1d
  Era 2 (single code) :a2, 2024-02-24, 1d
  Era 3 (single code) :a3, 2024-04-04, 1d
  section Era with 180-day gap
  One continuous chronic era :crit, b1, 2024-01-10, 85d
The same three diagnosis events yield three fragmented eras under the OHDSI default 30-day gap (looks intermittent) but one continuous chronic era under a 180-day gap. The persistence window is an analytic choice that must be pre-specified and sensitivity-tested, not inherited from the ETL default.

Worked example

Scenario

Patient 2001 is enrolled in a commercial claims database with continuous coverage from 2024-01-01 through 2024-06-30. She has three outpatient encounters where a heart-failure diagnosis code is recorded: February 1, February 29, and April 14. We want to show exactly how the OMOP ETL collapses these three CONDITION_OCCURRENCE rows into CONDITION_ERA records using the default 30-day persistence window.

Dataset

Raw CONDITION_OCCURRENCE rows for patient 2001 (heart failure, condition_concept_id 316139)

person_idcondition_concept_idcondition_start_datecondition_type_concept_id
20013161392024-02-01outpatient claim
20013161392024-02-29outpatient claim
20013161392024-04-14outpatient claim
FIG. 1 — DESIGN TIMELINE
Timeline for the worked example of omop-condition-occurrence-condition-era-rwe.
Data-grounded worked-example timeline (beginner layer), drawn to scale from worked_example.timeline_spec so the picture matches the numbers.

Steps

1Sort the three occurrences by date: Feb 01, Feb 29, Apr 14.
2Check the gap between occurrence A (Feb 01) and occurrence B (Feb 29): 28 days. Because 28 is less than or equal to the 30-day persistence window, A and B are bridged into the same era.
3The era built from A and B spans Feb 01 through Feb 29 and has an occurrence count of 2.
4Check the gap between occurrence B (Feb 29) and occurrence C (Apr 14): 45 days. Because 45 exceeds the 30-day persistence window, C starts a new era.
5Era 2 spans Apr 14 through Apr 14 and has an occurrence count of 1.
6Final result: 3 CONDITION_OCCURRENCE rows produce 2 CONDITION_ERA rows.

Result

3 CONDITION_OCCURRENCE events collapse into 2 CONDITION_ERAs: Era 1 covers 2024-02-01 to 2024-02-29 (28-day span, 2 occurrences); Era 2 covers 2024-04-14 to 2024-04-14 (1-day span, 1 occurrence). The 45-day gap between Feb 29 and Apr 14 exceeds the 30-day window and breaks continuity.

Caption

Three outpatient heart-failure claims for patient 2001. The 28-day gap between Feb 01 and Feb 29 falls within the 30-day persistence window, so those two occurrences merge into Era 1. The 45-day gap between Feb 29 and Apr 14 exceeds the window, so Apr 14 becomes a separate Era 2.

Alt Text

Timeline from January through June 2024 showing three diagnosis-event markers on Feb 01, Feb 29, and Apr 14, with a green span labeled Era 1 bridging Feb 01 to Feb 29, a gray gap band from Mar 01 to Apr 13 labeled 45-day gap, and a second green span labeled Era 2 on Apr 14.

Trade-offs

vs. Raw CONDITION_OCCURRENCE concept flag (>=1 code)
Pros of this
CONDITION_ERA gives a de-duplicated presence span and ready-made chronic-disease unit without hand-rolled gap logic.
vs. Validated phenotype/cohort algorithm (e.g., 1ip 2op rule, ATLAS cohort)
Pros of this
Eras/occurrences are the inputs; the raw era flag is faster and simpler for descriptive comorbidity adjustment.
vs. Source vocabulary (ICD 9/10) logic in native claims
Pros of this
Standardized condition_concept_id code lists are portable across data sources and OHDSI network studies.

Runnable example

Baseline era-presence flag + occurrence-based incident outcome from OMOP CDM tables (pandas). Required inputs (already loaded, OMOP-shaped, one schema per database): condition_occurrence : person_id, condition_concept_id, condition_start_date (datetime), condition_type_concept_id (provenance: inpatient principal /...

requires: pandas
import pandas as pd

HF_CONCEPTS = set(hf_concept_descendants)          # standard condition_concept_id list for heart failure
INPATIENT_PRINCIPAL = {38000183, 38000199}         # condition_type_concept_id values for inpatient primary dx
BASELINE_DAYS = 365
OUTPATIENT_GAP_DAYS = 30                            # 2-outpatient-codes-apart requirement

def add_baseline_hf(cohort: pd.DataFrame, condition_era: pd.DataFrame) -> pd.DataFrame:
    # STATE question -> CONDITION_ERA: any HF era overlapping [index-365, index].
    era = condition_era[condition_era["condition_concept_id"].isin(HF_CONCEPTS)]
    m = era.merge(cohort[["person_id", "index_date"]], on="person_id")
    win_start = m["index_date"] - pd.Timedelta(days=BASELINE_DAYS)
    overlap = (m["condition_era_start_date"] <= m["index_date"]) & (m["condition_era_end_date"] >= win_start)
    flagged = m.loc[overlap, "person_id"].unique()
    out = cohort.copy()
    out["baseline_hf"] = out["person_id"].isin(flagged).astype(int)
    return out

def add_incident_hf(cohort: pd.DataFrame, condition_occurrence: pd.DataFrame) -> pd.DataFrame:
    # EVENT question -> CONDITION_OCCURRENCE with a validated rule, first qualifying date AFTER index.
    co = condition_occurrence[condition_occurrence["condition_concept_id"].isin(HF_CONCEPTS)]
    co = co.merge(cohort[["person_id", "index_date"]], on="person_id")

    # Clean window: incident only if no HF occurrence in the 365 days before index.
    pre = co[(co["condition_start_date"] < co["index_date"]) &
             (co["condition_start_date"] >= co["index_date"] - pd.Timedelta(days=BASELINE_DAYS))]
    prevalent = set(pre["person_id"])
    post = co[(co["condition_start_date"] > co["index_date"]) & (~co["person_id"].isin(prevalent))].copy()

    # Rule A: one inpatient-principal code.
    ip = post[post["condition_type_concept_id"].isin(INPATIENT_PRINCIPAL)]
    ip_date = ip.groupby("person_id")["condition_start_date"].min()

    # Rule B: two outpatient codes >= OUTPATIENT_GAP_DAYS apart; qualifying date = the second code.
    op = post[~post["condition_type_concept_id"].isin(INPATIENT_PRINCIPAL)].sort_values(
        ["person_id", "condition_start_date"])
    op["prev_dt"] = op.groupby("person_id")["condition_start_date"].shift(1)
    op_qual = op[(op["condition_start_date"] - op["prev_dt"]).dt.days >= OUTPATIENT_GAP_DAYS]
    op_date = op_qual.groupby("person_id")["condition_start_date"].min()

    first = pd.concat([ip_date, op_date], axis=1).min(axis=1).rename("incident_hf_date")
    return cohort.merge(first, on="person_id", how="left")

Citations

FOUNDATIONAL / METHODS
  1. [1]Hripcsak G, Duke JD, Shah NH, et al. Observational Health Data Sciences and Informatics (OHDSI): opportunities for observational researchers. Studies in Health Technology and Informatics. 2015;216:574-578.
  2. [2]Reps JM, Schuemie MJ, Suchard MA, Ryan PB, Rijnbeek PR. Design and implementation of a standardized framework to generate and evaluate patient-level prediction models using observational healthcare data. Journal of the American Medical Informatics Association. 2018;25(8):969-975.
APPLIED EXAMPLES
  1. [3]Swerdel JN, Hripcsak G, Ryan PB. PheValuator: development and evaluation of a phenotype algorithm evaluator. Journal of Biomedical Informatics. 2019;97:103258.
REPORTING & GUIDANCE
  1. [4]Benchimol EI, Smeeth L, Guttmann A, et al. The REporting of studies Conducted using observational Routinely-collected health Data (RECORD) statement. PLoS Medicine. 2015;12(10):e1001885.