Update spine_coder/spine_coder_core.py
Browse files- spine_coder/spine_coder_core.py +69 -23
spine_coder/spine_coder_core.py
CHANGED
|
@@ -95,7 +95,36 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 95 |
suggestions: List[Dict[str, Any]] = []
|
| 96 |
case_modifiers: List[Dict[str, str]] = []
|
| 97 |
|
| 98 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
levels = _extract_levels(t)
|
| 100 |
inters = _count_interspaces(levels)
|
| 101 |
span = _span_segments(levels)
|
|
@@ -149,7 +178,6 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 149 |
no_decompression_due_to_revision = True
|
| 150 |
|
| 151 |
# ---------- (2b) New posterior instrumentation WITHOUT explicit fusion ----------
|
| 152 |
-
# If new screws/rods/construct placed but no TLIF/PLIF/posterior-fusion wording, still code instrumentation.
|
| 153 |
if (
|
| 154 |
not _has(t, r"\btlif\b|\bplif\b|posterolateral\b.*\bfusion|posterior\b.*\bfusion")
|
| 155 |
and (mentions_inst_post or _has(t, r"\b(new|placed|inserted|reinserted)\b.{0,20}\b(screw|rod|construct|instrumentation)\b"))
|
|
@@ -163,52 +191,69 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 163 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 164 |
_add(suggestions, code, desc, "New posterior instrumentation documented.", "instrumentation", False, 0.82)
|
| 165 |
|
| 166 |
-
# ----------
|
| 167 |
-
exposure_only = _has(t, r"\banterior (exposure|approach) performed\b") and not _has(
|
| 168 |
-
t, r"\bfusion|discectomy|interbody|arthrodesis|cage|plate|spacer"
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
# ---------- (2) ACDF with guards (history/removal/exposure-only) ----------
|
| 172 |
found_removal = _has(t, rem_pat_either)
|
| 173 |
acdf_history = _has(t, r"\b(prior|previous|history of|s/?p)\s*acdf\b")
|
| 174 |
-
acdf_current =
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
n = max(1, inters or 1)
|
| 177 |
_add(suggestions, "22551", "ACDF, first interspace (includes discectomy)",
|
| 178 |
"Anterior cervical fusion pattern detected.", "ACDF", True, 0.95, mods=lat_mods)
|
| 179 |
if n > 1:
|
| 180 |
_add(suggestions, "22552", f"ACDF, each additional interspace ×{n-1}",
|
| 181 |
"Multi-level ACDF.", "ACDF add-on", False, 0.9, units=(n-1), mods=lat_mods)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
if mentions_plate_ant:
|
| 183 |
-
est_span =
|
|
|
|
| 184 |
code = _inst_code_by_span(est_span, anterior=True)
|
| 185 |
if code:
|
| 186 |
desc = {"22845":"Anterior instrumentation (2–3 segments)",
|
| 187 |
"22846":"Anterior instrumentation (4–7 segments)",
|
| 188 |
"22847":"Anterior instrumentation (8+ segments)"}[code]
|
| 189 |
-
_add(suggestions, code, desc, "Anterior plate present; span estimated from levels.", "instrumentation", False, 0.8)
|
| 190 |
|
| 191 |
# ---------- (3a) Implicit TLIF when keywords imply the construct ----------
|
| 192 |
-
# Fire when facetectomy + interbody cage/device + pedicle screws appear together.
|
| 193 |
if (
|
| 194 |
not _has(t, r"\btlif\b|\bplif\b|posterior interbody fusion")
|
| 195 |
-
and _has(t, r"\bfacetectom(y|ies)\b|complete facetectomy|hemifacetectomy")
|
| 196 |
-
and _has(t, r"\binterbody (cage|device|spacer)\b|peek cage|titanium cage|allograft spacer")
|
| 197 |
and (mentions_pedicle or _has(t, r"\bpedicle screw(s)?\b"))
|
| 198 |
and region in {"lumbar", "thoracic"}
|
| 199 |
):
|
| 200 |
n = max(1, inters or 1)
|
| 201 |
_add(suggestions, "22633", "Posterior/posterolateral + posterior interbody, single level",
|
| 202 |
-
"Implicit TLIF/PLIF: facetectomy + interbody device + pedicle screws.", "TLIF/PLIF", True, 0.
|
| 203 |
if n > 1:
|
| 204 |
_add(suggestions, "22634", f"Posterior interbody each additional interspace ×{n-1}",
|
| 205 |
-
"Multi-level implicit TLIF/PLIF.", "TLIF/PLIF add-on", False, 0.
|
| 206 |
code = _inst_code_by_span(span or (n+1), anterior=False)
|
| 207 |
if code:
|
| 208 |
desc = {"22842":"Posterior segmental instrumentation (2–3 segments)",
|
| 209 |
"22843":"Posterior segmental instrumentation (4–7 segments)",
|
| 210 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 211 |
-
_add(suggestions, code, desc, "Posterior instrumentation detected.", "instrumentation", False, 0.
|
| 212 |
|
| 213 |
# ---------- (3) TLIF / PLIF (beats posterior fusion) ----------
|
| 214 |
if _has(t, r"\btlif\b|\bplif\b|posterior interbody fusion"):
|
|
@@ -227,7 +272,7 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 227 |
_add(suggestions, code, desc, "Posterior instrumentation detected.", "instrumentation", False, 0.82)
|
| 228 |
|
| 229 |
# ---------- (4) ALIF ----------
|
| 230 |
-
if _has(t, r"\balif\b|anterior lumbar interbody fusion")
|
| 231 |
n = max(1, inters or 1)
|
| 232 |
_add(suggestions, "22558", "Anterior lumbar interbody fusion, single interspace",
|
| 233 |
"ALIF detected.", "ALIF", True, 0.9)
|
|
@@ -395,7 +440,7 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 395 |
for k, pat in mod_map.items():
|
| 396 |
if _has(t, pat): case_modifiers.append({"modifier": k, "reason": reasons[k]})
|
| 397 |
|
| 398 |
-
#
|
| 399 |
if not any(m["modifier"]=="50" for m in case_modifiers):
|
| 400 |
if _has(t, r"\bbilateral\b") and _has(t, r"(foraminotom(y|ies)|facetectom(y|ies)|laminectom(y|ies)|laminotom(y|ies))"):
|
| 401 |
case_modifiers.append({"modifier":"50","reason": reasons["50"]})
|
|
@@ -446,8 +491,9 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 446 |
has_50 = any(m["modifier"] == "50" for m in case_modifiers)
|
| 447 |
lat_row_mods = [] if has_50 else lat_mods
|
| 448 |
for row in out:
|
| 449 |
-
if
|
| 450 |
-
row
|
|
|
|
| 451 |
if has_50:
|
| 452 |
for row in out:
|
| 453 |
if row.get("modifiers"):
|
|
@@ -484,7 +530,7 @@ def suggest_with_cpt_billing(note: str, payer: str = "Medicare", top_k: int = 10
|
|
| 484 |
elif _has(t, bilateral_procedure_pat):
|
| 485 |
case_laterality = "bilateral"
|
| 486 |
|
| 487 |
-
# ---------- OPTIONAL: surface tech flags in primary rationale
|
| 488 |
if flags_list:
|
| 489 |
for r in out:
|
| 490 |
if r.get("primary", False):
|
|
|
|
| 95 |
suggestions: List[Dict[str, Any]] = []
|
| 96 |
case_modifiers: List[Dict[str, str]] = []
|
| 97 |
|
| 98 |
+
# --- Early guard: exposure-only / access-only -----------------------------
|
| 99 |
+
# Flexible: catches "anterior exposure of L4–S1 performed by vascular surgeon ... No fusion performed"
|
| 100 |
+
exposure_only = (
|
| 101 |
+
_has(t, r"\bexpos(e|ed|ure)\b|\bapproach\b") and
|
| 102 |
+
_has(t, r"\banterior\b|retroperitoneal|vascular (surgeon|exposure)|access\b") and
|
| 103 |
+
_has(t, r"\bno (fusion|arthrodesis|interbody|implants?|cage|plate|screw|instrumentation)\b|without (fusion|interbody|instrumentation)") and
|
| 104 |
+
not _has(t, r"\bfusion|arthrodesis|discectom\w+|interbody|tlif|plif|alif|acdf|arthroplasty|stimulator|instrument|hardware|screw|rod|plate|cage")
|
| 105 |
+
)
|
| 106 |
+
if exposure_only:
|
| 107 |
+
return {
|
| 108 |
+
"payer": payer,
|
| 109 |
+
"region": "unknown",
|
| 110 |
+
"levels": [],
|
| 111 |
+
"interspaces_est": 0,
|
| 112 |
+
"span_segments_est": 0,
|
| 113 |
+
"suggestions": [{
|
| 114 |
+
"cpt": "00000",
|
| 115 |
+
"desc": "No recognizable spine CPT pattern found",
|
| 116 |
+
"rationale": "Exposure-only access without decompression/fusion/instrumentation.",
|
| 117 |
+
"confidence": 0.0, "category": "none", "primary": True, "modifiers": [], "units": 1, "score": 0.0
|
| 118 |
+
}],
|
| 119 |
+
"case_modifiers": [],
|
| 120 |
+
"flags": [],
|
| 121 |
+
"flags_map": {},
|
| 122 |
+
"laterality": "na",
|
| 123 |
+
"build": "FINAL-v2.1",
|
| 124 |
+
"mode": "standard",
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
# --- Levels / spans / laterality -----------------------------------------
|
| 128 |
levels = _extract_levels(t)
|
| 129 |
inters = _count_interspaces(levels)
|
| 130 |
span = _span_segments(levels)
|
|
|
|
| 178 |
no_decompression_due_to_revision = True
|
| 179 |
|
| 180 |
# ---------- (2b) New posterior instrumentation WITHOUT explicit fusion ----------
|
|
|
|
| 181 |
if (
|
| 182 |
not _has(t, r"\btlif\b|\bplif\b|posterolateral\b.*\bfusion|posterior\b.*\bfusion")
|
| 183 |
and (mentions_inst_post or _has(t, r"\b(new|placed|inserted|reinserted)\b.{0,20}\b(screw|rod|construct|instrumentation)\b"))
|
|
|
|
| 191 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 192 |
_add(suggestions, code, desc, "New posterior instrumentation documented.", "instrumentation", False, 0.82)
|
| 193 |
|
| 194 |
+
# ---------- (2) ACDF with guards (history/removal) + broadened trigger ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
found_removal = _has(t, rem_pat_either)
|
| 196 |
acdf_history = _has(t, r"\b(prior|previous|history of|s/?p)\s*acdf\b")
|
| 197 |
+
acdf_current = (
|
| 198 |
+
_has(t, r"\banterior cervical discectomy|anterior cervical fusion|smith[- ]?robinson\b")
|
| 199 |
+
or (
|
| 200 |
+
(region in {"cervical","cervicothoracic"})
|
| 201 |
+
and _has(t, r"\bdiscectom\w+\b")
|
| 202 |
+
and (_has(t, r"\b(interbody|cage|arthrodesis|plate)\b") or mentions_plate_ant)
|
| 203 |
+
)
|
| 204 |
+
or _has(t, r"\bacdf\b")
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
if acdf_current and not (found_removal and not _has(t, r"\bacdf\b")) and not exposure_only:
|
| 208 |
n = max(1, inters or 1)
|
| 209 |
_add(suggestions, "22551", "ACDF, first interspace (includes discectomy)",
|
| 210 |
"Anterior cervical fusion pattern detected.", "ACDF", True, 0.95, mods=lat_mods)
|
| 211 |
if n > 1:
|
| 212 |
_add(suggestions, "22552", f"ACDF, each additional interspace ×{n-1}",
|
| 213 |
"Multi-level ACDF.", "ACDF add-on", False, 0.9, units=(n-1), mods=lat_mods)
|
| 214 |
+
|
| 215 |
+
# Plate span inference: "spanning C4–C7" or "span C4–C7"
|
| 216 |
+
m_span = re.search(r"\bspan\w*\s*(c\d)\s*[-–—]\s*(c\d)\b", t, flags=re.I)
|
| 217 |
+
plate_span_est = None
|
| 218 |
+
if m_span:
|
| 219 |
+
try:
|
| 220 |
+
c_lo = int(re.sub(r"\D","", m_span.group(1)))
|
| 221 |
+
c_hi = int(re.sub(r"\D","", m_span.group(2)))
|
| 222 |
+
if c_hi >= c_lo:
|
| 223 |
+
plate_span_est = c_hi - c_lo + 1
|
| 224 |
+
except Exception:
|
| 225 |
+
plate_span_est = None
|
| 226 |
+
|
| 227 |
if mentions_plate_ant:
|
| 228 |
+
est_span = (plate_span_est if plate_span_est and plate_span_est >= 2
|
| 229 |
+
else (span if (span and span >= 2) else (n + 1)))
|
| 230 |
code = _inst_code_by_span(est_span, anterior=True)
|
| 231 |
if code:
|
| 232 |
desc = {"22845":"Anterior instrumentation (2–3 segments)",
|
| 233 |
"22846":"Anterior instrumentation (4–7 segments)",
|
| 234 |
"22847":"Anterior instrumentation (8+ segments)"}[code]
|
| 235 |
+
_add(suggestions, code, desc, "Anterior plate present; span estimated from levels/plate span.", "instrumentation", False, 0.8)
|
| 236 |
|
| 237 |
# ---------- (3a) Implicit TLIF when keywords imply the construct ----------
|
|
|
|
| 238 |
if (
|
| 239 |
not _has(t, r"\btlif\b|\bplif\b|posterior interbody fusion")
|
| 240 |
+
and _has(t, r"\bfacetectom(y|ies)\b|complete facetectomy|hemifacetectomy|transforaminal")
|
| 241 |
+
and _has(t, r"\binterbody (cage|device|spacer)\b|peek (cage|spacer)|titanium (cage|spacer)|allograft spacer")
|
| 242 |
and (mentions_pedicle or _has(t, r"\bpedicle screw(s)?\b"))
|
| 243 |
and region in {"lumbar", "thoracic"}
|
| 244 |
):
|
| 245 |
n = max(1, inters or 1)
|
| 246 |
_add(suggestions, "22633", "Posterior/posterolateral + posterior interbody, single level",
|
| 247 |
+
"Implicit TLIF/PLIF: facetectomy + interbody device + pedicle screws.", "TLIF/PLIF", True, 0.93, mods=lat_mods)
|
| 248 |
if n > 1:
|
| 249 |
_add(suggestions, "22634", f"Posterior interbody each additional interspace ×{n-1}",
|
| 250 |
+
"Multi-level implicit TLIF/PLIF.", "TLIF/PLIF add-on", False, 0.89, units=(n-1), mods=lat_mods)
|
| 251 |
code = _inst_code_by_span(span or (n+1), anterior=False)
|
| 252 |
if code:
|
| 253 |
desc = {"22842":"Posterior segmental instrumentation (2–3 segments)",
|
| 254 |
"22843":"Posterior segmental instrumentation (4–7 segments)",
|
| 255 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 256 |
+
_add(suggestions, code, desc, "Posterior instrumentation detected (implicit TLIF).", "instrumentation", False, 0.83)
|
| 257 |
|
| 258 |
# ---------- (3) TLIF / PLIF (beats posterior fusion) ----------
|
| 259 |
if _has(t, r"\btlif\b|\bplif\b|posterior interbody fusion"):
|
|
|
|
| 272 |
_add(suggestions, code, desc, "Posterior instrumentation detected.", "instrumentation", False, 0.82)
|
| 273 |
|
| 274 |
# ---------- (4) ALIF ----------
|
| 275 |
+
if _has(t, r"\balif\b|anterior lumbar interbody fusion"):
|
| 276 |
n = max(1, inters or 1)
|
| 277 |
_add(suggestions, "22558", "Anterior lumbar interbody fusion, single interspace",
|
| 278 |
"ALIF detected.", "ALIF", True, 0.9)
|
|
|
|
| 440 |
for k, pat in mod_map.items():
|
| 441 |
if _has(t, pat): case_modifiers.append({"modifier": k, "reason": reasons[k]})
|
| 442 |
|
| 443 |
+
# Force -50 if 'bilateral' appears near decompression nouns and not already set
|
| 444 |
if not any(m["modifier"]=="50" for m in case_modifiers):
|
| 445 |
if _has(t, r"\bbilateral\b") and _has(t, r"(foraminotom(y|ies)|facetectom(y|ies)|laminectom(y|ies)|laminotom(y|ies))"):
|
| 446 |
case_modifiers.append({"modifier":"50","reason": reasons["50"]})
|
|
|
|
| 491 |
has_50 = any(m["modifier"] == "50" for m in case_modifiers)
|
| 492 |
lat_row_mods = [] if has_50 else lat_mods
|
| 493 |
for row in out:
|
| 494 |
+
if row["category"] in {"decompression","decompression add-on","TLIF/PLIF","posterior_fusion","posterior_fusion add-on"}:
|
| 495 |
+
if lat_row_mods and not row.get("modifiers"):
|
| 496 |
+
row["modifiers"] = lat_row_mods[:]
|
| 497 |
if has_50:
|
| 498 |
for row in out:
|
| 499 |
if row.get("modifiers"):
|
|
|
|
| 530 |
elif _has(t, bilateral_procedure_pat):
|
| 531 |
case_laterality = "bilateral"
|
| 532 |
|
| 533 |
+
# ---------- OPTIONAL: surface tech flags in primary rationale ----------
|
| 534 |
if flags_list:
|
| 535 |
for r in out:
|
| 536 |
if r.get("primary", False):
|