Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
Project description
aimedicalcoding
Find every missing or broken standard clinical code in a CCDA or FHIR document — with the evidence needed to fix it.
aimedicalcoding parses a clinical document (CCDA XML or FHIR R4 Bundle JSON),
inspects every coded clinical item — labs, vitals, problems, medications,
immunizations, allergies, procedures, social history — and reports the ones
whose standard terminology code (LOINC · SNOMED CT · RxNorm · ICD-10-CM ·
CVX) is missing, local-only, crosswalkable, or malformed. Each finding (a
gap) is emitted as a self-contained JSON record carrying the classification,
the document facts, and a ready-made recovery plan (search terms, axis filters,
crosswalk candidates, check-digit corrections).
Items that are already correctly coded produce no output — the result is a work list, not an inventory.
Installation
pip install aimedicalcoding
Requires Python 3.10+. The only runtime dependency is lxml.
Quick start (CLI)
The input format is auto-detected — the same command works for CCDA XML and FHIR JSON, and the output schema is identical for both.
# human-readable gap report (card view)
aimedicalcoding patient.xml
# FHIR R4 Bundle — same flags, same output
aimedicalcoding bundle.json --table
# counts only
aimedicalcoding patient.xml --summary --table
# machine-readable JSON, written to a file as UTF-8
aimedicalcoding patient.xml --out gaps.json
# restrict to one terminology and/or certain contexts
aimedicalcoding patient.xml --system loinc --contexts result,vital
If the entry point is not on your PATH, python -m aimedicalcoding <file> [flags]
is equivalent.
CLI flags
| Flag | Meaning |
|---|---|
--system |
loinc | snomed | rxnorm | icd10cm | cvx | all (default all) |
--contexts |
comma-separated contexts to include, e.g. result,vital,problem |
--table |
compact one-line-per-gap table |
--json |
print the JSON report to stdout |
-o, --out FILE |
write the JSON report to FILE as UTF-8 (implies --json) |
--summary |
counts only |
--include-narrative |
also report narrative-table-only items (CCDA; off by default) |
--apply-dict |
expand lab abbreviations (BUN → urea nitrogen) in search terms |
--json-full |
dump the complete internal representation (debugging) |
Quick start (Python)
from aimedicalcoding import extract_gaps
result = extract_gaps("patient.xml") # CCDA — auto-detected
result = extract_gaps("bundle.json") # FHIR R4 Bundle — auto-detected
result = extract_gaps(raw_bytes, # bytes / str / path / file-like
systems=("loinc", "snomed", "rxnorm", "icd10cm", "cvx"))
len(result) # number of gaps
result.by_target # {"LOINC": 17, "SNOMED-CT": 30, "RxNorm": 4, ...}
result.by_pattern # {"LOCAL_CODE_ONLY": 15, "NULLFLAVOR": 32, ...}
result.gaps # list[dict] — one record per (item, missing terminology)
result.to_json() # the same JSON the CLI emits
Lower-level access — parse to the intermediate representation without gap analysis:
from aimedicalcoding.pipeline import parse_document
statements = parse_document(open("bundle.json", "rb").read())
for s in statements:
print(s.context, s.raw_text, [(c.system, c.code) for c in s.codings])
How it works
The library is built on two independent axes that meet at a format-agnostic intermediate representation (IR):
FORMAT axis (adapters) TERMINOLOGY axis (extractors)
CCDA XML ──┐ LOINC SNOMED CT
FHIR R4 ───┤──► ClinicalStatement ──► RxNorm ICD-10-CM CVX
│ (the IR)
(auto-detected) (identical for every input format)
- Detect — XML with a
<ClinicalDocument>root → CCDA adapter; JSON with"resourceType": "Bundle"→ FHIR adapter. - Parse to IR — the adapter walks the document and emits one
ClinicalStatementper clinical item: every coding found on the item (root code plus all translations /coding[]entries, each canonicalized by system), the human labels, the value (quantity or coded answer), specimen, panel membership, timestamps. - Extract gaps — each terminology extractor examines the statements relevant to it (labs/vitals for LOINC, problems/procedures/reactions for SNOMED, medications for RxNorm, diagnoses for ICD-10-CM, immunizations for CVX), validates any code present (check digits, format), and classifies everything else into a gap pattern.
- Serialize — one JSON record per (item × missing terminology), identical schema for CCDA and FHIR input.
What each adapter understands
CCDA: document/section/entry structure, <code> + <translation>
scan-and-identify, nullFlavor, <originalText> reference resolution into the
narrative, organizer → panel linkage, specimen, interpretation and reference
ranges, narrative-table fallback for sections without structured entries.
FHIR R4 Bundle: routing by resource type + Observation.category (with
section-tag fallback), CodeableConcept.coding[] as primary + translations,
valueQuantity / valueCodeableConcept / valueString / dataAbsentReason,
hasMember / DiagnosticReport.result panel linkage (children inherit context
and specimen), medicationReference → Medication.code and
specimen → Specimen.type resolution across the Bundle, per-reaction allergy
statements, and normalization of "unknown" null-marker strings.
Gap patterns
Each record's pattern says why the code is missing — which determines how
to fix it:
| Pattern | What's wrong | Recovery route |
|---|---|---|
LOCAL_CODE_ONLY |
a local/in-house/CPT code, no standard code | crosswalk local → target, else text search |
NULLFLAVOR |
code explicitly declared unknown (nullFlavor, "unknown", dataAbsentReason) |
resolve the recovered label |
DISPLAY_ONLY |
only a display name, no code and no absence marker | resolve the label |
NARRATIVE_ONLY |
exists only as narrative table text (CCDA, opt-in) | text search, low priority |
BAD_CODE |
right shape, fails validation (LOINC Luhn · SNOMED Verhoeff · ICD format) | check-digit / format correction (suggested fix included) |
ICD_ONLY |
ICD present, SNOMED missing | official ICD ↔ SNOMED map |
SNOMED_ONLY |
SNOMED present, target missing (ICD-10-CM; or LOINC on a lab/functional item) | SNOMED → ICD-10-CM / SNOMED → LOINC map |
CPT_ONLY |
CPT present, no LOINC | CPT → LOINC crosswalk |
NDC_ONLY |
NDC present, RxNorm/CVX missing | NDC ↔ RxNorm / CDC NDC ↔ CVX table |
RXNORM_ONLY |
RxNorm present, target missing (vaccine → CVX; drug allergen → SNOMED) | CVX ↔ RxNorm map / RxNorm ↔ SNOMED via UMLS |
MULTUM_ONLY / FDB_ONLY / MEDISPAN_ONLY |
proprietary drug vocabulary, RxNorm missing | vendor ↔ RxNorm via UMLS |
Output
The JSON envelope:
{
"source": "patient.xml",
"source_format": "ccda", // or "fhir" — auto-detected
"targets_checked": ["LOINC", "SNOMED-CT", "RxNorm", "ICD-10-CM", "CVX"],
"gap_count": 4,
"by_pattern": { "LOCAL_CODE_ONLY": 1, "NULLFLAVOR": 3 },
"by_target": { "LOINC": 1, "SNOMED-CT": 3 },
"gaps": [ ... ]
}
Each gap record separates four concerns — the classification, the facts, the plan, and the (pending) answer:
{
"id": 1,
"target": { "system": "LOINC", "slot": "code" }, // what's missing, and in which slot
"pattern": "LOCAL_CODE_ONLY", // why
"section": { "code": "30954-2", "name": "Results", "system": "LOINC" },
"observation": { // the facts, verbatim
"code": {
"display_text": "LIPID PANEL",
"original_text": "LIPID PANEL",
"codings": [
{ "code": null, "system": "LOINC", "system_raw": "http://loinc.org" },
{ "code": "80061", "system": "CPT", "system_raw": "http://www.ama-assn.org/go/cpt" }
]
},
"value": null,
"qualifiers": { "specimen": { "value": "Bld", "confidence": "low" }, "...": "..." }
},
"recovery": { // the plan
"search_terms": ["LIPID PANEL", "lipid panel"],
"match_key": "component:LIPID PANEL | specimen:Bld | property:? | scale:? | method:-",
"crosswalk": { "from_system": "http://www.ama-assn.org/go/cpt", "from_code": "80061" },
"suggested_action": "Try (source_system, local_code) crosswalk FIRST; ..."
},
"provenance": { "location": { "type": "fhirpath", "path": "..." } },
"resolution": { "status": "pending", "code": null, "...": "..." } // filled by your recovery step
}
Reading it: this lab was coded with CPT 80061 but its LOINC slot is empty
(code: null — the source wrote a null marker). The record hands you the CPT
crosswalk candidate, the text to search with, and the LOINC axis hints
(specimen/property/scale) to constrain candidates — everything a
crosswalk-database or LLM step needs to fill resolution.
Scope and status
- Emits gap detections + recovery evidence. It does not itself query
terminology databases or LLMs —
resolutionis intentionally leftpendingfor your downstream step. - Terminologies checked today: LOINC, SNOMED CT, RxNorm, ICD-10-CM, CVX. CPT / HCPCS / ICD-10-PCS are planned.
- Validation included: LOINC Luhn check digits, SNOMED Verhoeff check digits (incl. long-format extension SCTIDs), ICD-10-CM format rules, CVX shape.
- Code-system identification covers HL7 OIDs, FHIR URIs,
urn:oid:forms and free-text names for 20+ vocabularies (Epic/local OIDs are recognized as local codes, HL7 structural vocabularies are excluded from gap detection).
License
MIT — © 2026 Trove Health.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aimedicalcoding-0.3.12.tar.gz.
File metadata
- Download URL: aimedicalcoding-0.3.12.tar.gz
- Upload date:
- Size: 111.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85a9f6938453210c6139987b2ecf4d8b134ca7b9da235e9bf8a6691a9c4f11c8
|
|
| MD5 |
d1fddb39667c14970987ee1d175ef3d5
|
|
| BLAKE2b-256 |
203984206a282972ea4201f222722fdcd400befee20f9cd952d5589d84f617c8
|
File details
Details for the file aimedicalcoding-0.3.12-py3-none-any.whl.
File metadata
- Download URL: aimedicalcoding-0.3.12-py3-none-any.whl
- Upload date:
- Size: 83.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
42da0ff9b9da0589793758bc5f3905b53bed4b712fe4e29c8f7ff8932c3e6172
|
|
| MD5 |
acca47ae583752e4ba65b1217857163b
|
|
| BLAKE2b-256 |
7a7d31b15ab08c5f0496a4f2f5094b624f992e55c394b4409d4d807af05ef137
|