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.
| Step | Example |
|---|---|
| Find the mention (NER) | 'T2DM' in 'with T2DM and HTN' |
| Decide its status | Present, negated, uncertain, historical, family member |
| Normalise to a concept | Type 2 diabetes mellitus |
| Assign a code | ICD-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.
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- 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
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 []- '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.
| Approach | Strengths | Weaknesses |
|---|---|---|
| Dictionary and rules | Transparent, fast, no training data | Misses variants; needs maintenance |
| Classical ML (for example CRF) | Learns variants from examples | Needs annotated data; feature engineering |
| Clinical transformers (BioBERT, ClinicalBERT) | Strong accuracy on benchmark tasks | Needs labelled data and computing resources; can miss rare terms |
| Large language models | Flexible, few examples needed | Can 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)
- Reproduce the extractor and scores, then run it on the thirty notes and report precision and recall per code.
- Add synonyms and abbreviations from your error list and re-score; did precision fall?
- Add uncertainty and family-history handling from the previous lesson and measure the effect.
- Optional: run a scispaCy or medspaCy model on the same notes and compare.
- 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.
Sources
- Savova GK, et al. Mayo clinical Text Analysis and Knowledge Extraction System (cTAKES). J Am Med Inform Assoc 2010;17(5):507–513. doi:10.1136/jamia.2009.001560
- Alsentzer E, et al. Publicly available clinical BERT embeddings. Clinical NLP Workshop 2019. arXiv:1904.03323
- Lee J, et al. BioBERT: a pre-trained biomedical language representation model for biomedical text mining. Bioinformatics 2020;36(4):1234–1240. doi:10.1093/bioinformatics/btz682
- Agrawal M, et al. Large language models are few-shot clinical information extractors. EMNLP 2022. arXiv:2205.12689
- Soroush A, et al. Large language models are poor medical coders. NEJM AI 2024;1(5). doi:10.1056/AIdbp2300040
- Neumann M, et al. ScispaCy: fast and robust models for biomedical natural language processing. BioNLP 2019. arXiv:1902.07669
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.