LCM Logo

Medical coding & terminologies

ICD, SNOMED CT, LOINC, RxNorm, CPT, UMLS - the vocabularies that give health data meaning.

Sources last verified

Terminologies are the layer people skip, and skipping it is the single most common cause of wrong answers in health data analysis. The exchange standards and data models are containers; the codes inside them are the actual clinical content.

Two properties make this harder than it looks:

  • Terminologies encode purpose, not just meaning. ICD-10-CM exists to support billing. A diagnosis code on a claim is evidence that someone was reimbursed for treating a condition — not that the patient has it. Using billing codes as clinical truth is the field's most reliable source of bias.
  • Coverage overlaps but never coincides. The same lab test has a LOINC code, a local code, and possibly a SNOMED CT concept, and the mappings are many-to-many and incomplete.

By domain

DomainStandardWho maintains itFree?
Diagnoses (billing)ICD-10-CMCDC/NCHS (US), WHOYes
Diagnoses (clinical)SNOMED CTSNOMED InternationalMember countries only
Procedures (inpatient)ICD-10-PCSCMSYes
Procedures (outpatient)CPTAMANo — licensed
Supplies, drugs administeredHCPCS Level IICMSYes
Labs and observationsLOINCRegenstrief InstituteYes, with registration
Drugs (clinical)RxNormNLMYes
Drugs (package)NDCFDAYes
Drugs (classification)ATCWHORestricted for bulk use
VaccinesCVXCDCYes
Adverse eventsMedDRAICHNo — subscription
PhenotypesHPOMonarch InitiativeYes
Everything, cross-mappedUMLSNLMYes, with a license

Licensing is a real engineering constraint here, not a footnote. CPT and MedDRA cost money and their terms restrict redistribution — you cannot ship a container image with the CPT code descriptions baked in. SNOMED CT is free only in member countries (the US is one, via the UMLS). Check this before you design a system that depends on shipping code descriptions to users.

ICD

The International Classification of Diseases — the oldest and most widely used of them all.

  • Introduced: Traceable to the 1890s; WHO took over from ICD-6 in 1948. ICD-9 in 1979, ICD-10 in 1990. ICD-11 was adopted by the World Health Assembly in 2019 and came into effect on 1 January 2022
  • Purpose: Originally mortality statistics; now overwhelmingly billing and administrative reporting
  • Status today: ICD-10 is the global standard. The US has not adopted ICD-11 for morbidity coding and continues to use its own clinical modification
  • Structure: A strict hierarchy, encoded in the code string itself, so prefix matching works — E11.9 is a child of E11, which sits under E00-E89

The variants matter and are routinely confused:

VariantScopeSizeUsed for
ICD-10WHO international~14,000 codesInternational mortality/morbidity statistics
ICD-10-CMUS clinical modification~70,000 codesUS diagnosis coding since 1 Oct 2015
ICD-10-PCSUS procedure coding~80,000 codesUS inpatient procedures only
ICD-9-CMRetired US predecessor~17,000 codesData before 1 Oct 2015
ICD-11WHO, current~17,000 entitiesAdoption is gradual and patchy
icd-10-cm-hierarchy.txt
E00-E89    Endocrine, nutritional and metabolic diseases
  E08-E13  Diabetes mellitus
    E11    Type 2 diabetes mellitus
      E11.2   ...with kidney complications
        E11.21  ...with diabetic nephropathy
        E11.22  ...with diabetic chronic kidney disease
      E11.9   ...without complications

The 1 October 2015 ICD-9 to ICD-10 transition is a discontinuity in every US longitudinal dataset. Any cohort spanning that date needs explicit handling. CMS publishes General Equivalence Mappings (GEMs) as a crosswalk, but they are approximate and directionally asymmetric — forward-mapping then back-mapping does not return the original code. Never present a GEM-crossed trend line without saying so.

RPythonJuliaRust
LanguagePackageNotes
RicdComorbidity mapping, validation, explanation, and ICD-9/ICD-10 handling. The most complete single package in any language
RcomorbidityCharlson and Elixhauser scores from ICD-9/10, with the published weightings
RtouchICD-9/ICD-10 crosswalking via the CMS GEMs
RicdpicrInjury severity scoring from ICD codes
Rmedicalrisk, ICD10gmRisk indices; the German ICD-10-GM modification
Pythonsimple-icd-10Hierarchy navigation and code validation for WHO ICD-10
PythoncomorbidipyCharlson and Elixhauser, the Python counterpart to comorbidity
Pythonicd10-cmUS clinical modification lookups. Small and simple
APIWHO ICD APIAuthoritative for ICD-10 and ICD-11. Free with registration

SNOMED CT

The most clinically expressive terminology in existence, and the one designed for recording care rather than billing for it.

  • Introduced: Roots in SNOP (1965); SNOMED CT created in 2002 by merging SNOMED RT with the UK's Clinical Terms (Read codes). Stewardship moved to SNOMED International in 2007
  • Purpose: Represent clinical meaning precisely enough for decision support and computation
  • Size: Over 350,000 active concepts
  • Release cadence: International edition twice yearly, plus national extensions
  • Status today: The designated clinical terminology in FHIR, the standard vocabulary for conditions in OMOP, and mandated in the NHS. Its role is growing
  • Competing formats: ICD for billing — but they are complements, not rivals. SNOMED records, ICD bills

What sets it apart is that it is a description logic ontology, not a code list. Concepts are defined by their relationships to other concepts, so subsumption is computable rather than a matter of prefix matching:

snomed-concept.txt
Concept: 44054006 |Diabetes mellitus type 2 (disorder)|

  IS-A  73211009  |Diabetes mellitus (disorder)|
  IS-A  126877002 |Disorder of glucose metabolism (disorder)|

  Finding site       113331007 |Endocrine system structure|
  Associated morphology  ...

Post-coordination - build a concept that has no pre-existing code:
  44054006 |Diabetes mellitus type 2| :
    246112005 |Severity| = 24484000 |Severe|

Post-coordination is enormously powerful and, in practice, a serious burden: most systems cannot store or query post-coordinated expressions, so almost everyone sticks to pre-coordinated concepts.

JavaRPythonJuliaRust
LanguagePackageNotes
JavaSnowstormSNOMED International's own terminology server, with a FHIR terminology API. The reference deployment
Javasnomed-owl-toolkitClassify the ontology yourself, if you need to reason over it
RRdiagnosislistLoad a SNOMED CT release and work with concept lists and hierarchies. The only real CRAN option
RCodelistGeneratorIf your data is in OMOP, generate SNOMED concept sets from the vocabulary tables instead
PythonMedCATNamed entity recognition and linking to SNOMED/UMLS from clinical text. The strongest option for notes
PythonPyMedTerminoLocal access to SNOMED CT, ICD-10, and others through a common API

LOINC

Logical Observation Identifiers Names and Codes — the universal identifier for lab tests and clinical observations.

  • Introduced: 1994, by the Regenstrief Institute
  • Purpose: Give every lab test a universal code, so results move between institutions without a bilateral mapping for each local test menu
  • Status today: Universal for lab data exchange, required by US certification, and increasingly used for clinical documents and survey instruments too
  • Competing formats: None. LOINC won this outright
  • License: Free, with registration and acceptance of the license terms

A LOINC code is defined by six axes, which is why apparently identical tests get different codes — the distinctions are real:

loinc-axes.txt
2345-7  Glucose [Mass/volume] in Serum or Plasma

  Component     Glucose          (what is measured)
  Property      MCnc             (mass concentration)
  Time          Pt               (point in time, not timed collection)
  System        Ser/Plas         (specimen)
  Scale         Qn               (quantitative)
  Method        (blank)          (any method)

Compare:
  2339-0   Glucose [Mass/volume] in Blood          <- different specimen
  14749-6  Glucose [Moles/volume] in Serum or Plasma  <- different units (SI)
  1558-6   Fasting glucose [Mass/volume] in Serum or Plasma  <- different challenge

2345-7 and 14749-6 are the same analyte in different units — mg/dL versus mmol/L. Pooling them without unit conversion produces a bimodal distribution that looks like real biology and is not. Always check unit_concept_id (OMOP) or valueQuantity.code (FHIR) before aggregating lab values.

CLIRPythonJulia

There is no maintained LOINC-specific package in R or Python. In practice you load the release CSV into a table and join, or read LOINC through the OMOP vocabulary or the UMLS. Regenstrief's own tools are RELMA (a Windows mapping application) and a public FHIR terminology server.

RxNorm

The NLM's normalized naming system for clinical drugs, and the hub that connects every other drug vocabulary.

  • Introduced: Early 2000s, by the US National Library of Medicine
  • Purpose: Give a single identifier (RXCUI) to a clinical drug concept, and link it to the proprietary vocabularies used by pharmacy systems
  • Release cadence: Full monthly, with weekly updates
  • Status today: The US standard for medication exchange. Required by USCDI
  • License: Free. The full release needs a UMLS account; the RxNav APIs do not

The essential idea is the term type (TTY) ladder, from abstract ingredient to a specific packaged product:

rxnorm-tty.txt
IN     Ingredient                    metformin
PIN    Precise ingredient            metformin hydrochloride
SCDC   Clinical drug component       metformin hydrochloride 500 MG
SCDF   Clinical dose form            metformin hydrochloride Oral Tablet
SCD    Semantic clinical drug        metformin hydrochloride 500 MG Oral Tablet   <- RXCUI 861007
SBD    Semantic branded drug         metformin hydrochloride 500 MG Oral Tablet [Glucophage]

Pick your level deliberately. Counting exposures at SCD treats brand and generic as one drug; counting at IN collapses every dose and form. Neither is right in general, and the choice belongs in your methods section.

APIRPythonJulia
AccessHowNotes
APIRxNav / RxNorm APIFree, no key, well documented. Includes the RxClass and interaction APIs. This is the way in for most people
Rhttr2 against RxNavNo maintained CRAN package. rxnorm does not exist on CRAN despite what search results suggest
Pythonrequests against RxNavSame — no established package worth depending on
OMOPconcept / concept_relationshipIf your data is in OMOP, RxNorm is already loaded and joinable. Much the easiest route

NDC and ATC

Two drug vocabularies that sit either side of RxNorm.

NDC (National Drug Code) is the FDA's package-level identifier — manufacturer, product, package size. It is what appears on pharmacy claims, and it is not stable: codes are reused and retired, and the same product has different NDCs from different repackagers. Always map NDC to RxNorm before analysis rather than counting NDCs directly.

ATC (Anatomical Therapeutic Chemical) is the WHO's drug classification — a five-level hierarchy from anatomical group down to chemical substance. It is the standard for drug utilization research and the usual choice for grouping drugs into classes internationally.

atc-hierarchy.txt
A         Alimentary tract and metabolism
  A10     Drugs used in diabetes
    A10B  Blood glucose lowering drugs, excl. insulins
      A10BA    Biguanides
        A10BA02  metformin

ATC is free to browse but its bulk use is restricted by WHO licensing. In OMOP it is loaded as a classification vocabulary, which is the most practical way to use it.

CPT and HCPCS

The US procedure coding systems, and the place where licensing bites hardest.

  • CPT (Current Procedural Terminology) — maintained by the American Medical Association since 1966. Outpatient and physician procedures. Proprietary and licensed. You need a paid license to redistribute the code descriptions, which is why so many open tools show CPT numbers with no text beside them
  • HCPCS Level II — maintained by CMS for supplies, durable medical equipment, and physician-administered drugs. Free and public

HCPCS Level I is CPT, which is the source of endless confusion. When someone says "HCPCS codes" they usually mean Level II.

Do not embed CPT descriptions in a public repository, a container image, or a published dataset without a license. This is actively enforced. Analyses that only need to group procedures can often use ICD-10-PCS or SNOMED CT instead and avoid the problem entirely.

MedDRA

The Medical Dictionary for Regulatory Activities — the required terminology for adverse events.

  • Introduced: 1990s, developed under ICH; managed by the MedDRA MSSO
  • Purpose: Code adverse events and medical history for regulatory submission and pharmacovigilance
  • Status today: Mandatory for drug safety reporting to the FDA, EMA, and PMDA
  • License: Subscription, priced by organization revenue. Free for some non-profit and regulatory uses — check the current terms

Five levels, and knowing which one an analysis uses is essential to reading it:

meddra-hierarchy.txt
SOC   System Organ Class      Nervous system disorders
 HLGT High Level Group Term   Headaches
  HLT High Level Term         Headaches NEC
   PT Preferred Term          Headache            <- analysis is almost always at PT
    LLT Lowest Level Term     Head pain / Cephalgia / Headache

Verbatim investigator text maps to an LLT; the LLT rolls up to a PT. Safety tables are conventionally reported at PT and grouped by SOC.

UMLS

The Unified Medical Language System — not a terminology but a map between terminologies, and the tool that solves the "these two datasets use different code systems" problem.

  • Introduced: 1986, by the US National Library of Medicine
  • Purpose: Link concepts across more than 200 source vocabularies under a single Concept Unique Identifier (CUI)
  • Release cadence: Twice yearly
  • License: Free, but requires a UMLS Terminology Services account, and downstream use inherits each source vocabulary's own restrictions
umls-cui.txt
CUI: C0011860  |Diabetes Mellitus, Non-Insulin-Dependent|

  SNOMEDCT_US  44054006   Diabetes mellitus type 2
  ICD10CM      E11        Type 2 diabetes mellitus
  ICD9CM       250.00     Diabetes mellitus without mention of complication...
  MSH          D003924    Diabetes Mellitus, Type 2
  MEDDRA       10067585   Type 2 diabetes mellitus

That single CUI is the crosswalk. It is also the backbone of essentially every clinical NLP system.

The UMLS asserts concept identity across vocabularies, and that assertion is sometimes generous. Two codes sharing a CUI are usually equivalent and occasionally merely related. For anything consequential, spot-check the mappings in your own domain rather than trusting them wholesale.

PythonCLIRJuliaRust

Python owns clinical NLP. If your work involves extracting concepts from notes, that decision is effectively made for you.

LanguagePackageNotes
PythonMedCATConcept extraction and linking with self-supervised training. The strongest general option
PythonscispacyspaCy models for biomedical text, with UMLS entity linking
PythonQuickUMLSFast approximate dictionary matching against the Metathesaurus
Pythonmedspacy, negspacyClinical NLP pipelines; negation detection. Negation matters enormously — "no evidence of pneumonia" must not become a pneumonia diagnosis
PythonPyMedTerminoUnified local API over several terminologies
CLIMetamorphoSysThe official UMLS subsetting tool. Ships with the release. You will need it — the full Metathesaurus is far larger than any single project wants
APIUMLS REST APISearch and crosswalk without a local install

HPO and friends

For rare disease and genomics work, a different set of ontologies applies:

  • HPO (Human Phenotype Ontology, 2008) — standardized phenotypic abnormalities. The standard for describing patient phenotypes in genomic diagnosis
  • MONDO — a merged disease ontology that unifies OMIM, Orphanet, DOID, and others
  • OMIM — Online Mendelian Inheritance in Man. Authoritative for gene-disease relationships; free for academic use with registration
  • Phenopackets — the GA4GH standard for exchanging phenotype and genotype data about a subject. Increasingly the interchange format between clinical and genomic systems
PythonRJulia
LanguagePackageNotes
PythonprontoRead and manipulate OBO and OWL ontologies. The general-purpose choice for HPO and MONDO
Pythonowlready2OWL ontologies with a reasoner attached, when you need inference
PythonphenopacketsThe reference GA4GH Phenopacket implementation
RontologyIndex, ontologySimilarityRead OBO ontologies and compute semantic similarity between phenotype sets

Value sets

A value set is a named, versioned list of codes meaning "these codes count as X for this purpose." It is the unit that should be shared and cited, because a code list buried in a script is the least reproducible artifact in health data research.

  • VSAC (Value Set Authority Center) — NLM's repository, authoritative for US quality measures. Free with a UMLS account
  • OHDSI concept sets — the OMOP equivalent, defined in ATLAS and shareable as JSON
  • FHIR ValueSet — the standard resource for expressing one, and what a terminology server expands

Publish your value sets alongside your papers. Reviewers cannot evaluate a phenotype definition they cannot see, and the code list is very often where the result actually came from.

Code system identifiers

The same terminology is named differently depending on the standard carrying it. This table resolves a specific, recurring annoyance when moving between C-CDA and FHIR:

TerminologyOID (used in HL7 v2 / CDA)URI (used in FHIR)
LOINC2.16.840.1.113883.6.1http://loinc.org
SNOMED CT2.16.840.1.113883.6.96http://snomed.info/sct
ICD-10-CM2.16.840.1.113883.6.90http://hl7.org/fhir/sid/icd-10-cm
ICD-9-CM (dx)2.16.840.1.113883.6.103http://hl7.org/fhir/sid/icd-9-cm
RxNorm2.16.840.1.113883.6.88http://www.nlm.nih.gov/research/umls/rxnorm
CPT2.16.840.1.113883.6.12http://www.ama-assn.org/go/cpt
NDC2.16.840.1.113883.6.69http://hl7.org/fhir/sid/ndc
CVX2.16.840.1.113883.12.292http://hl7.org/fhir/sid/cvx
UCUM (units)2.16.840.1.113883.6.8http://unitsofmeasure.org

CVX

CDC's vaccine administered codes. Small, free, and unambiguous — a rare pleasure in this domain. Paired with MVX (manufacturer) and, for billing, CPT. Required by USCDI for immunization data.