← Methods repository
CONCEPTINTERMEDIATEPYTHON · R · SAS4 citations

OMOP Concept Set Development

The process of building, expanding, excluding, version-freezing, and validating standardized vocabulary concept sets (code lists) in the OMOP CDM so that exposure, outcome, and cohort-qualifying phenotypes are reproducible, network-portable, and resolvable against source codes.

Exposure Definitionomop-cdmconcept-setcode-listphenotypingstandardized-vocabulariesohdsirxnorm-snomedsource-to-standard-mapping
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

An OMOP concept set is a reusable, shareable code list that translates a clinical idea — like 'type 2 diabetes' or 'ACE inhibitor use' — into a precise list of standardized numeric identifiers that any OMOP database can look up. You pick one or more anchor concepts, expand them to include all child codes in the vocabulary hierarchy, and carve out any children you do not want. Because the list is built from standard vocabulary codes rather than database-specific billing strings, the same concept set can be applied across hospitals, insurers, or countries without rewriting it for each. One honest caveat: if the ETL that converted raw data to OMOP failed to map a source code, that person's record will be invisible to your concept set no matter how carefully it is built.

When to use it
Network or multi-database OMOP studies; keep a source-code fallback for unmapped codes.
Use descendant expansion with explicit exclusions and a manual review of the resolved set.
Start from a published set, then localize and re-estimate operating characteristics.
Watch out for
Depends on ETL mapping quality — codes mapped to concept_id = 0 are invisible to a standard set, whereas a raw source list would have caught them.
Pulls in unintended children (combination products, wrong forms) unless explicit exclusions and a resolved-list audit are added.
May not match the local indication, era, or data partner coding habits and may need re-validation.

An OMOP concept set is the machine-readable code list that operationalizes a clinical idea ("statin exposure," "type 2 diabetes," "acute myocardial infarction") against the OMOP Common Data Model. It is not a flat list of ICD/NDC strings: it is a concept-set expression — one or more standard `concept_id`s, each optionally flagged to include descendants (pull every child via `CONCEPT_ANCESTOR`), include mapped source codes, or be an exclusion — that resolves to a final set of standard concept_ids at a fixed vocabulary version. Concept sets are the atomic building block of every phenotype: exposures (drug eras), outcomes (condition/procedure occurrences), and cohort inclusion/exclusion criteria are all assembled from them, so a single mis-specified concept set silently propagates into the numerator, denominator, and follow-up of the entire study.

Core conceptual distinction

Three things are conflated by novices and must be kept separate.

  1. Source code vs standard concept: raw data carry source codes (ICD-9/10-CM, NDC, CPT, local lab codes) in `*_source_value`/`*_source_concept_id`; OMOP's ETL maps these to standard concepts (SNOMED for conditions, RxNorm for drugs, LOINC for measurements) via `CONCEPT_RELATIONSHIP` ("Maps to"). A concept set is normally defined on standard concepts (`standard_concept = 'S'`) so it is vocabulary- and source-agnostic, but it only captures person-time whose source codes actually mapped.
  2. Concept set vs phenotype: the concept set is the code list; the phenotype/cohort definition adds the entry event logic, occurrence counts, care-setting, and time windows (e.g., the 1-inpatient-or-2-outpatient rule) on top of it.
  3. Expression vs resolved set: the saved expression (include-descendants flags + exclusions) is stable, but the resolved list of concept_ids changes whenever the vocabulary version changes — so the version-frozen resolved set, not just the expression, is the reproducible artifact. The deliverable is a versioned, human-reviewed, machine-resolvable set plus its measured operating characteristics (PPV/sensitivity from chart review or PheValuator).

Pros, cons, and trade-offs

.

  • vs hand-curated source code lists (ICD/NDC strings in a SAS macro): A standard-concept set is portable across data partners and vocabulary updates, auto-expands to descendants so new child codes are captured without re-editing, and is reviewable in tooling (ATLAS). Cost: it depends on ETL mapping quality — anything mapped to `concept_id = 0` is invisible to a standard-concept set, whereas a raw source-code list would have caught it. Prefer the standard-concept set for network/multi-database studies; keep a source-code fallback for unmapped codes.
  • vs include-descendants "grab everything": Descendant expansion is the strength of OMOP (define "ACE inhibitors" at the RxNorm ingredient/ATC level and inherit every product), but blind expansion pulls in unintended children (combination products, veterinary forms, the wrong laterality). Cost of not expanding: you miss legitimate children and undercount. Prefer descendant expansion with explicit exclusions, then audit the resolved list.
  • vs a single shared "off-the-shelf" PheKB/OHDSI library set: Reusing a validated, published concept set buys validity evidence and comparability. Cost: it may not match your indication, era, or data partner's coding habits. Prefer a published set as the starting point, then localize and re-validate.

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

.

  • Source data are not in OMOP-CDM. On raw claims/EHR, build and validate source-code lists directly; a half-mapped local CDM gives you the illusion of standardization with hidden `concept_id = 0` loss.
  • You treat the unversioned expression as the reproducible object. Re-resolving the same expression against a newer vocabulary silently changes the captured concepts (codes get re-mapped, deprecated, or re-parented); a result "reproduced" on a different vocabulary version is not the same study. Freeze and report the vocabulary version and the resolved concept_id list.
  • Descendant expansion across a known structural break. Defining a condition set only on ICD-10-mapped SNOMED descendants will miss all pre-October-2015 ICD-9 person-time unless the source-to-standard map bridges both — producing a spurious incidence jump at the coding transition that looks like a real trend.
  • You skip measuring operating characteristics. A concept set with no PPV/sensitivity estimate, applied to a rare outcome, can be dominated by false positives; reporting it as a validated phenotype is misleading.
  • MA-only or capitated person-time. If a patient's care is paid under arrangements that do not generate adjudicated diagnosis/drug claims, there are no source codes to map at all — the concept set captures nothing, and "no event" is missingness, not absence.

Data-source operational depth

.

  • Claims (FFS vs MA, commercial): Exposures map NDC → RxNorm; conditions map ICD-9/10-CM → SNOMED; procedures map CPT/HCPCS → standard. Failure modes: (a) `concept_id = 0` — source codes the ETL could not map (retired NDCs, local/miscellaneous codes, repackaged NDCs) drop out of any standard-concept set; quantify `% of source rows with target_concept_id = 0` by data partner and decade and bridge with a source- concept fallback. (b) ICD-9→ICD-10 break (Oct 2015) changes the descendant tree and code granularity — validate capture on both sides of the break. (c) NDC repackaging churn means branded-NDC lists rot; define drug sets at the RxNorm ingredient level and let descendants resolve products. (d) Medicare Advantage / capitated person-time lacks FFS-adjudicated claims, so the set captures no codes — restrict to enrollees with the relevant benefit (A/B/D or commercial medical+pharmacy) and exclude unobservable spans.
  • EHR: Conditions arrive from problem lists, encounter diagnoses, and notes; structured capture is visit-driven and the same concept may be entered as a problem, a billing diagnosis, or only free text. Concept sets over structured fields miss note-only mentions; NLP-derived concepts and external-care leakage (care delivered outside the system) bias capture. Lab-based criteria need LOINC harmonization and unit reconciliation before a measurement concept set is trustworthy.
  • Registry: Disease and severity are often coded in registry-specific schemes that may not map cleanly to standard vocabularies; adjudicated outcomes are high-validity but require a crosswalk to OMOP concepts and explicit handling of registry completeness.
  • Linked claims–EHR–registry: The richest substrate, but each source maps to standard concepts through a different ETL; the same clinical event can appear as distinct concept_ids across sources. Reconcile by de-duplicating at the standard-concept level and documenting which source contributed each capture, and watch for date discrepancies (claim service date vs EHR encounter date) before assigning the phenotype index date.

Worked example (claims, OMOP-CDM)

Build a second-generation ACE-inhibitor exposure concept set and count incident users.

  1. Pick the standard anchor: in `CONCEPT`, select the RxNorm ingredient class for ACE inhibitors (`vocabulary_id = 'RxNorm'`, `concept_class_id = 'Ingredient'`, `standard_concept = 'S'`) — say the ingredient `concept_id`s for lisinopril, ramipril, enalapril, etc.
  2. Expand descendants: join to `CONCEPT_ANCESTOR` to pull every `descendant_concept_id` (clinical drugs, branded products, strengths) under those ingredients.
  3. Exclude: drop combination products you do not want (e.g., ACE-inhibitor + thiazide fixed-dose combos) by listing their concept_ids as an exclusion branch.
  4. Freeze: snapshot the resolved `concept_id` list together with the vocabulary version to a versioned JSON expression checked into the study repo.
  5. Apply to the CDM: select `DRUG_EXPOSURE` rows where `drug_concept_id IN (resolved set)`; for rows with `drug_concept_id = 0`, reconcile via `drug_source_concept_id`/`drug_source_value` against a source-code fallback and report the unmapped fraction.
  6. Define incident use: require continuous observation (`OBSERVATION_PERIOD`) covering a 365-day washout with no prior in-set fill, set index = `drug_exposure_start_date` of the first in-set fill, and confirm indication with a condition concept set in the baseline window.
  7. Numerator/denominator check: report distinct `person_id` count, fills per person, the `% concept_id = 0` reconciled, and capture on both sides of the ICD-9→ICD-10 break for any condition co-criteria.
  8. Validate: estimate PPV against chart review or run PheValuator, and report sensitivity to the include-descendants and exclusion choices.

Decision diagram

flowchart TD
  Src[Source codes in raw data<br/>ICD-9/10-CM, NDC, CPT, local lab] --> Map[Source-to-Standard map<br/>CONCEPT_RELATIONSHIP 'Maps to']
  Map -->|mapped| Std[Standard concepts<br/>SNOMED / RxNorm / LOINC, standard_concept='S']
  Map -->|concept_id = 0| Unmapped[Unmapped source codes<br/>source_concept fallback + audit %]
  Std --> Pick[Pick anchor concepts<br/>e.g. RxNorm ingredient]
  Pick --> Desc[Descendant expansion<br/>CONCEPT_ANCESTOR]
  Desc --> Excl[Apply exclusions<br/>combination / unwanted children]
  Excl --> Freeze[Version-frozen resolved set<br/>+ vocabulary version, JSON in repo]
  Freeze --> Apply[Apply to CDM table<br/>drug_concept_id IN resolved set]
  Unmapped -.recover.-> Apply
  Apply --> Eval[Evaluate phenotype<br/>PheValuator / chart-review PPV & sensitivity]
Concept-set development workflow in OMOP. Source codes are mapped to standard concepts; the expression selects anchors, expands descendants, prunes exclusions, and is version-frozen before being applied to the CDM and evaluated. Unmapped (concept_id = 0) source codes are audited and recovered via a source fallback.
flowchart TD
  Ing[Ingredient: ACE inhibitors<br/>RxNorm ingredient concept_ids] --> Lis[Lisinopril products]
  Ing --> Ram[Ramipril products]
  Ing --> Ena[Enalapril products]
  Lis --> L1[Lisinopril 10 MG Oral Tablet]
  Lis --> L2[Lisinopril 20 MG Oral Tablet]
  Ena --> E1[Enalapril 5 MG Oral Tablet]
  Ena --> EX[Enalapril/HCTZ combo tablet<br/>EXCLUDED]
  style EX stroke-dasharray: 5 5
Ancestor/descendant tree behind include-descendants expansion. Defining the set at the ingredient level inherits every clinical-drug descendant; an exclusion branch removes unwanted children such as fixed-dose combination products.

Worked example

Scenario

A pharmacoepidemiology team wants to identify all patients with a recorded type 2 diabetes diagnosis in a claims database that has been converted to OMOP CDM. They start with the SNOMED concept for 'Type 2 diabetes mellitus' as their seed, expand to all descendant codes (e.g., 'Type 2 diabetes with diabetic nephropathy'), and then exclude a small number of child codes that refer to neonatal or secondary diabetes — categories the team's clinical reviewer flagged as outside the study population. The table below shows the concept-set expression they build, with each row representing one entry and its include/exclude flag.

Dataset

Concept-set expression for type 2 diabetes mellitus (SNOMED, OMOP vocabulary). Each row is one entry in the expression; the include_descendants flag tells OMOP to pull all child codes under that ancestor.

concept_idconcept_namevocabularyinclude_descendantsflag
201826Type 2 diabetes mellitusSNOMEDTrueINCLUDE
4058243Secondary diabetes mellitusSNOMEDTrueEXCLUDE
4299544Neonatal diabetes mellitusSNOMEDTrueEXCLUDE

Steps

1Start with the seed concept: concept_id 201826 ('Type 2 diabetes mellitus') is the SNOMED standard concept at the right level of specificity — broad enough to capture all type 2 patients, specific enough to exclude type 1.
2Turn on include_descendants for the seed: OMOP's CONCEPT_ANCESTOR table now automatically adds all more-specific child codes beneath 201826, such as 'Type 2 diabetes mellitus with diabetic chronic kidney disease' (concept_id 4299544 is a different example — the tree has dozens of such children).
3A clinical reviewer scans the full resolved list of descendants and flags two branches to exclude: secondary diabetes (caused by another condition, not the same population) and neonatal diabetes (a distinct neonatal entity); both are added as EXCLUDE rows with include_descendants also turned on so their own children are also removed.
4The expression is saved and then resolved against the current OMOP vocabulary version: OMOP looks up every descendant of 201826, removes every descendant of 4058243 and 4299544, and returns the final list of concept_ids.
5That final resolved list — not just the three-row expression — is saved alongside the vocabulary version number to the study repository so any future analyst can reproduce the exact same code list.

Result

The concept-set expression contains 3 rows (1 include anchor, 2 exclude branches). When resolved against the vocabulary, it returns a specific list of standard concept_ids covering type 2 diabetes and all its clinical subtypes, with secondary and neonatal forms removed. Any DRUG_EXPOSURE or CONDITION_OCCURRENCE row in the database whose standard concept_id appears in that resolved list is counted; the expression is reproducible by anyone who has the same vocabulary version, and it travels unchanged to any other OMOP database in a network study.

Trade-offs

vs. Hand curated source code lists (ICD/NDC strings in a macro)
Pros of this
Portable across data partners and vocabulary updates; auto-expands to descendants; reviewable in standard tooling; survives new child codes without re-editing.
vs. Blind include descendants expansion
Pros of this
Inheriting children captures new products/subtypes automatically and reduces maintenance.
vs. Reusing an off the shelf published/library concept set unmodified
Pros of this
Inherits prior validity evidence and cross-study comparability.

Runnable example

Resolve an OMOP concept-set expression and count distinct exposed persons from claims-style OMOP tables. Required inputs (already loaded as DataFrames from the CDM): concept : concept_id, vocabulary_id, concept_class_id, standard_concept, concept_name concept_ancestor : ancestor_concept_id, descendant_concept_id...

requires: pandas
import pandas as pd

# Concept-set expression: anchor ingredient concept_ids to INCLUDE (with descendants),
# and combination-product concept_ids to EXCLUDE. These come from clinical review.
INCLUDE_INGREDIENTS = [1308216, 1341927, 1340128]   # lisinopril, ramipril, enalapril (RxNorm ingredients)
EXCLUDE_CONCEPTS    = [1310756]                      # e.g., a fixed-dose ACEi+thiazide combination

def resolve_concept_set(concept, concept_ancestor,
                        include_ingredients, exclude_concepts) -> pd.Series:
    # Standard concepts only; ingredients must be standard 'S' to be valid anchors.
    std = concept.loc[concept["standard_concept"] == "S", "concept_id"]
    # Descendant expansion: every clinical drug / product under the chosen ingredients.
    desc = concept_ancestor.loc[
        concept_ancestor["ancestor_concept_id"].isin(include_ingredients),
        "descendant_concept_id",
    ]
    included = set(desc) | set(include_ingredients)
    resolved = (included & set(std)) - set(exclude_concepts)
    return pd.Series(sorted(resolved), name="concept_id")

def exposed_counts(drug_exposure, resolved) -> dict:
    rset = set(resolved)
    on_standard = drug_exposure[drug_exposure["drug_concept_id"].isin(rset)]
    # Unmapped fallback: rows that failed source->standard mapping (drug_concept_id == 0).
    unmapped = drug_exposure[drug_exposure["drug_concept_id"] == 0]
    return {
        "resolved_concepts": len(rset),
        "exposed_persons": on_standard["person_id"].nunique(),
        "fills": len(on_standard),
        "pct_unmapped_rows": round(100 * len(unmapped) / max(len(drug_exposure), 1), 2),
    }

resolved = resolve_concept_set(concept, concept_ancestor,
                               INCLUDE_INGREDIENTS, EXCLUDE_CONCEPTS)
print(exposed_counts(drug_exposure, resolved))

Citations

FOUNDATIONAL / METHODS
  1. [1]Hripcsak G, Albers DJ. Next-generation phenotyping of electronic health records. Journal of the American Medical Informatics Association. 2013;20(1):117-121.
  2. [2]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.
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]Springate DA, Kontopantelis E, Ashcroft DM, et al. ClinicalCodes: an online clinical codes repository to improve the validity and reproducibility of research using electronic medical records. PLoS ONE. 2014;9(6):e99825.