Skip to content

Medical AI · Clinical NLP · Lesson 2 of 4

Named-entity recognition and clinical coding

Reviewed by Dr. Ram Kumar G, MBBS, MD (Internal Medicine), MPH (USA), on · Last updated

  • Intermediate
  • Technical focus
  • About 30 minutes
  • Includes exercise

Before this lesson: Working with clinical notes, Data standards: FHIR, HL7, ICD, SNOMED CT, LOINC and OMOP

In short

Named-entity recognition (NER) finds mentions of things such as conditions, medicines and procedures in text; normalisation maps each mention to a standard code, such as ICD-10 or SNOMED CT. Dictionary methods are transparent and fast but miss synonyms and spelling variants; machine-learning and transformer models find more but need labelled data and validation. Any extractor must handle negation and be scored against human annotations with precision and recall. Studies have found that large language models are unreliable at assigning medical codes directly.

You will learn to

  • Explain entity recognition and normalisation to clinical codes
  • Build a negation-aware dictionary extractor that assigns ICD-10 categories
  • Score an extractor with precision and recall against labelled notes
  • Compare dictionary, machine-learning, transformer and LLM approaches

From mentions to codes

Established clinical NLP systems such as cTAKES and MetaMap do this with large terminologies; newer approaches use transformer models trained on clinical text, such as ClinicalBERT, or large language models.

StepExample
Find the mention (NER)'T2DM' in 'with T2DM and HTN'
Decide its statusPresent, negated, uncertain, historical, family member
Normalise to a conceptType 2 diabetes mellitus
Assign a codeICD-10 E11; SNOMED CT 44054006

A dictionary extractor with negation

The extractor below maps a few terms to illustrative WHO ICD-10 categories and is tested on four synthetic notes coded by a fictional clinical coder.

python
import re

# Illustrative WHO ICD-10 categories; always confirm codes in the official classification
LEXICON = {
    "E11": ["type 2 diabetes", "t2dm", "type ii diabetes"],
    "I10": ["hypertension", "htn", "high blood pressure"],
    "N18": ["chronic kidney disease", "ckd"],
    "L97": ["foot ulcer", "ulcer of the foot", "ulcers"],
    "E78": ["hyperlipidaemia", "dyslipidaemia", "high cholesterol"],
}
NEGATION = r"\b(no|denies|without|negative for|absent)\b"

def extract_codes(note, use_negation=True):
    codes = set()
    for sentence in re.split(r"(?<=[.!?])\s+", note.lower()):
        for code, terms in LEXICON.items():
            for term in terms:
                m = re.search(rf"\b{re.escape(term)}\b", sentence)
                if m and not (use_negation and re.search(NEGATION, sentence[:m.start()])):
                    codes.add(code)
    return codes

NOTES = [   # synthetic notes with the codes a (fictional) clinical coder assigned
    ("58M with T2DM and HTN. No foot ulcers. Neuropathy on monofilament testing.", {"E11", "I10"}),
    ("Type 2 diabetes, CKD stage 3. Denies chest pain. High cholesterol on statin.", {"E11", "N18", "E78"}),
    ("Known DM2 for 6 years. Left heel ulcer, 2 cm, no signs of infection.", {"E11", "L97"}),
    ("Hypertension well controlled. Negative for diabetes on OGTT.", {"I10"}),
]

def score(use_negation):
    tp = fp = fn = 0
    for text, gold in NOTES:
        pred = extract_codes(text, use_negation)
        tp, fp, fn = tp + len(pred & gold), fp + len(pred - gold), fn + len(gold - pred)
    return tp / (tp + fp), tp / (tp + fn)

for label, neg in [("Without negation handling", False), ("With negation handling", True)]:
    precision, recall = score(neg)
    print(f"{label:<27} precision {precision:.2f}  recall {recall:.2f}")

# Without negation handling   precision 0.86  recall 0.75
# With negation handling      precision 1.00  recall 0.75
Dictionary-based extraction to ICD-10 categories, scored with and without negation handling.
  • Without negation handling, 'No foot ulcers' is coded as a foot ulcer, a false positive. With it, precision rises to 1.00.

Where dictionaries fail

python
for text, gold in NOTES:
    pred = extract_codes(text)
    if pred != gold:
        print(f"{text}\n  missed {sorted(gold - pred)}  extra {sorted(pred - gold)}")

# Known DM2 for 6 years. Left heel ulcer, 2 cm, no signs of infection.
#   missed ['E11', 'L97']  extra []
Continuing from the previous block: list the notes where extraction disagrees with the coder.
  • 'DM2' and 'heel ulcer' are not in the dictionary, so both codes are missed. Real notes contain endless variants: abbreviations, misspellings, body-site descriptions.
  • Coding also follows rules beyond mentions: in ICD-10, diabetes with neurological complications has its own category, so 'diabetes' and 'neuropathy' in one note may need a combination code rather than two separate ones.

Choosing an approach

Language models extract information from notes well in some studies (Agrawal et al., 2022), but when asked to generate billing codes directly they often produced wrong or invented codes (Soroush et al., 2024). Combining extraction by a model with lookup in an official terminology is safer than asking it for codes.

ApproachStrengthsWeaknesses
Dictionary and rulesTransparent, fast, no training dataMisses variants; needs maintenance
Classical ML (for example CRF)Learns variants from examplesNeeds annotated data; feature engineering
Clinical transformers (BioBERT, ClinicalBERT)Strong accuracy on benchmark tasksNeeds labelled data and computing resources; can miss rare terms
Large language modelsFlexible, few examples neededCan hallucinate entities or codes; needs validation and approved environments

Synthetic notes only

  • Every note, name and number in this track is invented. Never practise on real clinical notes outside an approved, secure environment, and never paste them into public AI tools.

Exercise

Build, score and improve a clinical entity extractor on synthetic notes.

Data: Thirty synthetic notes with coder-style labels, written for the notebook (Created for this course; no real patient data)

  1. Reproduce the extractor and scores, then run it on the thirty notes and report precision and recall per code.
  2. Add synonyms and abbreviations from your error list and re-score; did precision fall?
  3. Add uncertainty and family-history handling from the previous lesson and measure the effect.
  4. Optional: run a scispaCy or medspaCy model on the same notes and compare.
  5. Optional, with an approved model: ask an LLM to extract conditions (not codes), map them to codes with your lexicon, and compare with direct LLM coding.

Open the notebook in ColabView on GitHubWorked solution coming soon

Key takeaways

  • NER finds mentions; normalisation maps them to standard codes.
  • Handle negation and context before assigning codes.
  • Score extractors with precision and recall against human annotations.
  • Use models to extract and terminologies to code; LLMs are unreliable direct coders.

Terms used: ICD (International Classification of Diseases), Named-entity recognition (NER), Negation detection, SNOMED CT

Related: Working with clinical notes, Data standards: ICD, SNOMED CT, LOINC, Prompting for clinical tasks

FAQ

What is clinical named-entity recognition?

Automatically finding mentions of clinical concepts, such as conditions, medicines, procedures and test results, in free-text notes.

What is normalisation in clinical NLP?

Mapping each mention, whatever its wording, to a standard concept and code, such as SNOMED CT or ICD-10.

Can AI do clinical coding automatically?

It can assist, but fully automatic coding is hard. Coding rules, combination codes and documentation requirements mean human coders remain important, and LLMs have shown high error rates when generating codes directly.

How should an extractor be evaluated?

Against notes annotated by people, with precision (how many extracted items are right) and recall (how many true items were found), reported per entity type.

What tools exist for clinical NER?

Rule-based toolkits such as medspaCy and cTAKES, biomedical models such as scispaCy, BioBERT and ClinicalBERT, and large language models, each needing local validation.

Educational content only. This material teaches methods; it is not clinical advice. Code, models and examples here are not validated medical devices and must not be used to make decisions about real patients without proper clinical validation, governance and any required regulatory approval. Examples use synthetic or openly licensed de-identified data only. Never paste identifiable patient information into public AI tools. Mentions of models, tools or vendors are for identification, not endorsement.