← Methods repository
CONCEPTFOUNDATIONALPYTHON · R7 citations

Code Crosswalks and Mappings Between Coding Systems

The family of official and community-maintained translation tables that link one medical coding system to another — ICD-9-CM to ICD-10-CM via CMS General Equivalence Mappings, NDC to RxNorm via the NLM RxNav API, NDC to HCPCS J-codes via the CMS Average Sales Price crosswalk, and ICD-10-CM to SNOMED CT via the NLM rule-based map — allowing researchers to align code lists across vocabulary transitions or data sources while managing the approximation, one-to-many expansions, and version drift inherent in every translation.

Data Standardcoding-systemdata-standardprimitivecrosswalkmappingterminologyicd-10-cmicd-9-cm
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

Medical claims and electronic health records use different coding systems to label diagnoses, drugs, and procedures — and those systems change over time. A code crosswalk is an official translation table that links a code in one system to the closest match in another, the way a bilingual dictionary links words between languages. The key warning that beginners miss is that these translations are approximations, not exact equivalences — one old code often expands into several new ones, and translating back does not return the original set.

When to use it
Use raw crosswalks on non-OMOP data; use OMOP concept sets on OMOP-CDM data, where "Maps to" has already absorbed the crosswalk logic.
Prefer the OMOP approach for multi-site network studies; prefer direct crosswalk application for single-site studies on proprietary extracts.
Watch out for
Raw crosswalks require manual version management and do not benefit from the community-curated, version-controlled OMOP CONCEPT_RELATIONSHIP table, which integrates GEM-equivalent mappings with descendant hierarchies...
OMOP's versioned ETL embeds the crosswalk in a auditable, reproducible pipeline; ad hoc crosswalk application is harder to document and version-control.

A code crosswalk (also called a code mapping) is an official or curated translation table that associates codes in one medical coding system with the closest equivalent codes in another. Crosswalks exist because the healthcare data ecosystem spans multiple, independently governed vocabularies — ICD-9-CM for legacy diagnoses, ICD-10-CM for current diagnoses, NDC for drug package identity, RxNorm for drug ingredients, HCPCS/CPT for procedures and administered drugs, and SNOMED CT for clinical concepts — and no single coding system covers all data types or all calendar periods. Real-world evidence (RWE) studies routinely span vocabulary transitions or assemble data from sources that use different systems, making crosswalk literacy a foundational skill.

The inventory of major crosswalks

GEMs — General Equivalence Mappings (CMS/NCHS, ICD-9-CM ↔ ICD-10-CM/PCS). GEMs were developed jointly by CMS and the National Center for Health Statistics to support the October 1, 2015 transition from ICD-9-CM to ICD-10-CM (diagnoses) and ICD-10-PCS (inpatient procedures). Two files exist for each direction: the forward map (ICD-9-CM → ICD-10-CM) and the backward map (ICD-10-CM → ICD-9-CM). Each row carries four critical flags:

  1. the approximate flag (0 = exact match, 1 = approximate/best available),
  2. the no map flag (code has no usable equivalent),
  3. the combination flag (the ICD-9 concept requires multiple ICD-10 codes to fully express), and
  4. the scenario/choice list flags that group one-to-many alternatives. A single ICD-9-CM code commonly maps forward to 3–10 ICD-10-CM codes, and many maps carry approximate = 1, meaning granularity was genuinely lost or gained in translation. GEMs were last updated for FY2018; they are officially retired but remain the de facto standard tool for any study spanning the pre- and post-October-2015 period. Researchers must freeze the GEM version and document it, because there will be no future updates to reconcile.

NDC ↔ RxNorm (NLM RxNav API, monthly updates). The National Library of Medicine's RxNorm is the standard for drug ingredient, clinical drug (product), and branded product identity in the United States. The NLM RxNav API resolves an 11-digit NDC (as it appears in a claims or pharmacy dispensing record) to its RxNorm Concept Unique Identifier (RXCUI) at the ingredient or clinical drug level. This mapping is critical for two reasons: NDCs change whenever a manufacturer repackages or reformulates a product (a single ingredient can have hundreds of active NDCs at any moment), and NDC lists therefore rot rapidly; defining drug exposure at the RxNorm ingredient level and resolving NDCs through RxNorm insulates a study from NDC churn. The NLM also maintains historical NDC endpoints for mapping retired codes. The RxNorm mapping is updated monthly; a study using a snapshot must document the snapshot date.

NDC ↔ HCPCS (CMS Average Sales Price Drug Pricing crosswalk, quarterly). Medicare Part B covers many drugs administered in clinical settings and reimburses them under HCPCS Level II J-codes. The CMS publishes quarterly ASP (Average Sales Price) Drug Pricing files that include an NDC-to-HCPCS crosswalk, allowing researchers to recover the drug identity behind a J-code claim. This is essential for medical-benefit drug studies: a claim for J9271 (pembrolizumab) is informative on its own, but the NDC crosswalk confirms the specific product and links back to the RxNorm ingredient for pharmacological classification. The crosswalk also resolves "not otherwise classified" NOC codes (e.g., J3490, J9999), which appear when a drug has no dedicated J-code. Because NDCs change with ASP submission cycles, the specific quarterly file version must be documented.

SNOMED CT ↔ ICD-10-CM (NLM rule-based map, for reimbursement derivation). The NLM maintains a rule-based SNOMED CT–to–ICD-10-CM map that supports translation from clinical documentation systems (which may use SNOMED CT) to reimbursement coding (ICD-10-CM). The map is intentionally lossy: SNOMED CT's clinical granularity (laterality, severity, morphology) cannot always survive the translation to ICD-10-CM's billing categories. Researchers should treat SNOMED↔ICD-10-CM as a triage tool, not a reliable phenotype definition, and re-derive code lists natively in each system whenever possible.

CPT/HCPCS ↔ SNOMED CT and ICD-10-PCS ↔ SNOMED CT (partial maps). Partial procedure maps exist between CPT and SNOMED CT and between ICD-10-PCS and SNOMED CT, but coverage is incomplete and maintained by different organizations on different schedules. These are useful for concept-level harmonization across datasets but require the same caveat: verify coverage fractions before relying on them.

UMLS Metathesaurus as the CUI-level hub. The NLM Unified Medical Language System (UMLS) integrates more than 200 biomedical vocabularies under a single Concept Unique Identifier (CUI), enabling lookup from any supported source system to any other. The Metathesaurus connects ICD-9-CM, ICD-10-CM, SNOMED CT, RxNorm, LOINC, MeSH, and many others under one roof. UMLS requires a free UMLS Metathesaurus License; it is not public-domain. The breadth makes it the most comprehensive single hub, but its mappings vary in source and quality — some are algorithmically generated and should be reviewed for the specific concept.

OMOP CONCEPT_RELATIONSHIP "Maps to" as the operational crosswalk hub. Within the OMOP Common Data Model, the CONCEPT_RELATIONSHIP table stores the "Maps to" relationship that connects every source code (ICD-9-CM, ICD-10-CM, NDC, CPT, HCPCS, SNOMED CT) to its standard concept. This is a continuously maintained, versioned crosswalk hub — the OHDSI community updates it regularly, and new vocabulary versions are released quarterly. Researchers using OMOP inherit the crosswalk automatically through the ETL, but must still document the vocabulary version used (it is stored in the VOCABULARY table) and understand that source codes without a "Maps to" relationship fall to concept_id = 0 (unmapped) and are invisible to standard-concept queries.

The methodological core: crosswalks are approximations, not identities

Every crosswalk changes the measurement. This is the most important principle of crosswalk methodology, and the one most frequently violated. The specific failure modes are:

  • One-to-many inflation. When a single ICD-9-CM code maps forward to multiple ICD-10-CM codes, applying the GEM mechanically to expand a code list inflates the code count. If an analyst counts diagnosis codes or trends in code frequency, the ICD-10 transition will appear to generate more diagnoses — not because disease incidence changed, but because each ICD-9 concept now has more granular children. This is a cartographic artifact, not a clinical signal, and it is one of the primary drivers of apparent trend discontinuities at 2015-10-01.
  • Granularity loss on backward maps. When translating from ICD-10-CM (more granular) backward to ICD-9-CM (less granular), multiple distinct ICD-10-CM codes often collapse onto a single ICD-9-CM code. Information is irreversibly lost.
  • Asymmetry: forward ∘ backward ≠ identity. Applying the forward map and then the backward map does not return the original code set. This is not a defect in the GEMs; it is a logical consequence of the coding system transition — the two systems have different granularity and different clinical partitions. Researchers who assume round-trip equivalence will produce incorrect overlap analyses.
  • Version drift. Crosswalks are updated on different schedules (GEMs: frozen at FY2018; ASP crosswalk: quarterly; RxNorm: monthly; OMOP vocabularies: quarterly). A study that runs across multiple vocabulary update cycles may apply different translations to different time periods, introducing a time-varying mapping artifact unless the researcher pins a single snapshot and documents it.
  • Approximate flag is the norm, not the exception. In the forward GEM for ICD-9-CM to ICD-10-CM, the majority of entries carry approximate = 1, meaning the mapping is the best available, not a true clinical equivalence. Treating approximate = 0 (exact) rows as high-confidence and approximate = 1 rows as requiring clinical review is the standard practice.

Best practice: map the concept, not the code list

The gold standard is to re-derive the code list natively in each coding system — starting from the clinical concept, asking a subject-matter expert to curate the relevant codes independently in ICD-9-CM and in ICD-10-CM — rather than mechanically translating the ICD-9 list forward. Use the GEM as a first-pass triage tool to identify candidate codes in the target system, then clinician-review the candidate set. For transition-spanning trends, run ITS (interrupted time series) diagnostics at 2015-10-01 to distinguish cartographic from biological discontinuities.

Licensing and public-domain status

  • GEMs (ICD-9-CM ↔ ICD-10-CM/PCS): public domain, freely downloadable from CMS.
  • CMS ASP NDC-HCPCS crosswalk: public domain, freely downloadable from CMS.
  • NLM RxNav / RxNorm API: public domain for the API and underlying data.
  • UMLS Metathesaurus: requires a free UMLS Metathesaurus License (NLM sign-up).
  • SNOMED CT: requires a NRC (National Release Center) license in the US; obtained free through NLM for most research uses.

Pros, cons, and trade-offs — specific and comparative

  • Crosswalk (mechanical translation) vs concept re-derivation (native curation): Mechanical translation via GEMs is fast and reproducible, and it produces a code list that can be traced to an official table. Cost: it inherits all GEM approximations and one-to-many expansions, and it can include codes that a clinician familiar with the target system would exclude and miss codes the GEM did not capture. Prefer concept re-derivation for the final, study-grade code list; use the crosswalk as triage. The GEM-first-then-clinician-review workflow is the standard recommended by AHIMA and OHDSI.
  • Pinned crosswalk snapshot vs live API: Using a live RxNorm or OMOP vocabulary API at query time ensures the latest mappings but means the study results may change if the API is called at different times. For reproducible research, pin and archive the crosswalk snapshot with a date stamp. Prefer a pinned snapshot for published research. The OMOP versioned vocabulary download (available from the Athena portal) is the standard mechanism for this in OMOP studies.
  • GEMs vs OMOP "Maps to" for ICD-9/ICD-10 bridging: GEMs operate at the raw ICD code level; OMOP "Maps to" operates at the standard concept level (SNOMED for conditions, RxNorm for drugs) and abstracts away the ICD version. For network studies using OMOP, the "Maps to" relationships are preferred because they are vocabulary-version-agnostic. For studies on raw claims not in OMOP, GEMs are the correct tool. Prefer OMOP "Maps to" when the data are in OMOP-CDM; prefer GEMs when working directly with ICD claims.

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

  • As a substitute for clinical concept re-derivation. Applying the GEM forward map to generate an ICD-10-CM code list and treating the output as a validated phenotype is misleading. The GEM was designed for billing and administrative continuity, not for research phenotyping. The one-to-many expansions and approximate flags mean that a researcher who trusts the GEM output without clinical review will include codes that are clinically irrelevant and miss others that are clinically central.
  • For trend analysis without ITS diagnostics. Using a crosswalk to translate a code list and then measuring trends across the transition without testing for a cartographic discontinuity at 2015-10-01 is actively misleading. The coding transition itself introduces apparent level changes and slope breaks in virtually every ICD-based condition series; reporting these as clinical trends is incorrect.
  • When the approximate flag is ignored. Selecting only the zero-flag rows from the GEM (exact matches) and discarding approximate rows will silently exclude the majority of the mapping — most ICD-9 concepts have no exact ICD-10 equivalent. Ignoring the flag entirely and treating all rows as equivalent is also wrong because it obscures the uncertainty.
  • When version drift is ignored. Applying a quarterly ASP crosswalk from a different quarter than the study period, or applying the FY2016 GEM to FY2018 data, introduces uncontrolled mapping variation. For regulatory submissions (FDA, payer dossiers), the crosswalk version must be documented and justified.
  • For MA-only or capitated data. If the claims data derive from capitated arrangements that do not produce adjudicated ICD codes (e.g., Medicare Advantage encounter data with systematically missing diagnoses), no crosswalk can recover what was never coded. Restrict to FFS-observable person-time before applying any ICD-based crosswalk.

Data-source operational depth

  • Claims (FFS commercial / Medicare FFS): ICD-9/ICD-10-CM on the diagnosis fields; NDC on pharmacy claims; HCPCS/CPT on medical claims. Apply GEMs across the 2015 transition; use the ASP crosswalk to resolve J-codes to NDCs; use RxNorm to normalize NDC to ingredient. Failure mode: procedure codes on medical claims may be CPT (AMA-licensed, not in GEMs) — use the partial CPT↔SNOMED map or OMOP "Maps to" if the data are in OMOP. Document whether the study population includes Medicare Advantage spans; the NDC-HCPCS crosswalk is irrelevant for MA claims where drug administration records may be absent.
  • EHR: Problem lists and encounter diagnoses may carry ICD-10-CM or SNOMED CT depending on the system and configuration year. The SNOMED↔ICD-10-CM NLM map may be needed to align EHR concepts with claims. Orders and prescriptions may carry local drug codes that require a custom mapping step before RxNorm normalization.
  • Registry: Disease-specific registries often use registry-specific codes that require a project-specific crosswalk to standard vocabularies; the GEMs and RxNorm crosswalks do not cover registry-specific coding schemes.
  • OMOP-CDM: The ETL handles all source-to-standard mapping through the CONCEPT_RELATIONSHIP "Maps to" table; researchers build concept sets on standard concepts and inherit the crosswalk automatically. The workflow for a transition-spanning study is to verify that the ETL bridged both ICD-9-CM and ICD-10-CM to the same SNOMED CT standard concept and to quantify the concept_id = 0 unmapped fraction on both sides of the transition date.

Decision diagram

flowchart TD
  A[Clinical concept\ne.g. COPD exacerbation] -->|Start here| B[ICD-9-CM code list\npre-Oct 2015]
  A -->|Re-derive natively| C[ICD-10-CM code list\npost-Oct 2015]
  B -->|GEM forward map\napprox flag + 1:many| D[Candidate ICD-10-CM codes\nrequire clinical review]
  D -->|Clinician review\naccept / reject / add| C
  C -->|GEM backward map\ngranularity loss| E[ICD-9-CM round-trip\nNOT equal to B]
  B -->|Study crosses 2015-10-01| F[Transition-spanning analysis]
  C --> F
  F -->|ITS diagnostics at 2015-10-01| G[Cartographic vs biological\ndiscontinuity separated]
  H[NDC on pharmacy claim] -->|RxNorm API\nmonthly snapshot| I[RxNorm RXCUI\ningredient level]
  J[HCPCS J-code on medical claim] -->|CMS ASP crosswalk\nquarterly| K[NDC + drug identity]
  K --> I
  I --> L[Drug exposure defined\nat ingredient level]
  M[OMOP source code\nICD/NDC/CPT] -->|CONCEPT_RELATIONSHIP\nMaps to| N[Standard concept\nSNOMED/RxNorm/LOINC]
Crosswalk ecosystem: the GEM forward map generates candidate ICD-10-CM codes that require clinical review; the backward map does not restore the original set (asymmetry). NDC resolves to RxNorm via the NLM API; J-codes resolve to NDC via the CMS ASP crosswalk. OMOP's CONCEPT_RELATIONSHIP Maps to relationship abstracts all of these into a single versioned hub for OMOP-CDM studies.

Worked example

Scenario

An analyst is building a study on chronic obstructive pulmonary disease (COPD) hospitalizations in US commercial claims. The data span January 2013 through December 2018, which means the cohort crosses the ICD-9-to-ICD-10-CM transition on October 1, 2015. The analyst starts with one representative ICD-9-CM COPD hospitalization code — 491.21 (obstructive chronic bronchitis with acute exacerbation) — and wants to know what the GEM forward map produces and whether applying the backward map would return the starting code.

Dataset

Forward GEM rows for ICD-9-CM 491.21 (obstructive chronic bronchitis with acute exacerbation). Each row is one entry in the CMS FY2018 GEM forward-map file.

icd9_codeicd10_codeapproximate_flagno_map_flagcombination_flagscenariochoice_list
491.21J44.110011
491.21J44.010012
FIG. 1 — DESIGN TIMELINE
ICD-9-to-ICD-10 GEM expansion for COPD (491.21) — code cardinality across transition
ICD-9-to-ICD-10 GEM expansion for COPD (491.21) — code cardinality across transition

Steps

1The forward GEM for 491.21 returns 2 ICD-10-CM codes (J44.1 COPD with acute exacerbation and J44.0 COPD with acute lower respiratory infection). The approximate flag is 1 on both rows — neither is an exact equivalence.
2The scenario flag of 1 and choice_list values of 1 and 2 mean these two codes are alternatives; the GEM is presenting them as options rather than requiring both. A researcher must decide clinically which (or both) to include.
3The code count expands: one ICD-9 code becomes 2 candidate ICD-10 codes. If the analyst includes both, every pre-2015 hospitalization coded 491.21 will be matched against 2 ICD-10 codes post-2015 — inflating apparent code frequency at the transition even if COPD hospitalization rates are unchanged.
4Now apply the backward GEM to J44.1 (the primary forward-map result). The backward GEM returns 3 ICD-9-CM codes: 491.21, 491.20, and 496. The starting code 491.21 appears, but so do 2 additional codes — the round trip is NOT the original single code.
5Asymmetry count: forward map from 1 ICD-9 code produces 2 ICD-10 codes; backward map from the primary result produces 3 ICD-9 codes. 2 - 1 = 1 net inflation in the forward direction; 3 - 1 = 2 additional codes in the backward direction; 2 + 3 = 5 total codes involved in the round trip versus 1 original code.

Result

Starting from 1 ICD-9-CM code (491.21), the forward GEM produces 2 ICD-10-CM candidate codes (approximate = 1 on both). Backward-mapping J44.1 returns 3 ICD-9-CM codes — 2 more than the original 1. The round trip 1 -> 2 -> 3 demonstrates asymmetry: forward does not equal backward, and neither direction produces cardinality = 1. An analyst who naively counts "codes ever assigned to this condition" across the transition window will see an apparent 2 / 1 = 2.0x code-count multiplication at 2015-10-01 that is entirely cartographic.

Trade-offs

Pros of this
Crosswalks (GEMs, ASP, RxNorm) operate directly on raw code values and do not require a CDM transformation step, making them accessible on any claims extract without a full OMOP ETL.
Pros of this
Crosswalk methodology is vocabulary-agnostic and applies to any claims dataset regardless of whether an OMOP ETL exists.

Runnable example

Applies the CMS FY2018 GEM forward map to an ICD-9-CM code list and reports the full expansion including approximate flags, combination entries, and one-to-many counts. Then applies the backward map to the primary forward result to demonstrate asymmetry (forward-then-backward does not return the original set).

requires: pandas>=1.3
"""
Code Crosswalk Utilities — GEM expansion + ASP NDC-HCPCS join
=============================================================
Applies the CMS FY2018 GEM forward map to an ICD-9-CM code list,
reports one-to-many expansion and approximate flags, then demonstrates
backward-map asymmetry. Includes an ASP NDC-HCPCS J-code resolver.

Input files (public domain, download from CMS GEMs archive and ASP pages):
  gem_forward.tsv  — CMS 2018 ICD-9-CM to ICD-10-CM GEM forward map
  gem_backward.tsv — CMS 2018 ICD-10-CM to ICD-9-CM GEM backward map
  asp_crosswalk.csv — CMS quarterly ASP NDC-HCPCS crosswalk
"""
import pandas as pd
from pathlib import Path


def load_gem(path: str | Path) -> pd.DataFrame:
    """Load a CMS GEM flat file (space-delimited, no header).

    Columns (per CMS format): source_code, target_code,
    approximate, no_map, combination, scenario, choice_list.
    """
    cols = [
        "source_code", "target_code",
        "approximate", "no_map", "combination",
        "scenario", "choice_list",
    ]
    df = pd.read_csv(
        path, sep=r"\s+", header=None, names=cols,
        dtype=str
    )
    # Convert flag columns to int for filtering
    for c in ["approximate", "no_map", "combination", "scenario", "choice_list"]:
        df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0).astype(int)
    return df


def apply_forward_gem(
    code_list: list[str],
    gem_forward: pd.DataFrame,
    include_approximate: bool = True,
) -> pd.DataFrame:
    """Expand an ICD-9-CM code list via the GEM forward map.

    Returns all matching rows, with one_to_many_count added.
    include_approximate=False restricts to exact matches only
    (WARNING: drops the majority of rows — use for triage only).
    """
    df = gem_forward[gem_forward["source_code"].isin(code_list)].copy()
    if not include_approximate:
        df = df[df["approximate"] == 0]

    # Count how many ICD-10 targets each ICD-9 source maps to
    counts = (
        df[df["no_map"] == 0]
        .groupby("source_code")["target_code"]
        .count()
        .rename("one_to_many_count")
    )
    df = df.merge(counts, on="source_code", how="left")

    no_map_codes = df[df["no_map"] == 1]["source_code"].unique()
    if len(no_map_codes):
        print(
            f"WARNING: {len(no_map_codes)} code(s) have no_map=1 "
            f"(no GEM equivalent): {list(no_map_codes)}"
        )
    return df


def check_roundtrip_asymmetry(
    source_codes: list[str],
    gem_forward: pd.DataFrame,
    gem_backward: pd.DataFrame,
) -> dict:
    """Apply forward then backward and report asymmetry.

    Returns dict with original code count, forward count, roundtrip count.
    A round-trip that does NOT return the original set demonstrates asymmetry.
    """
    # Forward: ICD-9 -> ICD-10
    fwd = apply_forward_gem(source_codes, gem_forward)
    icd10_codes = fwd[fwd["no_map"] == 0]["target_code"].unique().tolist()

    # Backward: ICD-10 -> ICD-9
    bwd = gem_backward[gem_backward["source_code"].isin(icd10_codes)]
    icd9_roundtrip = bwd["target_code"].unique().tolist()

    original_set = set(source_codes)
    roundtrip_set = set(icd9_roundtrip)
    added = roundtrip_set - original_set
    lost = original_set - roundtrip_set

    return {
        "original_codes": source_codes,
        "original_count": len(source_codes),
        "icd10_forward_codes": icd10_codes,
        "icd10_forward_count": len(icd10_codes),
        "icd9_roundtrip_codes": icd9_roundtrip,
        "icd9_roundtrip_count": len(icd9_roundtrip),
        "codes_added_by_roundtrip": sorted(added),
        "codes_lost_by_roundtrip": sorted(lost),
        "is_symmetric": original_set == roundtrip_set,
    }


def load_asp_crosswalk(path: str | Path) -> pd.DataFrame:
    """Load the CMS quarterly ASP NDC-HCPCS crosswalk CSV.

    CMS publishes these as CSV/Excel; key columns: HCPCS_CD, NDC, LONG_DESC.
    Adjust column names to match the actual file header.
    """
    df = pd.read_csv(path, dtype=str)
    # Normalize column names to lowercase, strip spaces
    df.columns = [c.strip().lower().replace(" ", "_") for c in df.columns]
    return df


def resolve_jcode_to_ndc(
    claims: pd.DataFrame,
    asp_crosswalk: pd.DataFrame,
    hcpcs_col: str = "hcpcs_cd",
    ndc_col: str = "ndc",
) -> pd.DataFrame:
    """Join ASP crosswalk to medical claims on HCPCS code.

    Recovers drug identity (NDC and long description) for J-codes and NOC codes.
    Returns claims with ndc_from_asp and drug_description columns added.

    Note: multiple NDCs may map to one HCPCS code; the join produces one row per
    NDC match per claim. Deduplicate using claim-level NDC field if available.
    """
    asp_sub = asp_crosswalk[[hcpcs_col, ndc_col, "long_desc"]].rename(
        columns={
            ndc_col: "ndc_from_asp",
            "long_desc": "drug_description_from_asp",
        }
    )
    enriched = claims.merge(asp_sub, on=hcpcs_col, how="left")
    unresolved = enriched["ndc_from_asp"].isna().sum()
    if unresolved:
        print(
            f"INFO: {unresolved} claim rows could not be matched to an NDC "
            f"via the ASP crosswalk (missing HCPCS or new drug not in this quarter)."
        )
    return enriched


# ── Example usage ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    # Load GEM files (download from CMS GEMs archive)
    # gem_fwd = load_gem("2018_I9gem.txt")
    # gem_bwd = load_gem("2018_I10gem.txt")

    # COPD exacerbation example (see worked_example above)
    copd_icd9 = ["491.21"]

    # Demonstrate forward expansion and asymmetry:
    # result = check_roundtrip_asymmetry(copd_icd9, gem_fwd, gem_bwd)
    # print(result)
    # Expected: original_count=1, icd10_forward_count=2, icd9_roundtrip_count=3
    # is_symmetric=False (round trip 1 -> 2 -> 3, not 1 -> 1 -> 1)

    print("GEM crosswalk utilities loaded. Provide GEM files to run.")

Citations

FOUNDATIONAL / METHODS
  1. [1]Boyd AD, Li JJ, Burton MD, et al. The discriminatory cost of ICD-10-CM transition between clinical specialties: metrics, case study, and mitigating tools. Journal of the American Medical Informatics Association. 2013;20(4):708-717.
  2. [2]Slavova S, Costich JF, Luu H, Fields JR. Interrupted time series design to evaluate the effect of the ICD-9-CM to ICD-10-CM coding transition on injury hospitalization trends. Injury Epidemiology. 2018;5(1):34.
  3. [3]Liu S, Wei Ma, Moore R. RxNorm: prescription for electronic drug information exchange. IT Professional. 2005;7(5):17-23.
APPLIED EXAMPLES
  1. [4]Bodenreider O. The Unified Medical Language System (UMLS): integrating biomedical terminology. Nucleic Acids Research. 2004;32(Suppl 1):D267-D270.
REPORTING & GUIDANCE
  1. [5]Centers for Medicare and Medicaid Services. ICD-10-CM and ICD-10-PCS GEM Archive. Baltimore, MD: CMS; FY2018 (last updated). Accessed 2026.
  2. [6]National Library of Medicine. Unified Medical Language System (UMLS). Bethesda, MD: NLM. Accessed 2026.
  3. [7]Centers for Medicare and Medicaid Services. Medicare Part B Drug Average Sales Price: ASP Pricing Files (NDC-HCPCS Crosswalk). Baltimore, MD: CMS; updated quarterly. Accessed 2026.