CoolFace
Apppublic

Samvickid/pubmed-ehr

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
clinical_normalize.py122 linesDownload Raw Back to root
1"""2clinical_normalize.py — Patient-record consistency guard.3 4WHY THIS EXISTS5---------------6The canonical clinical truth for a patient is the `condition` label (a7semicolon-separated diagnosis list that also drives the risk tier and the8clinical narrative). Historically the structured `active_conditions` /9`diagnoses` lists were generated *independently* of that label, so 67% of10patients ended up with structured diagnoses that CONTRADICTED their own11condition string (e.g. condition "Obstructive sleep apnea; IBS; ..." but12active_conditions "Benign skin lesion"). RISKA reads the EHR record to explain13a risk decision, so it then cited diseases the patient never had.14 15This module reconciles a record so the structured diagnoses are always DERIVED16from the canonical `condition` label. It is:17 18  * clinic-agnostic  — works on any record shape, no PMC hardcoding;19  * idempotent       — running it twice yields the same result;20  * non-destructive when there is nothing to reconcile (a digitized clinic with21    no `condition` label keeps its own structured conditions).22 23Apply it at the moment a record is served/scored so EVERY current and FUTURE24clinic is corrected automatically — the correction lives in code, not in a25one-off data patch.26"""27 28import re29 30# Best-effort severity inference from the condition wording. Order matters:31# the first bucket whose keyword is present wins.32_SEVERE = (33    "failure", "malignant", "cancer", "carcinoma", "metastatic", "metastasis",34    "crisis", "acute", "severe", "sepsis", "septic", "shock", "infarction",35    "hemorrhage", "haemorrhage", "stage iv", "stage 4", "stage iii", "multi-organ",36    "multiorgan", "end-stage", "end stage", "embolism", "decompensated",37)38_MILD = (39    "mild", "benign", "controlled", "seasonal", "refractive", "deficiency",40    "osteopenia", "borderline", "pre-", "prediabet", "early", "minor",41    "intermittent", "remission",42)43 44# Lightweight ICD-ish codes for the most common labels. Unknown → "" (we do NOT45# fabricate a specific code we cannot stand behind).46_CODE = {47    "hypothyroidism": "E03.9", "hyperlipidemia": "E78.5",48    "essential hypertension": "I10", "hypertension": "I10",49    "type 2 diabetes mellitus": "E11.9", "type 2 diabetes": "E11.9",50    "obstructive sleep apnea": "G47.33", "asthma": "J45.909",51    "irritable bowel syndrome": "K58.9", "gout": "M10.9",52    "depressive disorder": "F32.9", "depression": "F32.9",53    "anxiety disorder": "F41.9", "seasonal allergies": "J30.2",54    "chronic kidney disease stage 2": "N18.2", "chronic kidney disease": "N18.9",55    "congestive heart failure": "I50.9", "rheumatoid arthritis": "M06.9",56    "osteoarthritis": "M19.90", "chronic sinusitis": "J32.9",57    "vitamin d deficiency": "E55.9", "mild anemia": "D64.9", "anemia": "D64.9",58    "systemic lupus erythematosus": "M32.9", "pulmonary hypertension": "I27.20",59    "benign skin lesion": "D23.9", "gerd": "K21.9",60    "chronic obstructive pulmonary disease": "J44.9", "copd": "J44.9",61}62 63 64def severity_for(name: str) -> str:65    n = (name or "").lower()66    for kw in _SEVERE:67        if kw in n:68            return "severe"69    for kw in _MILD:70        if kw in n:71            return "mild"72    return "moderate"73 74 75def code_for(name: str) -> str:76    return _CODE.get((name or "").strip().lower(), "")77 78 79def parse_condition_label(condition: str):80    """Split a canonical condition label into individual diagnosis names."""81    if not condition:82        return []83    # Primary separator is ';'. Fall back to ',' only when there are no ';'.84    parts = condition.split(";") if ";" in condition else condition.split(",")85    seen, out = set(), []86    for raw in parts:87        name = raw.strip().strip(".")88        if not name or name.upper() == "N/A":89            continue90        key = name.lower()91        if key in seen:92            continue93        seen.add(key)94        out.append(name)95    return out96 97 98def conditions_from_label(condition: str):99    """Structured active_conditions/diagnoses derived from the canonical label."""100    return [101        {"name": name, "code": code_for(name), "severity": severity_for(name)}102        for name in parse_condition_label(condition)103    ]104 105 106def reconcile_record(rec: dict) -> dict:107    """Return `rec` with structured diagnoses made consistent with `condition`.108 109    If the record carries a canonical `condition` label, its `active_conditions`110    and `diagnoses` are rebuilt from that label so they can never contradict it.111    Records WITHOUT a usable label (e.g. freshly digitized paper notes) are left112    untouched. Mutates and returns the same dict for convenience.113    """114    if not isinstance(rec, dict):115        return rec116    condition = rec.get("condition")117    derived = conditions_from_label(condition) if condition else []118    if derived:  # only override when we have a canonical label to trust119        rec["active_conditions"] = derived120        rec["diagnoses"] = list(derived)121    return rec122