Map ICD-10-CM to CPT Using Python Dictionaries
Problem: a diagnosis-to-procedure pairing must be validated in-memory before the service line reaches the SV1 segment of an X12 837P claim — a linear scan of a flat table or a per-claim database round-trip cannot keep up with batch scrubbing, and a naive dict blows past its RAM ceiling once the crosswalk crosses six figures of rows. This page builds a nested Python dictionary that answers “is this ICD-10-CM → CPT pairing valid for this payer?” in O(1) average time, with memory bounded by sys.intern and every miss routed deterministically instead of raising an unhandled KeyError.
The lookup lives inside the ICD-10-CM to CPT crosswalk mapping tier of the scrubbing pipeline — after payer resolution, before service-line assembly. A miss here is not a crash; it is a routing decision handed to the same fallback routing logic for invalid codes state machine that catches an unknown payer or a bad CPT.
Prerequisites
Spec Reference: What the Lookup Keys On
The resolver keys on three values and returns a payer-scoped rule payload. Only these fields participate in the dictionary structure; everything else is carried inside the rule payload.
| Value | Source | Format | Role in lookup |
|---|---|---|---|
| ICD-10-CM code | HI01-2 diagnosis (837P HI segment) |
3–7 alphanumerics, no decimal in the segment | Top-level dictionary key |
| CPT / HCPCS code | Service line procedure | 5 numeric (CPT) or 1 alpha + 4 numeric (HCPCS II) | Second-level key |
| Payer identifier | NM109 in Loop 2010BB (NM1*PR) |
Payer ID / CMS Plan ID | Third-level key |
| Rule payload | Crosswalk / contract table | dict (modifiers, effective dates, override flags) | Leaf value |
The output of a successful lookup feeds one composite element: in the 5010 837P, SV101 carries the procedure qualifier and code (e.g. HC:99213), while diagnosis pointer indices that reference HI entries by position live in SV107–SV110, not alongside the CPT code. The resolver builds the SV101 composite; the caller populates the pointers from the claim’s HI ordering.
SV101 HC:CPT composite, while a missing key at any level routes to the fallback queue with a PHI-masked log — never a bare KeyError.Step-by-Step Implementation
Step 1 — Structure the nested crosswalk for O(1) lookups
A flat {(icd, cpt): rule} table cannot express the many-to-many, payer-scoped reality of medical-necessity rules. Model the crosswalk as three nested dictionaries — {ICD10: {CPT: {payer_id: rule_payload}}} — so each level is an average-case O(1) hash lookup and the same ICD-10 code can fan out to many procedures and payers without duplication.
from typing import Dict, Tuple, Optional
import sys
import logging
# Type aliases for clarity
ICD10Code = str
CPTCode = str
PayerID = str
RulePayload = Dict[str, object]
# Core crosswalk structure: {ICD10: {CPT: {payer_id: rule_payload}}}
CROSSWALK: Dict[ICD10Code, Dict[CPTCode, Dict[PayerID, RulePayload]]] = {}
def load_crosswalk_chunk(raw_records: list[Tuple[str, str, str, RulePayload]]) -> None:
"""Memory-optimized ingestion using string interning on every code key."""
for icd, cpt, payer, rule_data in raw_records:
# Intern strings to collapse duplicate object overhead in memory
icd_key = sys.intern(icd)
cpt_key = sys.intern(cpt)
payer_key = sys.intern(payer)
CROSSWALK.setdefault(icd_key, {}).setdefault(cpt_key, {})[payer_key] = rule_data
Step 2 — Bound the memory footprint
A 100,000+ row mapping loaded into plain Python dicts can consume 150–300 MB of RAM through per-object overhead, which is fatal when many worker processes each hold a copy. Four measures keep the footprint bounded:
- Call
sys.intern()on every code string at ingestion (step 1) so the millions of repeated ICD-10, CPT, and payer strings collapse to single interned objects. - Wrap completed inner dicts with
types.MappingProxyTypewhere the rules are immutable at runtime — this prevents accidental mutation of shared rule payloads, though it does not itself reduce memory. - Hydrate lazily via
sqlite3with an LRU cache layer, loading only the dictionary slices for the payer batches currently in flight rather than the whole table. - Enforce a ceiling in CI/CD by profiling with
sys.getsizeof()so a code-set release that balloons the table fails the build instead of the pod.
The same memory discipline underpins the CPT modifier validation matrix, which loads NCCI PTP/MUE tables the same way; keep both structures on the same profiling budget.
Step 3 — Resolve a pairing with PHI-safe fallback routing
A scrubbing pipeline must never leak Protected Health Information into logs, and it must never halt a batch on a single unmapped code. Redact before logging, and return a structured result — never a bare exception — so a miss becomes a routing decision.
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class CrosswalkResult:
is_valid: bool
mapped_cpt: Optional[str]
payer_rule: Optional[RulePayload]
error_code: Optional[str] = None
masked_log_msg: Optional[str] = None
def redact_phi(raw_text: str) -> str:
"""Strip SSN, MRN, and DOB patterns before anything reaches a log sink."""
patterns = [
r'\b\d{3}-\d{2}-\d{4}\b', # SSN
r'\bMRN[:\s]*\w{6,12}\b', # MRN
r'\b\d{2}/\d{2}/\d{4}\b', # DOB
]
sanitized = raw_text
for pat in patterns:
sanitized = re.sub(pat, '[REDACTED]', sanitized)
return sanitized
def resolve_mapping(icd10: str, cpt: str, payer_id: str) -> CrosswalkResult:
"""Lookup with explicit fallback routing and PHI-masked logging — never raises to the caller."""
try:
payer_rules = CROSSWALK.get(icd10, {}).get(cpt, {})
if payer_id in payer_rules:
return CrosswalkResult(is_valid=True, mapped_cpt=cpt, payer_rule=payer_rules[payer_id])
# Fallback routing for invalid / unmapped codes — a miss is a decision, not a crash
logging.warning(
redact_phi(f"Crosswalk miss: ICD10={icd10}, CPT={cpt}, Payer={payer_id}")
)
return CrosswalkResult(
is_valid=False, mapped_cpt=None, payer_rule=None,
error_code="NO_MAPPING_FOUND",
masked_log_msg="Routed to payer fallback queue",
)
except Exception as exc:
logging.error(redact_phi(f"Crosswalk resolution failure: {exc}"))
return CrosswalkResult(
is_valid=False, mapped_cpt=None, payer_rule=None,
error_code="INTERNAL_LOOKUP_ERROR",
masked_log_msg="Escalated to exception handler",
)
Step 4 — Emit the 837P SV1 payload
Wrap the resolver in an engine that enforces the payer’s NCCI-driven modifier requirements and returns an EDI-ready dict for SV1 construction. The lock guards the shared crosswalk and validation cache when workers share the structure.
import threading
from typing import List
class CrosswalkEngine:
def __init__(self) -> None:
self._lock = threading.RLock()
self._validation_cache: Dict[str, bool] = {}
def validate_and_map(
self,
icd10: str,
procedure_code: str,
payer_id: str,
modifiers: Optional[List[str]] = None,
) -> dict:
"""
Validate the ICD-10 / CPT pairing and return an EDI-ready payload for
837P SV1 construction. Diagnosis pointer indices (SV107-SV110) are
populated separately from the claim's HI ordering, not here.
"""
with self._lock:
result = resolve_mapping(icd10, procedure_code, payer_id)
if not result.is_valid:
return {"status": "FALLBACK", "error": result.error_code, "log": result.masked_log_msg}
rule = result.payer_rule
# Enforce NCCI modifier requirements before the line is accepted
required_mods = rule.get("required_modifiers", []) if rule else []
if modifiers and required_mods and not set(required_mods).issubset(set(modifiers)):
return {
"status": "DENY",
"error": "MODIFIER_MISMATCH",
"log": redact_phi(f"Missing modifiers {required_mods} for {procedure_code}"),
}
# Return EDI-compatible data for the SV101 composite
return {
"status": "VALID",
"sv101_qualifier": "HC",
"sv101_code": procedure_code,
"sv1_modifiers": modifiers or [],
"payer_override": rule.get("override_flag", False) if rule else False,
"effective_date": rule.get("effective_date", "2024-01-01") if rule else None,
}
Verification
Confirm the resolver behaves correctly before wiring it into service-line assembly:
- Known pairing — load a record via
load_crosswalk_chunkand callvalidate_and_mapwith its ICD-10, CPT, and payer; expect"status": "VALID"and"sv101_code"equal to the procedure, with noWARNINGline. - Unmapped pairing — call with a fabricated CPT; expect a single
Crosswalk missWARNING,"status": "FALLBACK", and"error": "NO_MAPPING_FOUND". - Modifier gate — supply a rule with
required_modifiersand omit one; expect"status": "DENY"and"error": "MODIFIER_MISMATCH", with the missing-modifier log already PHI-masked. - No PHI leak — grep the log stream for any SSN, MRN, or DOB pattern from your test fixtures; it must never appear — only
[REDACTED]should be present. - Downstream acks — a
VALIDpayload that builds a well-formedSV101should return a clean999(accepted) and277CA(accepted-for-processing); aFALLBACKline should never have been transmitted, so no reject is generated.
Common Gotchas
- Uninterned keys cause silent misses. If any code reaches
resolve_mappingwithout passing through thesys.intern()ingestion path, an equal-but-not-identical string still hashes correctly, so this is not usually the miss source — but a stale cache after a payer-table update is: invalidate the validation cache whenever you reload a crosswalk slice, or aKeyError-adjacent miss will look like a bad code. - The CPT does not carry the diagnosis pointer. A frequent 837P
SV1rejection comes from developers packing pointer indices next to the code; populateSV107–SV110with 1-basedHI-segment indices instead, exactly as the X12 837P segment architecture guide specifies, and keep the composite inSV101limited toHC:<CPT>. - HCPCS Level II codes are alphanumeric. DMEPOS and supply lines carry a leading alpha character (e.g.
E0114), so never coerce the second-level key toint; the same string handling that the HCPCS Level II integration patterns page uses applies here. - Effective dating drifts against CMS quarterly releases. A pairing valid last quarter can be retired this quarter; pin every rule payload to an
effective_dateand re-run regression tests against each CMS code-set release so a retired pairing fails in CI, not in the payer’s front-end edits — and reconcile any resulting adjustment against the X12 835 remittance structure so a denied line maps back to the ledger.
Related
- ICD-10-CM to CPT Crosswalk Mapping — the medical-necessity and NCCI context this dictionary implements.
- Building a CPT Modifier Validation Matrix — the modifier-enforcement structure
validate_and_mapdelegates to. - X12 837P Segment Architecture Guide — where the
SV1service line andHIdiagnosis pointers live.