Spaces:
Sleeping
Sleeping
Add spine_coder package (core + dataset + tests)
Browse files- spine_coder/spine_coder/__pycache__/__init__.cpython-312.pyc +0 -0
- spine_coder/spine_coder/__pycache__/spine_coder_core.cpython-312.pyc +0 -0
- spine_coder/spine_coder/spine_coder_core.py +52 -12
- spine_coder/spine_coder/tests/__pycache__/conftest.cpython-312-pytest-8.4.2.pyc +0 -0
- spine_coder/spine_coder/tests/__pycache__/test_conflicts.cpython-312-pytest-8.4.2.pyc +0 -0
- spine_coder/spine_coder/tests/__pycache__/test_coverage_and_modifiers.cpython-312-pytest-8.4.2.pyc +0 -0
- spine_coder/spine_coder/tests/__pycache__/test_dataset_basic.cpython-312-pytest-8.4.2.pyc +0 -0
spine_coder/spine_coder/__pycache__/__init__.cpython-312.pyc
CHANGED
|
Binary files a/spine_coder/spine_coder/__pycache__/__init__.cpython-312.pyc and b/spine_coder/spine_coder/__pycache__/__init__.cpython-312.pyc differ
|
|
|
spine_coder/spine_coder/__pycache__/spine_coder_core.cpython-312.pyc
CHANGED
|
Binary files a/spine_coder/spine_coder/__pycache__/spine_coder_core.cpython-312.pyc and b/spine_coder/spine_coder/__pycache__/spine_coder_core.cpython-312.pyc differ
|
|
|
spine_coder/spine_coder/spine_coder_core.py
CHANGED
|
@@ -19,6 +19,7 @@ def _level_sort_key(lv: str) -> Tuple[int, int]:
|
|
| 19 |
|
| 20 |
def _extract_levels(t: str) -> List[str]:
|
| 21 |
levels = set()
|
|
|
|
| 22 |
for m in re.finditer(r"\b([CTLS])\s?(\d{1,2})\s*[-–]\s*([CTLS])?\s?(\d{1,2})\b", t, flags=re.I):
|
| 23 |
pfx1, n1, pfx2, n2 = m.group(1).upper(), int(m.group(2)), (m.group(3) or m.group(1)).upper(), int(m.group(4))
|
| 24 |
if pfx1 != pfx2:
|
|
@@ -26,6 +27,7 @@ def _extract_levels(t: str) -> List[str]:
|
|
| 26 |
else:
|
| 27 |
lo, hi = sorted([n1, n2])
|
| 28 |
for k in range(lo, hi+1): levels.add(f"{pfx1}{k}")
|
|
|
|
| 29 |
for m in re.finditer(r"\b([CTLS])\s?(\d{1,2})\b", t, flags=re.I):
|
| 30 |
levels.add(f"{m.group(1).upper()}{int(m.group(2))}")
|
| 31 |
return sorted(levels, key=_level_sort_key)
|
|
@@ -40,7 +42,9 @@ def _count_interspaces(levels: List[str]) -> int:
|
|
| 40 |
for arr in by_band.values():
|
| 41 |
if len(arr) >= 2:
|
| 42 |
inters += sum(1 for i in range(1, len(arr)) if arr[i]-arr[i-1]==1)
|
|
|
|
| 43 |
if "L5" in levels and "S1" in levels: inters += 1
|
|
|
|
| 44 |
if inters == 0:
|
| 45 |
for arr in by_band.values():
|
| 46 |
if len(arr) >= 2: inters = 1; break
|
|
@@ -61,7 +65,7 @@ def _laterality_mods(t: str) -> List[str]:
|
|
| 61 |
mods: List[str] = []
|
| 62 |
if _has(t, r"\bleft\b|left-sided|left side|leftward|hemilam\w* left|foraminotomy left|\blt\b"):
|
| 63 |
mods.append("LT")
|
| 64 |
-
#
|
| 65 |
if _has(t, r"\bright\b|right-sided|right side|rightward|hemilam\w* right|foraminotomy right"):
|
| 66 |
mods.append("RT")
|
| 67 |
return list(sorted(set(mods)))
|
|
@@ -82,6 +86,17 @@ def _inst_code_by_span(span_segments: int, anterior: bool) -> str:
|
|
| 82 |
else:
|
| 83 |
return "22842" if span_segments <= 3 else ("22843" if span_segments <= 7 else "22844")
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
# ---------- main ----------
|
| 86 |
def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[str, Any]:
|
| 87 |
t = _norm(note)
|
|
@@ -93,13 +108,22 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 93 |
span = _span_segments(levels)
|
| 94 |
lat_mods = _laterality_mods(t)
|
| 95 |
|
| 96 |
-
# ---------- Region ----------
|
| 97 |
region = "unknown"
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
mentions_plate_ant = _has(t, r"\b(anterior (plate|instrument(ation)?)|plate fixed|plating)\b") or _has(t, r"\bplate\b")
|
| 104 |
mentions_pedicle = _has(t, r"\bpedicle screw|pedicle-screw|pedicle fixation|s2ai\b")
|
| 105 |
mentions_rod = _has(t, r"\brod(s)?\b")
|
|
@@ -211,7 +235,7 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 211 |
"Pulse generator implanted.", "neurostimulator", False, 0.84)
|
| 212 |
avoid_decompression_for_stimulator = True
|
| 213 |
|
| 214 |
-
# ---------- (7) Decompression (guarded
|
| 215 |
if (not no_decompression_due_to_revision) and (not avoid_decompression_for_stimulator):
|
| 216 |
if _has(t, r"\bdecompression(s)?\b|laminectom(y|ies)|laminotom(y|ies)|foraminotom(y|ies)|foraminal decompression(s)?|neuroforamen|lateral recess|central stenosis") \
|
| 217 |
and not _has(t, r"\b(prior|previous|history of|s/?p)\s+(decompression|laminectomy|laminotomy|foraminotomy)\b"):
|
|
@@ -244,7 +268,6 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 244 |
if _has(t, r"\bcorpectomy\b") and (region == "cervical" or _has(t, r"\bcervic")):
|
| 245 |
_add(suggestions, "63081", "Cervical corpectomy for decompression, first segment",
|
| 246 |
"Cervical corpectomy documented.", "tumor/corpectomy", True, 0.86)
|
| 247 |
-
# rough extra estimate
|
| 248 |
if _has(t, r"additional (level|segment)|two levels|three levels|multi|c\d-?c\d"):
|
| 249 |
extra = max(1, inters or 1)
|
| 250 |
_add(suggestions, "63082", f"Each additional cervical segment ×{extra}",
|
|
@@ -270,7 +293,6 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 270 |
_add(suggestions, base, "Posterior column osteotomy (Smith-Petersen), first level",
|
| 271 |
"Deformity correction with SPO documented.", "deformity", True, 0.83)
|
| 272 |
if _has(t, r"additional (level|segment)|two levels|three levels|multi"):
|
| 273 |
-
# For simplicity keep base+1 add-on mapping
|
| 274 |
_add(suggestions, str(int(base)+1), "Each additional vertebral segment ×1+",
|
| 275 |
"Multi-level osteotomy.", "deformity add-on", False, 0.78)
|
| 276 |
|
|
@@ -336,7 +358,7 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 336 |
for k, pat in mod_map.items():
|
| 337 |
if _has(t, pat): case_modifiers.append({"modifier": k, "reason": reasons[k]})
|
| 338 |
|
| 339 |
-
# EXPLICIT_BILATERAL_DECOMP
|
| 340 |
if not any(m["modifier"]=="50" for m in case_modifiers):
|
| 341 |
if _has(t, r"\bbilateral\b") and _has(t, r"(foraminotom(y|ies)|facetectom(y|ies)|laminectom(y|ies)|laminotom(y|ies))"):
|
| 342 |
case_modifiers.append({"modifier":"50","reason": reasons["50"]})
|
|
@@ -388,6 +410,19 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 388 |
out.sort(key=lambda r: (not r.get("primary", False), -(r.get("score", r.get("confidence",0.0))), _cpt_num(r)))
|
| 389 |
if isinstance(top_k, int) and top_k > 0: out = out[:top_k]
|
| 390 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
return {
|
| 392 |
"payer": payer,
|
| 393 |
"region": region,
|
|
@@ -395,5 +430,10 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 395 |
"interspaces_est": inters,
|
| 396 |
"span_segments_est": span,
|
| 397 |
"suggestions": out,
|
| 398 |
-
"case_modifiers": case_modifiers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
}
|
|
|
|
| 19 |
|
| 20 |
def _extract_levels(t: str) -> List[str]:
|
| 21 |
levels = set()
|
| 22 |
+
# Ranges like C4–C7 (en/em dashes) or hyphen
|
| 23 |
for m in re.finditer(r"\b([CTLS])\s?(\d{1,2})\s*[-–]\s*([CTLS])?\s?(\d{1,2})\b", t, flags=re.I):
|
| 24 |
pfx1, n1, pfx2, n2 = m.group(1).upper(), int(m.group(2)), (m.group(3) or m.group(1)).upper(), int(m.group(4))
|
| 25 |
if pfx1 != pfx2:
|
|
|
|
| 27 |
else:
|
| 28 |
lo, hi = sorted([n1, n2])
|
| 29 |
for k in range(lo, hi+1): levels.add(f"{pfx1}{k}")
|
| 30 |
+
# Singles like C5, L4, S1
|
| 31 |
for m in re.finditer(r"\b([CTLS])\s?(\d{1,2})\b", t, flags=re.I):
|
| 32 |
levels.add(f"{m.group(1).upper()}{int(m.group(2))}")
|
| 33 |
return sorted(levels, key=_level_sort_key)
|
|
|
|
| 42 |
for arr in by_band.values():
|
| 43 |
if len(arr) >= 2:
|
| 44 |
inters += sum(1 for i in range(1, len(arr)) if arr[i]-arr[i-1]==1)
|
| 45 |
+
# Special-case L5–S1 interspace if both present
|
| 46 |
if "L5" in levels and "S1" in levels: inters += 1
|
| 47 |
+
# Fallback: if multiple segments but no explicit adjacency caught, assume at least 1 interspace
|
| 48 |
if inters == 0:
|
| 49 |
for arr in by_band.values():
|
| 50 |
if len(arr) >= 2: inters = 1; break
|
|
|
|
| 65 |
mods: List[str] = []
|
| 66 |
if _has(t, r"\bleft\b|left-sided|left side|leftward|hemilam\w* left|foraminotomy left|\blt\b"):
|
| 67 |
mods.append("LT")
|
| 68 |
+
# RIGHT: avoid bare \brt\b to reduce false positives
|
| 69 |
if _has(t, r"\bright\b|right-sided|right side|rightward|hemilam\w* right|foraminotomy right"):
|
| 70 |
mods.append("RT")
|
| 71 |
return list(sorted(set(mods)))
|
|
|
|
| 86 |
else:
|
| 87 |
return "22842" if span_segments <= 3 else ("22843" if span_segments <= 7 else "22844")
|
| 88 |
|
| 89 |
+
def tech_flags(note: str) -> dict:
|
| 90 |
+
"""Surface microscope, navigation, neuromonitoring, and fluoro flags."""
|
| 91 |
+
t = _norm(note)
|
| 92 |
+
flags = {
|
| 93 |
+
"microscope": _has(t, r"\bmicroscope\b|operating microscope"),
|
| 94 |
+
"nav": _has(t, r"\bnavigation\b|o-?arm|computer[- ]?assisted"),
|
| 95 |
+
"io_monitor": _has(t, r"\bneuromonitor\w*|\b(ssep|meps?)\b|intraoperative monitoring"),
|
| 96 |
+
"fluoro": _has(t, r"\bfluoro\w*|\bc-arm\b"),
|
| 97 |
+
}
|
| 98 |
+
return {"flags": flags}
|
| 99 |
+
|
| 100 |
# ---------- main ----------
|
| 101 |
def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[str, Any]:
|
| 102 |
t = _norm(note)
|
|
|
|
| 108 |
span = _span_segments(levels)
|
| 109 |
lat_mods = _laterality_mods(t)
|
| 110 |
|
| 111 |
+
# ---------- Region (with transitional cervicothoracic first) ----------
|
| 112 |
region = "unknown"
|
| 113 |
+
lvlset = set(levels)
|
| 114 |
+
# Transitional zone FIRST: cervicothoracic (e.g., C7–T2)
|
| 115 |
+
if _has(t, r"\bcervico[- ]?thoracic\b") \
|
| 116 |
+
or _has(t, r"\bc7\s*[-–]\s*t[1-3]\b") \
|
| 117 |
+
or ("C7" in lvlset and any(x in lvlset for x in ("T1","T2","T3"))):
|
| 118 |
+
region = "cervicothoracic"
|
| 119 |
+
elif any(l.startswith("C") for l in levels) or _has(t, r"\bcervic"):
|
| 120 |
+
region = "cervical"
|
| 121 |
+
elif any(l.startswith("T") for l in levels) or _has(t, r"\bthorac"):
|
| 122 |
+
region = "thoracic"
|
| 123 |
+
elif any(l.startswith(("L","S")) for l in levels) or _has(t, r"\blumbar|sacrum"):
|
| 124 |
+
region = "lumbar"
|
| 125 |
+
|
| 126 |
+
# Helper flags for instrumentation
|
| 127 |
mentions_plate_ant = _has(t, r"\b(anterior (plate|instrument(ation)?)|plate fixed|plating)\b") or _has(t, r"\bplate\b")
|
| 128 |
mentions_pedicle = _has(t, r"\bpedicle screw|pedicle-screw|pedicle fixation|s2ai\b")
|
| 129 |
mentions_rod = _has(t, r"\brod(s)?\b")
|
|
|
|
| 235 |
"Pulse generator implanted.", "neurostimulator", False, 0.84)
|
| 236 |
avoid_decompression_for_stimulator = True
|
| 237 |
|
| 238 |
+
# ---------- (7) Decompression (guarded) ----------
|
| 239 |
if (not no_decompression_due_to_revision) and (not avoid_decompression_for_stimulator):
|
| 240 |
if _has(t, r"\bdecompression(s)?\b|laminectom(y|ies)|laminotom(y|ies)|foraminotom(y|ies)|foraminal decompression(s)?|neuroforamen|lateral recess|central stenosis") \
|
| 241 |
and not _has(t, r"\b(prior|previous|history of|s/?p)\s+(decompression|laminectomy|laminotomy|foraminotomy)\b"):
|
|
|
|
| 268 |
if _has(t, r"\bcorpectomy\b") and (region == "cervical" or _has(t, r"\bcervic")):
|
| 269 |
_add(suggestions, "63081", "Cervical corpectomy for decompression, first segment",
|
| 270 |
"Cervical corpectomy documented.", "tumor/corpectomy", True, 0.86)
|
|
|
|
| 271 |
if _has(t, r"additional (level|segment)|two levels|three levels|multi|c\d-?c\d"):
|
| 272 |
extra = max(1, inters or 1)
|
| 273 |
_add(suggestions, "63082", f"Each additional cervical segment ×{extra}",
|
|
|
|
| 293 |
_add(suggestions, base, "Posterior column osteotomy (Smith-Petersen), first level",
|
| 294 |
"Deformity correction with SPO documented.", "deformity", True, 0.83)
|
| 295 |
if _has(t, r"additional (level|segment)|two levels|three levels|multi"):
|
|
|
|
| 296 |
_add(suggestions, str(int(base)+1), "Each additional vertebral segment ×1+",
|
| 297 |
"Multi-level osteotomy.", "deformity add-on", False, 0.78)
|
| 298 |
|
|
|
|
| 358 |
for k, pat in mod_map.items():
|
| 359 |
if _has(t, pat): case_modifiers.append({"modifier": k, "reason": reasons[k]})
|
| 360 |
|
| 361 |
+
# EXPLICIT_BILATERAL_DECOMP
|
| 362 |
if not any(m["modifier"]=="50" for m in case_modifiers):
|
| 363 |
if _has(t, r"\bbilateral\b") and _has(t, r"(foraminotom(y|ies)|facetectom(y|ies)|laminectom(y|ies)|laminotom(y|ies))"):
|
| 364 |
case_modifiers.append({"modifier":"50","reason": reasons["50"]})
|
|
|
|
| 410 |
out.sort(key=lambda r: (not r.get("primary", False), -(r.get("score", r.get("confidence",0.0))), _cpt_num(r)))
|
| 411 |
if isinstance(top_k, int) and top_k > 0: out = out[:top_k]
|
| 412 |
|
| 413 |
+
# Tech flags
|
| 414 |
+
_tflags = tech_flags(note).get("flags", {})
|
| 415 |
+
_tflag_list = [k for k, v in _tflags.items() if v]
|
| 416 |
+
|
| 417 |
+
# Case-level laterality (meta)
|
| 418 |
+
case_laterality = "na"
|
| 419 |
+
if _has(t, r"\bbilateral\b|both sides"):
|
| 420 |
+
case_laterality = "bilateral"
|
| 421 |
+
elif _has(t, r"\bleft[- ]sided|\bleft\b"):
|
| 422 |
+
case_laterality = "left"
|
| 423 |
+
elif _has(t, r"\bright[- ]sided|\bright\b"):
|
| 424 |
+
case_laterality = "right"
|
| 425 |
+
|
| 426 |
return {
|
| 427 |
"payer": payer,
|
| 428 |
"region": region,
|
|
|
|
| 430 |
"interspaces_est": inters,
|
| 431 |
"span_segments_est": span,
|
| 432 |
"suggestions": out,
|
| 433 |
+
"case_modifiers": case_modifiers,
|
| 434 |
+
"flags": _tflag_list, # compact list for UI
|
| 435 |
+
"flags_map": _tflags, # full boolean map
|
| 436 |
+
"laterality": case_laterality,
|
| 437 |
+
"build": "FINAL-v2.0",
|
| 438 |
+
"mode": "standard",
|
| 439 |
}
|
spine_coder/spine_coder/tests/__pycache__/conftest.cpython-312-pytest-8.4.2.pyc
CHANGED
|
Binary files a/spine_coder/spine_coder/tests/__pycache__/conftest.cpython-312-pytest-8.4.2.pyc and b/spine_coder/spine_coder/tests/__pycache__/conftest.cpython-312-pytest-8.4.2.pyc differ
|
|
|
spine_coder/spine_coder/tests/__pycache__/test_conflicts.cpython-312-pytest-8.4.2.pyc
CHANGED
|
Binary files a/spine_coder/spine_coder/tests/__pycache__/test_conflicts.cpython-312-pytest-8.4.2.pyc and b/spine_coder/spine_coder/tests/__pycache__/test_conflicts.cpython-312-pytest-8.4.2.pyc differ
|
|
|
spine_coder/spine_coder/tests/__pycache__/test_coverage_and_modifiers.cpython-312-pytest-8.4.2.pyc
CHANGED
|
Binary files a/spine_coder/spine_coder/tests/__pycache__/test_coverage_and_modifiers.cpython-312-pytest-8.4.2.pyc and b/spine_coder/spine_coder/tests/__pycache__/test_coverage_and_modifiers.cpython-312-pytest-8.4.2.pyc differ
|
|
|
spine_coder/spine_coder/tests/__pycache__/test_dataset_basic.cpython-312-pytest-8.4.2.pyc
CHANGED
|
Binary files a/spine_coder/spine_coder/tests/__pycache__/test_dataset_basic.cpython-312-pytest-8.4.2.pyc and b/spine_coder/spine_coder/tests/__pycache__/test_dataset_basic.cpython-312-pytest-8.4.2.pyc differ
|
|
|