aimedicalcoding 0.3.4__tar.gz → 0.3.6__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.
Files changed (73) hide show
  1. {aimedicalcoding-0.3.4/aimedicalcoding.egg-info → aimedicalcoding-0.3.6}/PKG-INFO +1 -1
  2. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/ccda/codes.py +12 -0
  3. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/serialize.py +13 -22
  4. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/loinc/axes.py +42 -1
  5. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6/aimedicalcoding.egg-info}/PKG-INFO +1 -1
  6. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/GAP_SCHEMA.md +7 -8
  7. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/setup.py +1 -1
  8. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/LICENSE +0 -0
  9. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/MANIFEST.in +0 -0
  10. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/README.md +0 -0
  11. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/README_PYPI.md +0 -0
  12. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/__init__.py +0 -0
  13. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/__main__.py +0 -0
  14. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/__init__.py +0 -0
  15. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/base.py +0 -0
  16. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/ccda/__init__.py +0 -0
  17. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/ccda/adapter.py +0 -0
  18. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/ccda/narrative.py +0 -0
  19. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/ccda/sections.py +0 -0
  20. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/custom_json/__init__.py +0 -0
  21. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/custom_json/adapter.py +0 -0
  22. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/fhir/__init__.py +0 -0
  23. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/fhir/adapter.py +0 -0
  24. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/fhir/codes.py +0 -0
  25. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/adapters/fhir/mapping.py +0 -0
  26. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/api.py +0 -0
  27. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/core/__init__.py +0 -0
  28. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/core/checkdigits.py +0 -0
  29. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/core/models.py +0 -0
  30. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/core/system_registry.py +0 -0
  31. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/pipeline/__init__.py +0 -0
  32. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/pipeline/orchestrator.py +0 -0
  33. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/recovery/__init__.py +0 -0
  34. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/recovery/engine.py +0 -0
  35. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/__init__.py +0 -0
  36. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/base.py +0 -0
  37. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/cvx/__init__.py +0 -0
  38. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/cvx/handler.py +0 -0
  39. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/cvx/normalize.py +0 -0
  40. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/cvx/patterns.py +0 -0
  41. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/icd10cm/__init__.py +0 -0
  42. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/icd10cm/handler.py +0 -0
  43. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/icd10cm/icdformat.py +0 -0
  44. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/icd10cm/patterns.py +0 -0
  45. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/loinc/__init__.py +0 -0
  46. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/loinc/handler.py +0 -0
  47. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/loinc/normalize.py +0 -0
  48. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/loinc/patterns.py +0 -0
  49. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/rxnorm/__init__.py +0 -0
  50. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/rxnorm/handler.py +0 -0
  51. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/rxnorm/normalize.py +0 -0
  52. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/rxnorm/patterns.py +0 -0
  53. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/snomed/__init__.py +0 -0
  54. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/snomed/handler.py +0 -0
  55. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/snomed/hierarchy.py +0 -0
  56. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/snomed/patterns.py +0 -0
  57. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/systems/snomed/qualifiers.py +0 -0
  58. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/terminology/__init__.py +0 -0
  59. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding/terminology/service.py +0 -0
  60. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding.egg-info/SOURCES.txt +0 -0
  61. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding.egg-info/dependency_links.txt +0 -0
  62. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding.egg-info/entry_points.txt +0 -0
  63. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding.egg-info/requires.txt +0 -0
  64. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/aimedicalcoding.egg-info/top_level.txt +0 -0
  65. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/ARCHITECTURE.md +0 -0
  66. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/OUTPUT_GUIDE.md +0 -0
  67. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/patterns/CVX_pattern_analysis.md +0 -0
  68. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/patterns/ICD10CM_pattern_analysis.md +0 -0
  69. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/patterns/LOINC_pattern_analysis.md +0 -0
  70. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/patterns/RxNorm_pattern_analysis.md +0 -0
  71. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/docs/patterns/SNOMED_pattern_analysis.md +0 -0
  72. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/pyproject.toml +0 -0
  73. {aimedicalcoding-0.3.4 → aimedicalcoding-0.3.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aimedicalcoding
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
5
5
  Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
6
6
  License: MIT
@@ -186,6 +186,18 @@ def extract_companion(obs_el: etree._Element) -> dict:
186
186
  unit = (low.get("unit") if low is not None else None) or (
187
187
  high.get("unit") if high is not None else None)
188
188
  out["reference_range"] = f"{lo}-{hi}" + (f" {unit}" if unit else "")
189
+
190
+ # method: the LOINC Method axis, carried structurally as <methodCode>.
191
+ # Prefer the displayName (e.g. "Automated") over the raw code.
192
+ method = obs_el.find(q("methodCode"))
193
+ if method is not None and (method.get("displayName") or method.get("code")):
194
+ out["method"] = method.get("displayName") or method.get("code")
195
+
196
+ # body site: <targetSiteCode> (usually SNOMED, e.g. "Right arm").
197
+ site = obs_el.find(q("targetSiteCode"))
198
+ if site is not None and (site.get("displayName") or site.get("code")):
199
+ out["body_site"] = site.get("displayName") or site.get("code")
200
+ out["body_site_code"] = site.get("code")
189
201
  return out
190
202
 
191
203
 
@@ -87,14 +87,20 @@ def _qualifiers(stmt: ClinicalStatement) -> dict:
87
87
  unit = stmt.value.unit if stmt.value else None
88
88
  q = snomed_q.extract_qualifiers(stmt, stmt.raw_text)
89
89
  out = {
90
- "specimen": {"value": spec_hint, "source": spec_src, "confidence": spec_conf},
90
+ "specimen": {"value": spec_hint, "source": spec_src, "confidence": spec_conf,
91
+ # explicit specimen disagrees with the analyte text (data-quality signal)
92
+ "conflict": loinc_axes.specimen_conflict(stmt)},
91
93
  "property": loinc_axes.property_from_unit(unit),
92
94
  "scale": loinc_axes.scale_from_value(stmt),
95
+ "method": stmt.companion.get("method"),
96
+ "body_site": stmt.companion.get("body_site"),
93
97
  "laterality": q["laterality"],
94
98
  "severity": q["severity"],
95
99
  "course": q["course"],
96
100
  # structural negationInd wins over text cues ("denies", "no known")
97
101
  "negation": ("negated" if stmt.companion.get("negation") else q["negation_context"]),
102
+ "interpretation": stmt.companion.get("interpretation"),
103
+ "reference_range": stmt.companion.get("reference_range"),
98
104
  "panel": (stmt.panel.code if stmt.panel else None),
99
105
  "effective_time": stmt.effective_time,
100
106
  }
@@ -121,20 +127,6 @@ def _dedupe(items) -> list:
121
127
  return out
122
128
 
123
129
 
124
- # SNOMED search-term enrichment (Problems 11450-4, Allergies 48765-2, Social
125
- # History 29762-2), matched by section LOINC or by the contexts those sections
126
- # route to (FHIR resources often carry no meta.tag/section code). Which slot's
127
- # labels get appended differs by section:
128
- # - Social History -> CODE tag labels (the question, e.g. "Gender identity" —
129
- # the value label is often a placeholder like "Not on file")
130
- # - Problems / Allergies -> VALUE tag labels (the concept; the code tag is a
131
- # generic marker like ASSERTION / "Problem" — appending it is noise)
132
- _SNOMED_CODE_LABEL_SECTIONS = {"29762-2"}
133
- _SNOMED_CODE_LABEL_CONTEXTS = {"social_history"}
134
- _SNOMED_VALUE_LABEL_SECTIONS = {"11450-4", "48765-2"}
135
- _SNOMED_VALUE_LABEL_CONTEXTS = {"problem", "allergy_substance", "allergy_reaction"}
136
-
137
-
138
130
  def _recovery(record: NormalizedRecord, slot: str, system: str) -> dict:
139
131
  stmt, ev, g = record.statement, record.gap.evidence, record.gap
140
132
 
@@ -146,18 +138,17 @@ def _recovery(record: NormalizedRecord, slot: str, system: str) -> dict:
146
138
  terms = [stmt.display_name, stmt.original_text]
147
139
  # add any normalized term the handler produced
148
140
  terms += [ev.get("analyte_normalized"), ev.get("term_normalized"), ev.get("ingredient")]
149
- # SNOMED only, Problems/Allergies/Social History only: append trailing
150
- # label context from the section-appropriate slot (see constants above).
141
+ # SNOMED: guarantee the TARGET slot's own labels are present, mirroring
142
+ # `target.slot` in the output record — code-slot gaps carry the code tag's
143
+ # displayName/originalText, value-slot gaps the value tag's.
151
144
  if system == System.SNOMEDCT.value:
152
- if (stmt.section_code in _SNOMED_CODE_LABEL_SECTIONS
153
- or stmt.context in _SNOMED_CODE_LABEL_CONTEXTS):
154
- terms += [stmt.display_name, stmt.original_text]
155
- elif (stmt.section_code in _SNOMED_VALUE_LABEL_SECTIONS
156
- or stmt.context in _SNOMED_VALUE_LABEL_CONTEXTS):
145
+ if slot == "value":
157
146
  if stmt.value:
158
147
  terms += [stmt.value.coded_value.display
159
148
  if stmt.value.coded_value else None,
160
149
  stmt.value.original_text]
150
+ else:
151
+ terms += [stmt.display_name, stmt.original_text]
161
152
 
162
153
  rec = {
163
154
  "search_terms": _dedupe(terms),
@@ -14,11 +14,26 @@ from .normalize import normalize_analyte
14
14
 
15
15
  # UCUM unit -> LOINC Property hint
16
16
  _UNIT_PROPERTY = {
17
+ # mass concentration
17
18
  "mg/dl": "MCnc", "g/dl": "MCnc", "ug/dl": "MCnc", "ng/ml": "MCnc",
18
- "mmol/l": "SCnc", "meq/l": "SCnc", "umol/l": "SCnc", "u/l": "CCnc",
19
+ "mg/l": "MCnc", "ug/l": "MCnc", "ng/dl": "MCnc", "pg/ml": "MCnc",
20
+ "g/l": "MCnc",
21
+ # substance concentration
22
+ "mmol/l": "SCnc", "meq/l": "SCnc", "umol/l": "SCnc", "nmol/l": "SCnc",
23
+ "pmol/l": "SCnc", "mol/l": "SCnc",
24
+ # catalytic (enzyme) concentration
25
+ "u/l": "CCnc", "iu/l": "CCnc", "u/ml": "CCnc",
26
+ # arbitrary / international-unit concentration
27
+ "iu/ml": "ACnc", "miu/ml": "ACnc", "iu/dl": "ACnc",
28
+ # number concentration (counts per volume) — CBC analytes (platelets, WBC, RBC)
29
+ "10*3/ul": "NCnc", "10*6/ul": "NCnc", "10*9/l": "NCnc", "10*12/l": "NCnc",
30
+ "10*3/l": "NCnc", "/ul": "NCnc", "/l": "NCnc", "k/ul": "NCnc",
31
+ "m/ul": "NCnc", "cells/ul": "NCnc", "/mm3": "NCnc",
32
+ # fractions / other
19
33
  "%": "MFr", "fl": "EntVol", "pg": "EntMass",
20
34
  "mm[hg]": "Pres", "/min": "NRat", "cel": "Temp", "kg": "Mass",
21
35
  "cm": "Len", "kg/m2": "Ratio", "[degf]": "Temp", "/min)": "NRat",
36
+ "s": "Time", "mm/h": "Vel",
22
37
  }
23
38
 
24
39
  _SPECIMEN_HINTS = {
@@ -143,6 +158,11 @@ def infer_specimen(stmt: ClinicalStatement) -> tuple[Optional[str], str, str]:
143
158
 
144
159
 
145
160
  def method_from_text(stmt: ClinicalStatement) -> Optional[str]:
161
+ # Structural <methodCode> (LOINC Method axis) is more reliable than text
162
+ # cues — use it first (e.g. "Automated"), then fall back to text keywords.
163
+ method = stmt.companion.get("method")
164
+ if method:
165
+ return method
146
166
  text = (stmt.raw_text or "").lower()
147
167
  for kw in ("fasting", "post", "challenge", "poc", "point of care", "random"):
148
168
  if kw in text:
@@ -150,6 +170,24 @@ def method_from_text(stmt: ClinicalStatement) -> Optional[str]:
150
170
  return None
151
171
 
152
172
 
173
+ # specimen words that name a specimen in analyte text (reuse the hint map)
174
+ def specimen_conflict(stmt: ClinicalStatement) -> bool:
175
+ """True when an EXPLICIT specimen disagrees with a specimen named in the
176
+ analyte text — a data-quality signal (LOINC_pattern_analysis §1.2). E.g. an
177
+ explicit "Urine specimen" on a "Platelets ... in Blood" observation."""
178
+ explicit = None
179
+ if stmt.companion.get("specimen") or stmt.companion.get("specimen_code"):
180
+ explicit = _specimen_to_hint(stmt.companion.get("specimen"),
181
+ stmt.companion.get("specimen_code"))
182
+ if not explicit:
183
+ return False
184
+ text = (stmt.raw_text or "").lower()
185
+ for word, hint in _SPECIMEN_HINTS.items():
186
+ if word in text and hint != explicit:
187
+ return True
188
+ return False
189
+
190
+
153
191
  def build_match_key(stmt: ClinicalStatement, expand_abbrev: bool = False) -> str:
154
192
  """Composite disambiguation key (component + specimen + property + scale + method).
155
193
 
@@ -191,9 +229,12 @@ def evidence_packet(stmt: ClinicalStatement, expand_abbrev: bool = False) -> dic
191
229
  "specimen_hint": specimen_hint,
192
230
  "specimen_source": specimen_source, # explicit | text | panel | none
193
231
  "specimen_confidence": specimen_conf, # high | medium | low | none
232
+ "specimen_conflict": specimen_conflict(stmt), # explicit vs analyte text
194
233
  "method_hint": method_from_text(stmt),
195
234
  "value": stmt.value.raw if stmt.value else None,
196
235
  "reference_range": stmt.companion.get("reference_range"),
236
+ "interpretation": stmt.companion.get("interpretation"),
237
+ "body_site": stmt.companion.get("body_site"),
197
238
  "panel": (stmt.panel.code if stmt.panel else None),
198
239
  "context": stmt.context,
199
240
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aimedicalcoding
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / ICD-10-CM / CVX) in CCDA and FHIR documents, with recovery evidence
5
5
  Author: Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan
6
6
  License: MIT
@@ -63,9 +63,11 @@ Each record separates four concerns:
63
63
  "codings": [ ... ]
64
64
  },
65
65
  "qualifiers": { // disambiguation context (superset; nulls where N/A)
66
- "specimen": { "value","source","confidence" }, // LOINC
67
- "property": "MCnc", "scale": "Qn", // LOINC
66
+ "specimen": { "value","source","confidence","conflict" }, // LOINC; conflict=explicit specimen disagrees with analyte text
67
+ "property": "MCnc", "scale": "Qn", // LOINC (property from UCUM unit; NCnc for counts, etc.)
68
+ "method": "Automated", "body_site": "Right arm", // LOINC Method axis (<methodCode>) / <targetSiteCode>
68
69
  "laterality": null, "severity": null, "course": null, "negation": null, // SNOMED
70
+ "interpretation": "N", "reference_range": "150-400 10*3/uL", // observation companion data
69
71
  "panel": "24323-8", "effective_time": null
70
72
  }
71
73
  }
@@ -89,12 +91,9 @@ Each record separates four concerns:
89
91
  - `target.slot = "code"` (LOINC) → search `observation.code.display_text` / `.original_text`.
90
92
  - `target.slot = "value"` (SNOMED value-slot) → search `observation.value.display_text` / `.original_text`.
91
93
  `recovery.search_terms` already gives you the right ones, deduped.
92
- For **SNOMED-CT** targets, extra label context is appended at the end,
93
- section-specific: **Social History (29762-2)** appends the CODE tag's
94
- `display_text`/`original_text` (the question useful when the value label
95
- is a placeholder like "Not on file"); **Problems (11450-4)** and
96
- **Allergies (48765-2)** append the VALUE tag's labels (the concept — the
97
- code tag there is a generic marker like ASSERTION/"Problem").
94
+ For **SNOMED-CT** targets the terms follow `target.slot`: value-slot gaps
95
+ carry the VALUE tag's `display_text`/`original_text`, code-slot gaps the
96
+ CODE tag's — the other slot's labels are not mixed in.
98
97
 
99
98
  ---
100
99
 
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="aimedicalcoding",
5
- version="0.3.4",
5
+ version="0.3.6",
6
6
  author="Aakash Kag, Vinay Shankar Miryala, Siva Karthik, Mohammed Rayaan",
7
7
  description=(
8
8
  "Detect missing/broken clinical codes (LOINC / SNOMED CT / RxNorm / "
File without changes