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
| Domain | Standard | Who maintains it | Free? |
|---|---|---|---|
| Diagnoses (billing) | ICD-10-CM | CDC/NCHS (US), WHO | Yes |
| Diagnoses (clinical) | SNOMED CT | SNOMED International | Member countries only |
| Procedures (inpatient) | ICD-10-PCS | CMS | Yes |
| Procedures (outpatient) | CPT | AMA | No — licensed |
| Supplies, drugs administered | HCPCS Level II | CMS | Yes |
| Labs and observations | LOINC | Regenstrief Institute | Yes, with registration |
| Drugs (clinical) | RxNorm | NLM | Yes |
| Drugs (package) | NDC | FDA | Yes |
| Drugs (classification) | ATC | WHO | Restricted for bulk use |
| Vaccines | CVX | CDC | Yes |
| Adverse events | MedDRA | ICH | No — subscription |
| Phenotypes | HPO | Monarch Initiative | Yes |
| Everything, cross-mapped | UMLS | NLM | Yes, 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.9is a child ofE11, which sits underE00-E89
The variants matter and are routinely confused:
| Variant | Scope | Size | Used for |
|---|---|---|---|
| ICD-10 | WHO international | ~14,000 codes | International mortality/morbidity statistics |
| ICD-10-CM | US clinical modification | ~70,000 codes | US diagnosis coding since 1 Oct 2015 |
| ICD-10-PCS | US procedure coding | ~80,000 codes | US inpatient procedures only |
| ICD-9-CM | Retired US predecessor | ~17,000 codes | Data before 1 Oct 2015 |
| ICD-11 | WHO, current | ~17,000 entities | Adoption is gradual and patchy |
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 complicationsThe 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.
| Language | Package | Notes |
|---|---|---|
| R | icd | Comorbidity mapping, validation, explanation, and ICD-9/ICD-10 handling. The most complete single package in any language |
| R | comorbidity | Charlson and Elixhauser scores from ICD-9/10, with the published weightings |
| R | touch | ICD-9/ICD-10 crosswalking via the CMS GEMs |
| R | icdpicr | Injury severity scoring from ICD codes |
| R | medicalrisk, ICD10gm | Risk indices; the German ICD-10-GM modification |
| Python | simple-icd-10 | Hierarchy navigation and code validation for WHO ICD-10 |
| Python | comorbidipy | Charlson and Elixhauser, the Python counterpart to comorbidity |
| Python | icd10-cm | US clinical modification lookups. Small and simple |
| API | WHO ICD API | Authoritative 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:
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| Language | Package | Notes |
|---|---|---|
| Java | Snowstorm | SNOMED International's own terminology server, with a FHIR terminology API. The reference deployment |
| Java | snomed-owl-toolkit | Classify the ontology yourself, if you need to reason over it |
| R | Rdiagnosislist | Load a SNOMED CT release and work with concept lists and hierarchies. The only real CRAN option |
| R | CodelistGenerator | If your data is in OMOP, generate SNOMED concept sets from the vocabulary tables instead |
| Python | MedCAT | Named entity recognition and linking to SNOMED/UMLS from clinical text. The strongest option for notes |
| Python | PyMedTermino | Local 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:
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 challenge2345-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.
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:
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.
| Access | How | Notes |
|---|---|---|
| API | RxNav / RxNorm API | Free, no key, well documented. Includes the RxClass and interaction APIs. This is the way in for most people |
| R | httr2 against RxNav | No maintained CRAN package. rxnorm does not exist on CRAN despite what search results suggest |
| Python | requests against RxNav | Same — no established package worth depending on |
| OMOP | concept / concept_relationship | If 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.
A Alimentary tract and metabolism
A10 Drugs used in diabetes
A10B Blood glucose lowering drugs, excl. insulins
A10BA Biguanides
A10BA02 metforminATC 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:
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 / HeadacheVerbatim 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
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 mellitusThat 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.
Python owns clinical NLP. If your work involves extracting concepts from notes, that decision is effectively made for you.
| Language | Package | Notes |
|---|---|---|
| Python | MedCAT | Concept extraction and linking with self-supervised training. The strongest general option |
| Python | scispacy | spaCy models for biomedical text, with UMLS entity linking |
| Python | QuickUMLS | Fast approximate dictionary matching against the Metathesaurus |
| Python | medspacy, negspacy | Clinical NLP pipelines; negation detection. Negation matters enormously — "no evidence of pneumonia" must not become a pneumonia diagnosis |
| Python | PyMedTermino | Unified local API over several terminologies |
| CLI | MetamorphoSys | The official UMLS subsetting tool. Ships with the release. You will need it — the full Metathesaurus is far larger than any single project wants |
| API | UMLS REST API | Search 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
| Language | Package | Notes |
|---|---|---|
| Python | pronto | Read and manipulate OBO and OWL ontologies. The general-purpose choice for HPO and MONDO |
| Python | owlready2 | OWL ontologies with a reasoner attached, when you need inference |
| Python | phenopackets | The reference GA4GH Phenopacket implementation |
| R | ontologyIndex, ontologySimilarity | Read 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:
| Terminology | OID (used in HL7 v2 / CDA) | URI (used in FHIR) |
|---|---|---|
| LOINC | 2.16.840.1.113883.6.1 | http://loinc.org |
| SNOMED CT | 2.16.840.1.113883.6.96 | http://snomed.info/sct |
| ICD-10-CM | 2.16.840.1.113883.6.90 | http://hl7.org/fhir/sid/icd-10-cm |
| ICD-9-CM (dx) | 2.16.840.1.113883.6.103 | http://hl7.org/fhir/sid/icd-9-cm |
| RxNorm | 2.16.840.1.113883.6.88 | http://www.nlm.nih.gov/research/umls/rxnorm |
| CPT | 2.16.840.1.113883.6.12 | http://www.ama-assn.org/go/cpt |
| NDC | 2.16.840.1.113883.6.69 | http://hl7.org/fhir/sid/ndc |
| CVX | 2.16.840.1.113883.12.292 | http://hl7.org/fhir/sid/cvx |
| UCUM (units) | 2.16.840.1.113883.6.8 | http://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.