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.
Metadata
Release files for aimedicalcoding 0.3.17
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aimedicalcoding-0.3.17.tar.gz | 116.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aimedicalcoding-0.3.17-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 205.1 kB
Release files / aimedicalcoding-0.3.17.tar.gz
| Download URL | aimedicalcoding-0.3.17.tar.gz |
|---|---|
| Size | 116.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
abdb1d54e7b2adaa2dc83e42dad93d3c97965109a155846368d24173b697840d
|
|
BLAKE2b-256 checksum How to use checksums |
71a76d9d685abe3130095db3235cc6a26bdf85f3f7dcaf1c9a60a9ca46391b0b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|
Release files / aimedicalcoding-0.3.17-py3-none-any.whl
| Download URL | aimedicalcoding-0.3.17-py3-none-any.whl |
|---|---|
| Size | 88.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5ae4a121807b30e9ac853fa597ebf01df90a7e440211711f65e974f21165ebf7
|
|
BLAKE2b-256 checksum How to use checksums |
e3dea93b96a8b211c90b1d074f78bfabf68918b04d5f17f2f9ad5efd3d2a93b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|