aimedicalcoding 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aimedicalcoding-0.3.1/LICENSE +21 -0
- aimedicalcoding-0.3.1/MANIFEST.in +14 -0
- aimedicalcoding-0.3.1/PKG-INFO +276 -0
- aimedicalcoding-0.3.1/README.md +262 -0
- aimedicalcoding-0.3.1/README_PYPI.md +239 -0
- aimedicalcoding-0.3.1/aimedicalcoding/__init__.py +29 -0
- aimedicalcoding-0.3.1/aimedicalcoding/__main__.py +313 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/base.py +25 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/adapter.py +294 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/codes.py +198 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/narrative.py +121 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/ccda/sections.py +44 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/custom_json/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/custom_json/adapter.py +30 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/adapter.py +326 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/codes.py +211 -0
- aimedicalcoding-0.3.1/aimedicalcoding/adapters/fhir/mapping.py +68 -0
- aimedicalcoding-0.3.1/aimedicalcoding/api.py +163 -0
- aimedicalcoding-0.3.1/aimedicalcoding/core/__init__.py +34 -0
- aimedicalcoding-0.3.1/aimedicalcoding/core/checkdigits.py +103 -0
- aimedicalcoding-0.3.1/aimedicalcoding/core/models.py +149 -0
- aimedicalcoding-0.3.1/aimedicalcoding/core/system_registry.py +185 -0
- aimedicalcoding-0.3.1/aimedicalcoding/pipeline/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/pipeline/orchestrator.py +33 -0
- aimedicalcoding-0.3.1/aimedicalcoding/recovery/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/recovery/engine.py +16 -0
- aimedicalcoding-0.3.1/aimedicalcoding/serialize.py +257 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/base.py +43 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/__init__.py +15 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/handler.py +55 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/normalize.py +75 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/cvx/patterns.py +134 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/__init__.py +16 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/handler.py +55 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/icdformat.py +43 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/icd10cm/patterns.py +133 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/__init__.py +14 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/axes.py +199 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/handler.py +80 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/normalize.py +81 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/loinc/patterns.py +166 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/__init__.py +13 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/handler.py +55 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/normalize.py +88 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/rxnorm/patterns.py +143 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/__init__.py +15 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/handler.py +60 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/hierarchy.py +96 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/patterns.py +166 -0
- aimedicalcoding-0.3.1/aimedicalcoding/systems/snomed/qualifiers.py +90 -0
- aimedicalcoding-0.3.1/aimedicalcoding/terminology/__init__.py +3 -0
- aimedicalcoding-0.3.1/aimedicalcoding/terminology/service.py +31 -0
- aimedicalcoding-0.3.1/aimedicalcoding.egg-info/PKG-INFO +276 -0
- aimedicalcoding-0.3.1/aimedicalcoding.egg-info/SOURCES.txt +71 -0
- aimedicalcoding-0.3.1/aimedicalcoding.egg-info/dependency_links.txt +1 -0
- aimedicalcoding-0.3.1/aimedicalcoding.egg-info/entry_points.txt +2 -0
- aimedicalcoding-0.3.1/aimedicalcoding.egg-info/requires.txt +6 -0
- aimedicalcoding-0.3.1/aimedicalcoding.egg-info/top_level.txt +1 -0
- aimedicalcoding-0.3.1/docs/ARCHITECTURE.md +242 -0
- aimedicalcoding-0.3.1/docs/GAP_SCHEMA.md +202 -0
- aimedicalcoding-0.3.1/docs/OUTPUT_GUIDE.md +116 -0
- aimedicalcoding-0.3.1/docs/patterns/CVX_pattern_analysis.md +97 -0
- aimedicalcoding-0.3.1/docs/patterns/ICD10CM_pattern_analysis.md +185 -0
- aimedicalcoding-0.3.1/docs/patterns/LOINC_pattern_analysis.md +501 -0
- aimedicalcoding-0.3.1/docs/patterns/RxNorm_pattern_analysis.md +312 -0
- aimedicalcoding-0.3.1/docs/patterns/SNOMED_pattern_analysis.md +456 -0
- aimedicalcoding-0.3.1/pyproject.toml +9 -0
- aimedicalcoding-0.3.1/setup.cfg +4 -0
- aimedicalcoding-0.3.1/setup.py +43 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trove Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# sdist contents. IMPORTANT: setuptools does NOT honor .gitignore — the PHI
|
|
2
|
+
# directories must be pruned explicitly here.
|
|
3
|
+
include LICENSE
|
|
4
|
+
include README.md
|
|
5
|
+
include README_PYPI.md
|
|
6
|
+
recursive-include docs *.md
|
|
7
|
+
prune docs/input
|
|
8
|
+
prune docs/Input_FHIRs
|
|
9
|
+
prune docs/samples
|
|
10
|
+
recursive-exclude aimedicalcoding/tests *
|
|
11
|
+
exclude gaps*.json
|
|
12
|
+
exclude *.gaps.json
|
|
13
|
+
exclude run.sh
|
|
14
|
+
exclude test.py
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aimedicalcoding
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
|
|
5
|
+
Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: loinc snomed rxnorm icd-10-cm cvx ccda fhir clinical-terminology medical-coding interoperability
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: lxml>=4.9
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Requires-Dist: build; extra == "dev"
|
|
25
|
+
Requires-Dist: twine; extra == "dev"
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: keywords
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: provides-extra
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
37
|
+
|
|
38
|
+
# aimedicalcoding
|
|
39
|
+
|
|
40
|
+
**Find every missing or broken standard clinical code in a CCDA or FHIR document — with the evidence needed to fix it.**
|
|
41
|
+
|
|
42
|
+
`aimedicalcoding` parses a clinical document (CCDA XML or FHIR R4 Bundle JSON),
|
|
43
|
+
inspects every coded clinical item — labs, vitals, problems, medications,
|
|
44
|
+
immunizations, allergies, procedures, social history — and reports the ones
|
|
45
|
+
whose standard terminology code (**LOINC · SNOMED CT · RxNorm · ICD-10-CM ·
|
|
46
|
+
CVX**) is missing, local-only, crosswalkable, or malformed. Each finding (a
|
|
47
|
+
*gap*) is emitted as a self-contained JSON record carrying the classification,
|
|
48
|
+
the document facts, and a ready-made recovery plan (search terms, axis filters,
|
|
49
|
+
crosswalk candidates, check-digit corrections).
|
|
50
|
+
|
|
51
|
+
Items that are already correctly coded produce no output — the result is a
|
|
52
|
+
work list, not an inventory.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install aimedicalcoding
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python 3.10+. The only runtime dependency is `lxml`.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick start (CLI)
|
|
67
|
+
|
|
68
|
+
The input format is **auto-detected** — the same command works for CCDA XML
|
|
69
|
+
and FHIR JSON, and the output schema is identical for both.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# human-readable gap report (card view)
|
|
73
|
+
aimedicalcoding patient.xml
|
|
74
|
+
|
|
75
|
+
# FHIR R4 Bundle — same flags, same output
|
|
76
|
+
aimedicalcoding bundle.json --table
|
|
77
|
+
|
|
78
|
+
# counts only
|
|
79
|
+
aimedicalcoding patient.xml --summary --table
|
|
80
|
+
|
|
81
|
+
# machine-readable JSON, written to a file as UTF-8
|
|
82
|
+
aimedicalcoding patient.xml --out gaps.json
|
|
83
|
+
|
|
84
|
+
# restrict to one terminology and/or certain contexts
|
|
85
|
+
aimedicalcoding patient.xml --system loinc --contexts result,vital
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If the entry point is not on your PATH, `python -m aimedicalcoding <file> [flags]`
|
|
89
|
+
is equivalent.
|
|
90
|
+
|
|
91
|
+
### CLI flags
|
|
92
|
+
|
|
93
|
+
| Flag | Meaning |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `--system` | `loinc` \| `snomed` \| `rxnorm` \| `icd10cm` \| `cvx` \| `all` (default `all`) |
|
|
96
|
+
| `--contexts` | comma-separated contexts to include, e.g. `result,vital,problem` |
|
|
97
|
+
| `--table` | compact one-line-per-gap table |
|
|
98
|
+
| `--json` | print the JSON report to stdout |
|
|
99
|
+
| `-o, --out FILE` | write the JSON report to FILE as UTF-8 (implies `--json`) |
|
|
100
|
+
| `--summary` | counts only |
|
|
101
|
+
| `--include-narrative` | also report narrative-table-only items (CCDA; off by default) |
|
|
102
|
+
| `--apply-dict` | expand lab abbreviations (BUN → urea nitrogen) in search terms |
|
|
103
|
+
| `--json-full` | dump the complete internal representation (debugging) |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Quick start (Python)
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from aimedicalcoding import extract_gaps
|
|
111
|
+
|
|
112
|
+
result = extract_gaps("patient.xml") # CCDA — auto-detected
|
|
113
|
+
result = extract_gaps("bundle.json") # FHIR R4 Bundle — auto-detected
|
|
114
|
+
result = extract_gaps(raw_bytes, # bytes / str / path / file-like
|
|
115
|
+
systems=("loinc", "snomed", "rxnorm", "icd10cm", "cvx"))
|
|
116
|
+
|
|
117
|
+
len(result) # number of gaps
|
|
118
|
+
result.by_target # {"LOINC": 17, "SNOMED-CT": 30, "RxNorm": 4, ...}
|
|
119
|
+
result.by_pattern # {"LOCAL_CODE_ONLY": 15, "NULLFLAVOR": 32, ...}
|
|
120
|
+
result.gaps # list[dict] — one record per (item, missing terminology)
|
|
121
|
+
result.to_json() # the same JSON the CLI emits
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Lower-level access — parse to the intermediate representation without gap
|
|
125
|
+
analysis:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from aimedicalcoding.pipeline import parse_document
|
|
129
|
+
|
|
130
|
+
statements = parse_document(open("bundle.json", "rb").read())
|
|
131
|
+
for s in statements:
|
|
132
|
+
print(s.context, s.raw_text, [(c.system, c.code) for c in s.codings])
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## How it works
|
|
138
|
+
|
|
139
|
+
The library is built on two independent axes that meet at a format-agnostic
|
|
140
|
+
intermediate representation (IR):
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
FORMAT axis (adapters) TERMINOLOGY axis (extractors)
|
|
144
|
+
CCDA XML ──┐ LOINC SNOMED CT
|
|
145
|
+
FHIR R4 ───┤──► ClinicalStatement ──► RxNorm ICD-10-CM CVX
|
|
146
|
+
│ (the IR)
|
|
147
|
+
(auto-detected) (identical for every input format)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
1. **Detect** — XML with a `<ClinicalDocument>` root → CCDA adapter; JSON with
|
|
151
|
+
`"resourceType": "Bundle"` → FHIR adapter.
|
|
152
|
+
2. **Parse to IR** — the adapter walks the document and emits one
|
|
153
|
+
`ClinicalStatement` per clinical item: every coding found on the item (root
|
|
154
|
+
code *plus* all translations / `coding[]` entries, each canonicalized by
|
|
155
|
+
system), the human labels, the value (quantity or coded answer), specimen,
|
|
156
|
+
panel membership, timestamps.
|
|
157
|
+
3. **Extract gaps** — each terminology extractor examines the statements
|
|
158
|
+
relevant to it (labs/vitals for LOINC, problems/procedures/reactions for
|
|
159
|
+
SNOMED, medications for RxNorm, diagnoses for ICD-10-CM, immunizations for
|
|
160
|
+
CVX), validates any code present (check digits, format), and classifies
|
|
161
|
+
everything else into a gap pattern.
|
|
162
|
+
4. **Serialize** — one JSON record per (item × missing terminology), identical
|
|
163
|
+
schema for CCDA and FHIR input.
|
|
164
|
+
|
|
165
|
+
### What each adapter understands
|
|
166
|
+
|
|
167
|
+
**CCDA**: document/section/entry structure, `<code>` + `<translation>`
|
|
168
|
+
scan-and-identify, `nullFlavor`, `<originalText>` reference resolution into the
|
|
169
|
+
narrative, organizer → panel linkage, specimen, interpretation and reference
|
|
170
|
+
ranges, narrative-table fallback for sections without structured entries.
|
|
171
|
+
|
|
172
|
+
**FHIR R4 Bundle**: routing by resource type + `Observation.category` (with
|
|
173
|
+
section-tag fallback), `CodeableConcept.coding[]` as primary + translations,
|
|
174
|
+
`valueQuantity` / `valueCodeableConcept` / `valueString` / `dataAbsentReason`,
|
|
175
|
+
`hasMember` / `DiagnosticReport.result` panel linkage (children inherit context
|
|
176
|
+
and specimen), `medicationReference → Medication.code` and
|
|
177
|
+
`specimen → Specimen.type` resolution across the Bundle, per-reaction allergy
|
|
178
|
+
statements, and normalization of `"unknown"` null-marker strings.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Gap patterns
|
|
183
|
+
|
|
184
|
+
Each record's `pattern` says *why* the code is missing — which determines how
|
|
185
|
+
to fix it:
|
|
186
|
+
|
|
187
|
+
| Pattern | What's wrong | Recovery route |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `LOCAL_CODE_ONLY` | a local/in-house/CPT code, no standard code | crosswalk local → target, else text search |
|
|
190
|
+
| `NULLFLAVOR` | code explicitly declared unknown (`nullFlavor`, `"unknown"`, `dataAbsentReason`) | resolve the recovered label |
|
|
191
|
+
| `DISPLAY_ONLY` | only a display name, no code and no absence marker | resolve the label |
|
|
192
|
+
| `NARRATIVE_ONLY` | exists only as narrative table text (CCDA, opt-in) | text search, low priority |
|
|
193
|
+
| `BAD_CODE` | right shape, fails validation (LOINC Luhn · SNOMED Verhoeff · ICD format) | check-digit / format correction (suggested fix included) |
|
|
194
|
+
| `ICD_ONLY` | ICD present, SNOMED missing | official ICD ↔ SNOMED map |
|
|
195
|
+
| `SNOMED_ONLY` | SNOMED present, ICD-10-CM missing | official SNOMED → ICD-10-CM map |
|
|
196
|
+
| `NDC_ONLY` | NDC present, RxNorm/CVX missing | NDC ↔ RxNorm / CDC NDC ↔ CVX table |
|
|
197
|
+
| `RXNORM_ONLY` | RxNorm vaccine product, CVX missing | CVX ↔ RxNorm map |
|
|
198
|
+
| `MULTUM_ONLY` / `FDB_ONLY` / `MEDISPAN_ONLY` | proprietary drug vocabulary, RxNorm missing | vendor ↔ RxNorm via UMLS |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Output
|
|
203
|
+
|
|
204
|
+
The JSON envelope:
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
{
|
|
208
|
+
"source": "patient.xml",
|
|
209
|
+
"source_format": "ccda", // or "fhir" — auto-detected
|
|
210
|
+
"targets_checked": ["LOINC", "SNOMED-CT", "RxNorm", "ICD-10-CM", "CVX"],
|
|
211
|
+
"gap_count": 4,
|
|
212
|
+
"by_pattern": { "LOCAL_CODE_ONLY": 1, "NULLFLAVOR": 3 },
|
|
213
|
+
"by_target": { "LOINC": 1, "SNOMED-CT": 3 },
|
|
214
|
+
"gaps": [ ... ]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Each gap record separates four concerns — the classification, the facts, the
|
|
219
|
+
plan, and the (pending) answer:
|
|
220
|
+
|
|
221
|
+
```jsonc
|
|
222
|
+
{
|
|
223
|
+
"id": 1,
|
|
224
|
+
"target": { "system": "LOINC", "slot": "code" }, // what's missing, and in which slot
|
|
225
|
+
"pattern": "LOCAL_CODE_ONLY", // why
|
|
226
|
+
"section": { "code": "30954-2", "name": "Results", "system": "LOINC" },
|
|
227
|
+
|
|
228
|
+
"observation": { // the facts, verbatim
|
|
229
|
+
"code": {
|
|
230
|
+
"display_text": "LIPID PANEL",
|
|
231
|
+
"original_text": "LIPID PANEL",
|
|
232
|
+
"codings": [
|
|
233
|
+
{ "code": null, "system": "LOINC", "system_raw": "http://loinc.org" },
|
|
234
|
+
{ "code": "80061", "system": "CPT", "system_raw": "http://www.ama-assn.org/go/cpt" }
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
"value": null,
|
|
238
|
+
"qualifiers": { "specimen": { "value": "Bld", "confidence": "low" }, "...": "..." }
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
"recovery": { // the plan
|
|
242
|
+
"search_terms": ["LIPID PANEL", "lipid panel"],
|
|
243
|
+
"match_key": "component:LIPID PANEL | specimen:Bld | property:? | scale:? | method:-",
|
|
244
|
+
"crosswalk": { "from_system": "http://www.ama-assn.org/go/cpt", "from_code": "80061" },
|
|
245
|
+
"suggested_action": "Try (source_system, local_code) crosswalk FIRST; ..."
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
"provenance": { "location": { "type": "fhirpath", "path": "..." } },
|
|
249
|
+
"resolution": { "status": "pending", "code": null, "...": "..." } // filled by your recovery step
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Reading it: this lab was coded with CPT `80061` but its LOINC slot is empty
|
|
254
|
+
(`code: null` — the source wrote a null marker). The record hands you the CPT
|
|
255
|
+
crosswalk candidate, the text to search with, and the LOINC axis hints
|
|
256
|
+
(specimen/property/scale) to constrain candidates — everything a
|
|
257
|
+
crosswalk-database or LLM step needs to fill `resolution`.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Scope and status
|
|
262
|
+
|
|
263
|
+
- Emits **gap detections + recovery evidence**. It does not itself query
|
|
264
|
+
terminology databases or LLMs — `resolution` is intentionally left `pending`
|
|
265
|
+
for your downstream step.
|
|
266
|
+
- Terminologies checked today: LOINC, SNOMED CT, RxNorm, ICD-10-CM, CVX.
|
|
267
|
+
CPT / HCPCS / ICD-10-PCS are planned.
|
|
268
|
+
- Validation included: LOINC Luhn check digits, SNOMED Verhoeff check digits
|
|
269
|
+
(incl. long-format extension SCTIDs), ICD-10-CM format rules, CVX shape.
|
|
270
|
+
- Code-system identification covers HL7 OIDs, FHIR URIs, `urn:oid:` forms and
|
|
271
|
+
free-text names for 20+ vocabularies (Epic/local OIDs are recognized as
|
|
272
|
+
local codes, HL7 structural vocabularies are excluded from gap detection).
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT — © 2026 Trove Health.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# aimedicalcoding
|
|
2
|
+
|
|
3
|
+
Recover missing clinical codes (**LOINC · SNOMED CT · RxNorm · ICD-10-CM**) from
|
|
4
|
+
CCDA / FHIR / custom documents. One pass detects every observation whose code is
|
|
5
|
+
missing, local, ICD/NDC/SNOMED-only, malformed, or blank, and emits a
|
|
6
|
+
**target-tagged gap record** for the (DB + LLM) recovery layer. DB and LLM layers
|
|
7
|
+
are placeholders for now.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Editable (dev) from the repo:
|
|
12
|
+
```bash
|
|
13
|
+
pip install -e .
|
|
14
|
+
```
|
|
15
|
+
Build and install a wheel:
|
|
16
|
+
```bash
|
|
17
|
+
python -m build && pip install dist/aimedicalcoding-*.whl
|
|
18
|
+
```
|
|
19
|
+
Straight from CodeCommit (no registry):
|
|
20
|
+
```bash
|
|
21
|
+
pip install "git+https://git-codecommit.us-east-1.amazonaws.com/v1/repos/Trident.MedCodeParser@v0.2.0"
|
|
22
|
+
```
|
|
23
|
+
Version is set manually in `setup.py` (`version="0.3.0"`) — bump it there for
|
|
24
|
+
each release, then build.
|
|
25
|
+
|
|
26
|
+
## Use as a library
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from aimedicalcoding import extract_gaps
|
|
30
|
+
|
|
31
|
+
result = extract_gaps("patient.xml") # CCDA — checks LOINC + SNOMED + RxNorm
|
|
32
|
+
result = extract_gaps("bundle.json") # FHIR R4 Bundle — same call, auto-detected
|
|
33
|
+
result = extract_gaps(data_bytes, systems=("rxnorm",)) # path | bytes | file-like; pick systems
|
|
34
|
+
result.by_target # {"LOINC": 5, "SNOMED-CT": 6, "RxNorm": 4}
|
|
35
|
+
result.by_pattern # {"LOCAL_CODE_ONLY": 6, ...}
|
|
36
|
+
result.gaps # list[dict] — target-tagged records (see docs/GAP_SCHEMA.md)
|
|
37
|
+
result.to_json() # str
|
|
38
|
+
result.to_dict() # full envelope
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use as a CLI
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
aimedicalcoding patient.xml --json # installed entry point
|
|
45
|
+
# or, without installing:
|
|
46
|
+
./run.sh patient.xml --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Build / publish (dev workflow)
|
|
50
|
+
|
|
51
|
+
Install dev tools (pytest, build, twine) once:
|
|
52
|
+
```bash
|
|
53
|
+
pip install -e ".[dev]"
|
|
54
|
+
```
|
|
55
|
+
Run tests:
|
|
56
|
+
```bash
|
|
57
|
+
pytest
|
|
58
|
+
```
|
|
59
|
+
Build distributables (version comes from the git tag):
|
|
60
|
+
```bash
|
|
61
|
+
python -m build
|
|
62
|
+
```
|
|
63
|
+
Publish privately to AWS CodeArtifact (the repo is on CodeCommit):
|
|
64
|
+
```bash
|
|
65
|
+
aws codeartifact login --tool twine --domain <domain> --repository <repo>
|
|
66
|
+
twine upload --repository codeartifact dist/*
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Status
|
|
70
|
+
- **CCDA adapter** — implemented (`adapters/ccda/`): document/section/entry codes,
|
|
71
|
+
scan-and-identify over `<code>` + `<translation>`, `<value>`/units/interpretation/
|
|
72
|
+
reference range, originalText `#reference` resolution, narrative-table fallback,
|
|
73
|
+
organizer→panel linkage.
|
|
74
|
+
- **FHIR adapter** — implemented (`adapters/fhir/`): parses an R4 Bundle
|
|
75
|
+
(Trove flavor) into the same IR. Routing by resource type +
|
|
76
|
+
`Observation.category`, falling back to the CCDA section LOINC stamped in
|
|
77
|
+
`meta.tag` (reuses `SECTION_ROUTING`). Handles `coding[]` as root +
|
|
78
|
+
translations, the Trove `"unknown"` null marker (code / display / system /
|
|
79
|
+
text → normalized to null; all-unknown concepts become a NULLFLAVOR signal),
|
|
80
|
+
`dataAbsentReason`, `hasMember`/`DiagnosticReport.result` → panel linkage
|
|
81
|
+
(children inherit context + specimen), `medicationReference` → `Medication.code`
|
|
82
|
+
and `specimen` → `Specimen.type` resolution across the Bundle, per-reaction
|
|
83
|
+
`allergy_reaction` statements, and value-slot placement for `Condition` /
|
|
84
|
+
`FamilyMemberHistory`. `detect()` requires `resourceType == "Bundle"`.
|
|
85
|
+
- **Custom JSON adapter** — placeholder (config-driven mapping TODO).
|
|
86
|
+
- **systems/loinc/** — LOINC gaps-only extractor (implemented): classifies each
|
|
87
|
+
LOINC-expected observation into a pattern and emits records ONLY for codes that
|
|
88
|
+
need correcting/filling, each with a recovery evidence packet + composite match
|
|
89
|
+
key. Patterns: `LOCAL_CODE_ONLY`, `NULLFLAVOR`, `DISPLAY_ONLY`, `NARRATIVE_ONLY`,
|
|
90
|
+
`BAD_CODE`. Valid LOINCs (incl. via `<translation>`) are skipped.
|
|
91
|
+
- **systems/snomed/** — SNOMED CT gaps-only extractor (implemented): reads the
|
|
92
|
+
concept from the value/answer slot (problems, family hx, social hx, reactions)
|
|
93
|
+
or the code slot (procedures, allergen substance), classifies by section →
|
|
94
|
+
target hierarchy (ECL), and emits gaps. Patterns add `ICD_ONLY` (ICD↔SNOMED
|
|
95
|
+
crosswalk). Valid SNOMED + structural `ASSERTION` codes are skipped.
|
|
96
|
+
- **systems/rxnorm/** — RxNorm gaps-only extractor (implemented): medications
|
|
97
|
+
(CCDA `manufacturedMaterial/code`). Disambiguates by ingredient + strength +
|
|
98
|
+
dose form + route → SCD/SBD. Patterns add `NDC_ONLY` (NDC↔RxNorm crosswalk).
|
|
99
|
+
RxCUI has no check digit (validity = DB existence + TTY).
|
|
100
|
+
- **systems/icd10cm/** — ICD-10-CM gaps-only extractor (implemented): billing
|
|
101
|
+
diagnoses, the mirror of SNOMED (same value slot). Reads value + `<translation>`,
|
|
102
|
+
disambiguates by laterality/encounter/complication. Patterns add `SNOMED_ONLY`
|
|
103
|
+
(SNOMED→ICD-10-CM map). Format-validated (regex), no check digit.
|
|
104
|
+
- **systems/cvx/** — CVX gaps-only extractor (implemented): immunizations
|
|
105
|
+
(CCDA `manufacturedMaterial/code` under `11369-6`). Code slot, no check digit
|
|
106
|
+
(short numeric). Patterns add `RXNORM_ONLY` (CVX↔RxNorm map) and `NDC_ONLY`
|
|
107
|
+
(CDC vaccine NDC↔CVX table); recovery key is the normalized vaccine concept.
|
|
108
|
+
- **terminology/ · recovery/ · CPT/ICD-10-PCS systems** — placeholder contracts only.
|
|
109
|
+
|
|
110
|
+
## LOINC gap extraction
|
|
111
|
+
```python
|
|
112
|
+
from aimedicalcoding.systems.loinc import LoincGapExtractor
|
|
113
|
+
recs = LoincGapExtractor().extract_from_document(open("doc.xml", "rb").read())
|
|
114
|
+
for r in recs: # only codes needing work
|
|
115
|
+
print(r.gap.type, r.gap.match_key, r.gap.evidence)
|
|
116
|
+
```
|
|
117
|
+
Note: the adapters (CCDA and FHIR) still extract *everything* (panel/sibling
|
|
118
|
+
context is needed to recover a gap); the LOINC layer is what filters to gaps-only.
|
|
119
|
+
|
|
120
|
+
## Layout
|
|
121
|
+
```
|
|
122
|
+
core/ IR models, system registry (OID/URI/name → System), check digits
|
|
123
|
+
adapters/ FORMAT axis — ccda (done), fhir (done), custom_json (stub)
|
|
124
|
+
systems/ TERMINOLOGY axis — loinc/snomed/rxnorm/icd10cm/cvx extractors
|
|
125
|
+
terminology/ DB service (stub)
|
|
126
|
+
recovery/ LLM engine (stub)
|
|
127
|
+
pipeline/ orchestrator: detect format → run adapter → IR
|
|
128
|
+
tests/ fixtures/sample_ccda.xml + sample_fhir_bundle.json + per-axis tests
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Run on a document (CLI)
|
|
132
|
+
Works identically for CCDA XML and FHIR JSON — the format is auto-detected and
|
|
133
|
+
the output schema is the same. From the repo root; each block is copy-paste ready.
|
|
134
|
+
|
|
135
|
+
Try the bundled sample (all targets, card view):
|
|
136
|
+
```bash
|
|
137
|
+
./run.sh docs/samples/sample_all_systems.xml
|
|
138
|
+
```
|
|
139
|
+
A FHIR R4 Bundle (same flags, same output):
|
|
140
|
+
```bash
|
|
141
|
+
./run.sh /path/to/bundle.json --table
|
|
142
|
+
```
|
|
143
|
+
Compact table:
|
|
144
|
+
```bash
|
|
145
|
+
./run.sh /path/to/patient.xml --table
|
|
146
|
+
```
|
|
147
|
+
Counts only (per pattern + per target):
|
|
148
|
+
```bash
|
|
149
|
+
./run.sh /path/to/patient.xml --summary
|
|
150
|
+
```
|
|
151
|
+
Target-tagged JSON for downstream consumers (written as UTF-8; prefer `--out`
|
|
152
|
+
over shell `>` — PowerShell redirection re-encodes output as UTF-16):
|
|
153
|
+
```bash
|
|
154
|
+
./run.sh /path/to/patient.xml --out gaps.json
|
|
155
|
+
```
|
|
156
|
+
Filter to one terminology and/or certain sections:
|
|
157
|
+
```bash
|
|
158
|
+
./run.sh /path/to/patient.xml --system loinc --contexts result,vital
|
|
159
|
+
```
|
|
160
|
+
Opt-in extras (abbreviations, narrative-only rows, raw IR):
|
|
161
|
+
```bash
|
|
162
|
+
./run.sh /path/to/patient.xml --apply-dict --include-narrative
|
|
163
|
+
./run.sh /path/to/patient.xml --json-full
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Output is **gaps only** — observations whose code needs correcting/filling.
|
|
167
|
+
**One pass checks both LOINC and SNOMED**; each gap is tagged with its `target`
|
|
168
|
+
(`{system, slot}`). `--system` just filters which targets to show.
|
|
169
|
+
|
|
170
|
+
Flags:
|
|
171
|
+
- `--system` — `loinc` | `snomed` | `rxnorm` | `icd10cm` | `all` (default `all`); CPT/ICD-10-PCS planned.
|
|
172
|
+
- `--contexts result,vital` — restrict to certain section contexts (default: all).
|
|
173
|
+
- `--include-narrative` — also report `NARRATIVE_ONLY` (human-readable table text); **off by default**.
|
|
174
|
+
- `--apply-dict` — expand lab abbreviations (BUN→urea nitrogen) in the match key; **off by default**.
|
|
175
|
+
- `--table` / `--json` / `--json-full` / `--summary` — output modes.
|
|
176
|
+
- `-o/--out FILE` — write the JSON report to FILE as UTF-8 (implies `--json`).
|
|
177
|
+
|
|
178
|
+
`run.sh` cds to the repo root, ensures `lxml` is installed, then runs the CLI.
|
|
179
|
+
Equivalent without the wrapper: `python3 -m aimedicalcoding <document> [flags]`.
|
|
180
|
+
|
|
181
|
+
## Coding systems the parser generates gaps for
|
|
182
|
+
One pass over a document classifies every observation and emits a target-tagged
|
|
183
|
+
gap for each terminology whose code is missing, non-standard, or malformed. The
|
|
184
|
+
five terminologies currently generated, with where each lives in a CCDA and in
|
|
185
|
+
a FHIR Bundle, and which gap patterns each can produce:
|
|
186
|
+
|
|
187
|
+
| Target | CCDA source (slot) | FHIR source (slot) | Gap patterns it can emit |
|
|
188
|
+
|---|---|---|---|
|
|
189
|
+
| **LOINC** | results / vitals / surveys — observation `code` (question) | `Observation.code` (category `laboratory`/`vital-signs`/`social-history`/`survey`) | `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
|
|
190
|
+
| **SNOMED CT** | problems / family hx / social hx / reactions — `value`; procedures / allergen substance — `code` | `Condition.code` / `FamilyMemberHistory.condition[].code` / `Observation.valueCodeableConcept` / `reaction[].manifestation[]` — value slot; `Procedure.code` / `AllergyIntolerance.code` — code slot | `LOCAL_CODE_ONLY` · **`ICD_ONLY`** · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
|
|
191
|
+
| **RxNorm** | medications — `manufacturedMaterial/code` | `Medication.code` via `medicationReference` (or inline `medicationCodeableConcept`) | **`NDC_ONLY`** · **`MULTUM_ONLY`** · **`FDB_ONLY`** · **`MEDISPAN_ONLY`** · `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` |
|
|
192
|
+
| **ICD-10-CM** | problems / encounters — diagnosis `value` | `Condition.code` (value slot) | **`SNOMED_ONLY`** · `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
|
|
193
|
+
| **CVX** | immunizations — `manufacturedMaterial/code` | `Immunization.vaccineCode` | **`RXNORM_ONLY`** · **`NDC_ONLY`** · `LOCAL_CODE_ONLY` · `NULLFLAVOR` · `DISPLAY_ONLY` · `NARRATIVE_ONLY` · `BAD_CODE` |
|
|
194
|
+
|
|
195
|
+
FHIR note: Trove bundles use the literal string `"unknown"` as the null marker
|
|
196
|
+
(the CCDA `nullFlavor` equivalent). The FHIR adapter normalizes it everywhere
|
|
197
|
+
(code / display / system / text); an entirely-"unknown" concept is reported as
|
|
198
|
+
a `NULLFLAVOR` gap, and a real translation next to an "unknown" primary (e.g.
|
|
199
|
+
CPT beside an "unknown" LOINC) is reported as `LOCAL_CODE_ONLY` with a
|
|
200
|
+
crosswalk candidate.
|
|
201
|
+
|
|
202
|
+
Planned (contracts stubbed): CPT · HCPCS · ICD-10-PCS.
|
|
203
|
+
|
|
204
|
+
## Gap patterns (glossary)
|
|
205
|
+
Two families. **Absence/quality patterns** apply to every system; **crosswalk
|
|
206
|
+
patterns** (bold above) mean a code from *another* system is present that maps
|
|
207
|
+
deterministically to the target. Full schema + per-pattern JSON in
|
|
208
|
+
`docs/GAP_SCHEMA.md`.
|
|
209
|
+
|
|
210
|
+
| Pattern | What's wrong | Recovery hint |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `LOCAL_CODE_ONLY` | a local/in-house/proprietary code, no standard code | crosswalk local→target, else text+axis search |
|
|
213
|
+
| `NULLFLAVOR` | code deliberately blank (`nullFlavor`); label from originalText/narrative | resolve recovered label |
|
|
214
|
+
| `DISPLAY_ONLY` | a display name (or empty code) only, no usable code | resolve label |
|
|
215
|
+
| `NARRATIVE_ONLY` | exists only as narrative-table text (opt-in) | value + reference range constrain |
|
|
216
|
+
| `BAD_CODE` | right shape but fails validation (LOINC Mod-10 · SNOMED Verhoeff · ICD format · CVX numeric) | check-digit / format correction |
|
|
217
|
+
| `ICD_ONLY` | ICD-9/10 present, no SNOMED | ICD↔SNOMED map (NLM) |
|
|
218
|
+
| `SNOMED_ONLY` | SNOMED present, no ICD-10-CM | SNOMED→ICD-10-CM map (NLM) |
|
|
219
|
+
| `NDC_ONLY` | NDC present, no RxNorm/CVX | NDC↔RxNorm / CDC NDC↔CVX table |
|
|
220
|
+
| `RXNORM_ONLY` | RxNorm vaccine product, no CVX | CVX↔RxNorm map |
|
|
221
|
+
| `MULTUM_ONLY` / `FDB_ONLY` / `MEDISPAN_ONLY` | proprietary drug vocab, no RxNorm | vendor↔RxNorm via UMLS CUI |
|
|
222
|
+
|
|
223
|
+
Skipped (never emitted): a valid code for the target (incl. via `<translation>`),
|
|
224
|
+
and observations whose only code is an HL7 structural marker like `ASSERTION` /
|
|
225
|
+
`ActClass` (the real concept lives in the value slot).
|
|
226
|
+
|
|
227
|
+
## Output schema (for the recovery / LLM team)
|
|
228
|
+
The `--json` output is **format-agnostic** (identical for CCDA and FHIR) and
|
|
229
|
+
flat. Per gap: `id, pattern, concept, section{}, codings[], absent_reason,
|
|
230
|
+
value{type}, reference_range?, match_key, specimen{}, panel?, checkdigit_correction?,
|
|
231
|
+
suggested_action, location{}`. The full contract, field reference, and a worked
|
|
232
|
+
JSON example per pattern are in **`docs/GAP_SCHEMA.md`** — share that with the LLM team.
|
|
233
|
+
|
|
234
|
+
## Tests
|
|
235
|
+
```bash
|
|
236
|
+
pip3 install lxml pytest --break-system-packages
|
|
237
|
+
python3 -m pytest aimedicalcoding/tests/ -q
|
|
238
|
+
python3 -m aimedicalcoding.tests.test_loinc_gaps # prints a gap summary
|
|
239
|
+
python3 -m aimedicalcoding.tests.test_fhir_adapter # prints parsed FHIR statements
|
|
240
|
+
```
|
|
241
|
+
Fixtures are synthetic (no PHI): `fixtures/sample_ccda.xml`,
|
|
242
|
+
`fixtures/sample_fhir_bundle.json`. Real documents live in `docs/input/` and
|
|
243
|
+
`docs/Input_FHIRs/` (both gitignored — never commit).
|
|
244
|
+
|
|
245
|
+
## Quick use
|
|
246
|
+
```python3
|
|
247
|
+
from aimedicalcoding.pipeline import parse_document
|
|
248
|
+
stmts = parse_document(open("doc.xml", "rb").read())
|
|
249
|
+
for s in stmts:
|
|
250
|
+
print(s.context, s.raw_text, [(c.system, c.code) for c in s.codings])
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Docs
|
|
254
|
+
- **`docs/GAP_SCHEMA.md`** — the JSON output contract for consumers: envelope,
|
|
255
|
+
field reference, the discriminated `value`, and a JSON example per pattern with
|
|
256
|
+
the recommended recovery action. **Share this with the LLM/recovery team.**
|
|
257
|
+
- **`docs/OUTPUT_GUIDE.md`** — how to read the human card/table report.
|
|
258
|
+
- **`docs/ARCHITECTURE.md`** — system design.
|
|
259
|
+
- **`docs/patterns/`** — per-terminology pattern analyses (LOINC, SNOMED, RxNorm):
|
|
260
|
+
structure, identifiers, where each appears in CCDA/FHIR, disambiguation, gap taxonomy.
|
|
261
|
+
- **`docs/samples/`** — example CCDAs (`sample_all_systems.xml` exercises every
|
|
262
|
+
pattern across all three systems).
|