Codex commited on
Commit ·
6686bd2
1
Parent(s): 3eb3fdc
feat: robust marker-name matching + cleaned real training labels
Browse files- markers.resolve: normalize lab qualifiers/punctuation/word-order/British spelling and split slash/comma-joined names so real-report variants (Serum Sodium, Total WBC Count / TLC, Haemoglobin (Hb), PCV / Hematocrit, S. TSH, FBS) resolve. Raises true + measured extraction accuracy with no retraining and surfaces more interpretation cards.
- eval/data/real/labels_train.jsonl: cleaned canonical real labels for the fine-tune mix-in.
- eval/data/real/labels_train.jsonl +6 -0
- src/markers.py +47 -12
eval/data/real/labels_train.jsonl
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"image": "01_sterling_accuris.pdf", "tests": [{"marker": "Hemoglobin", "value": "14.5", "unit": "g/dL", "reference_range": "13.0 - 16.5", "status": "normal"}, {"marker": "Hematocrit", "value": "43.3", "unit": "%", "reference_range": "40 - 49", "status": "normal"}, {"marker": "MCV", "value": "90.3", "unit": "fL", "reference_range": "83 - 101", "status": "normal"}, {"marker": "Platelet Count", "value": "150000", "unit": "/cmm", "reference_range": "150000 - 410000", "status": "high"}], "notes": []}
|
| 2 |
+
{"image": "05_gribbles_cbm.pdf", "tests": [{"marker": "Sodium", "value": "141", "unit": "mmol/L", "reference_range": "135-145", "status": "normal"}, {"marker": "Potassium", "value": "4.1", "unit": "mmol/L", "reference_range": "(3.5-5.1)", "status": "normal"}, {"marker": "Chloride", "value": "99", "unit": "mmol/L", "reference_range": "(95-110)", "status": "normal"}, {"marker": "Creatinine", "value": "88", "unit": "umol/L", "reference_range": "(44-110)", "status": "high"}, {"marker": "eGFR", "value": "60", "unit": "mL/min/1.73m²", "reference_range": "> 90", "status": "low"}, {"marker": "AST", "value": "33", "unit": "U/L", "reference_range": "< 41", "status": "normal"}, {"marker": "ALT", "value": "25", "unit": "U/L", "reference_range": "< 51", "status": "normal"}, {"marker": "Glucose", "value": "5.6", "unit": "mmol/L", "reference_range": "(3.9 - 6.0)", "status": "low"}], "notes": []}
|
| 3 |
+
{"image": "08_investigation_scanned.pdf", "tests": [{"marker": "eGFR", "value": "115.5", "unit": "ml/min/1.73q.m", "reference_range": ">60.0", "status": "normal"}, {"marker": "Sodium", "value": "139", "unit": "mmol/L", "reference_range": "[136.0-145.0]", "status": "normal"}, {"marker": "Potassium", "value": "4.01", "unit": "mmol/L", "reference_range": "[3.50-5.10]", "status": "normal"}, {"marker": "Chloride", "value": "104.7", "unit": "mmol/L", "reference_range": "[98.0-107.0]", "status": "normal"}], "notes": []}
|
| 4 |
+
{"image": "09_pathkind_pl02.pdf", "tests": [{"marker": "Hemoglobin", "value": "12.6", "unit": "gm/dL", "reference_range": "12.0 - 15.0", "status": "low"}, {"marker": "White Blood Cell Count", "value": "5.4", "unit": "thou/μL", "reference_range": "4.0 - 10.0", "status": "normal"}, {"marker": "Red Blood Cell Count", "value": "4.1", "unit": "million/μL", "reference_range": "3.8 - 4.8", "status": "low"}, {"marker": "Hematocrit", "value": "36.8", "unit": "%", "reference_range": "36.0 - 46.0", "status": "low"}, {"marker": "MCV", "value": "94.1", "unit": "fL", "reference_range": "83.0 - 101.0", "status": "normal"}, {"marker": "Platelet Count", "value": "210", "unit": "thou/μL", "reference_range": "150 - 410", "status": "normal"}], "notes": []}
|
| 5 |
+
{"image": "15_gribbles_crp.pdf", "tests": [{"marker": "Glucose", "value": "5.5", "unit": "mmol/L", "reference_range": "3.9 - 6.0", "status": "low"}, {"marker": "HbA1c", "value": "5.1", "unit": "mmol/mol", "reference_range": "4 - 5.6", "status": "normal"}], "notes": []}
|
| 6 |
+
{"image": "16_zrt_female_hormones.pdf", "tests": [{"marker": "Free T4", "value": "1.4", "unit": "ng/dL", "reference_range": "0.7-2.5", "status": "normal"}, {"marker": "TSH", "value": "0.2", "unit": "mU/L", "reference_range": "0.5-3.0", "status": "low"}], "notes": []}
|
src/markers.py
CHANGED
|
@@ -51,12 +51,12 @@ MARKERS: tuple[Marker, ...] = (
|
|
| 51 |
# --- Complete blood count ---
|
| 52 |
Marker("Hemoglobin", "g/dL", 13.5, 17.5, "CBC", "oxygen-carrying protein in red blood cells", ("Hgb", "HGB", "Hb")),
|
| 53 |
Marker("Hematocrit", "%", 38.8, 50.0, "CBC", "fraction of blood made up of red cells", ("Hct", "HCT", "PCV")),
|
| 54 |
-
Marker("White Blood Cell Count", "10^3/uL", 4.5, 11.0, "CBC", "immune cells that fight infection", ("WBC", "Leukocytes", "WBC Count")),
|
| 55 |
Marker("Platelet Count", "10^3/uL", 150, 400, "CBC", "cell fragments that help blood clot", ("Platelets", "PLT")),
|
| 56 |
Marker("Red Blood Cell Count", "10^6/uL", 4.5, 5.9, "CBC", "number of oxygen-carrying red cells", ("RBC", "Erythrocytes")),
|
| 57 |
Marker("MCV", "fL", 80, 100, "CBC", "average size of red blood cells", ("Mean Corpuscular Volume",)),
|
| 58 |
# --- Metabolic panel ---
|
| 59 |
-
Marker("Glucose", "mg/dL", 70, 99, "Metabolic", "blood sugar level", ("Fasting Glucose", "GLU", "Blood Sugar")),
|
| 60 |
Marker("Creatinine", "mg/dL", 0.7, 1.3, "Metabolic", "kidney-function waste product", ("Cr", "Serum Creatinine")),
|
| 61 |
Marker("eGFR", "mL/min/1.73m2", 90, None, "Metabolic", "estimated kidney filtration rate", ("GFR", "Estimated GFR")),
|
| 62 |
Marker("Blood Urea Nitrogen", "mg/dL", 7, 20, "Metabolic", "kidney-function waste product", ("BUN", "Urea Nitrogen")),
|
|
@@ -88,24 +88,40 @@ MARKERS: tuple[Marker, ...] = (
|
|
| 88 |
)
|
| 89 |
|
| 90 |
|
| 91 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
_LOOKUP: dict[str, Marker] = {}
|
|
|
|
| 93 |
for _m in MARKERS:
|
| 94 |
_LOOKUP[_m.name.casefold()] = _m
|
|
|
|
| 95 |
for _a in _m.aliases:
|
| 96 |
_LOOKUP.setdefault(_a.casefold(), _m)
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
-
def
|
| 100 |
-
"""Match an extracted marker name (canonical or alias) to a known Marker.
|
| 101 |
-
|
| 102 |
-
Real reports print verbose names like "Packed Cell Volume (PCV)" or "Hemoglobin (HB/Hgb)".
|
| 103 |
-
We try the exact name, then the text outside the parentheses, then the abbreviation inside,
|
| 104 |
-
so both the canonical form and the lab's variant resolve to the same marker.
|
| 105 |
-
"""
|
| 106 |
-
if not name:
|
| 107 |
-
return None
|
| 108 |
key = name.strip().casefold()
|
|
|
|
|
|
|
| 109 |
if key in _LOOKUP:
|
| 110 |
return _LOOKUP[key]
|
| 111 |
m = re.search(r"\(([^)]*)\)", key)
|
|
@@ -115,4 +131,23 @@ def resolve(name: str) -> Marker | None:
|
|
| 115 |
for cand in (outer, inner):
|
| 116 |
if cand in _LOOKUP:
|
| 117 |
return _LOOKUP[cand]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
return None
|
|
|
|
| 51 |
# --- Complete blood count ---
|
| 52 |
Marker("Hemoglobin", "g/dL", 13.5, 17.5, "CBC", "oxygen-carrying protein in red blood cells", ("Hgb", "HGB", "Hb")),
|
| 53 |
Marker("Hematocrit", "%", 38.8, 50.0, "CBC", "fraction of blood made up of red cells", ("Hct", "HCT", "PCV")),
|
| 54 |
+
Marker("White Blood Cell Count", "10^3/uL", 4.5, 11.0, "CBC", "immune cells that fight infection", ("WBC", "Leukocytes", "WBC Count", "TLC", "Total Leucocyte Count")),
|
| 55 |
Marker("Platelet Count", "10^3/uL", 150, 400, "CBC", "cell fragments that help blood clot", ("Platelets", "PLT")),
|
| 56 |
Marker("Red Blood Cell Count", "10^6/uL", 4.5, 5.9, "CBC", "number of oxygen-carrying red cells", ("RBC", "Erythrocytes")),
|
| 57 |
Marker("MCV", "fL", 80, 100, "CBC", "average size of red blood cells", ("Mean Corpuscular Volume",)),
|
| 58 |
# --- Metabolic panel ---
|
| 59 |
+
Marker("Glucose", "mg/dL", 70, 99, "Metabolic", "blood sugar level", ("Fasting Glucose", "GLU", "Blood Sugar", "FBS", "RBS", "Fasting Blood Sugar")),
|
| 60 |
Marker("Creatinine", "mg/dL", 0.7, 1.3, "Metabolic", "kidney-function waste product", ("Cr", "Serum Creatinine")),
|
| 61 |
Marker("eGFR", "mL/min/1.73m2", 90, None, "Metabolic", "estimated kidney filtration rate", ("GFR", "Estimated GFR")),
|
| 62 |
Marker("Blood Urea Nitrogen", "mg/dL", 7, 20, "Metabolic", "kidney-function waste product", ("BUN", "Urea Nitrogen")),
|
|
|
|
| 88 |
)
|
| 89 |
|
| 90 |
|
| 91 |
+
# Lab qualifiers we strip when matching ("Serum Sodium" == "Sodium", "Total WBC Count" == "WBC").
|
| 92 |
+
_QUALIFIERS = frozenset((
|
| 93 |
+
"serum", "plasma", "blood", "total", "count", "counts", "level", "levels",
|
| 94 |
+
"estimation", "absolute", "fasting", "random", "s", "p", "the",
|
| 95 |
+
))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _normalize(name: str) -> str:
|
| 99 |
+
"""Collapse a printed marker name to a comparable core: drop parentheticals + punctuation,
|
| 100 |
+
normalise British spelling, remove lab qualifiers, and sort tokens (word order varies)."""
|
| 101 |
+
s = name.casefold().strip()
|
| 102 |
+
s = re.sub(r"\([^)]*\)", " ", s) # drop parentheticals
|
| 103 |
+
s = s.replace("haemo", "hemo").replace("haema", "hema") # British -> US
|
| 104 |
+
s = s.replace("leuco", "leuko").replace("oe", "e")
|
| 105 |
+
s = re.sub(r"[^a-z0-9 ]", " ", s) # punctuation -> space
|
| 106 |
+
tokens = sorted(t for t in s.split() if t and t not in _QUALIFIERS)
|
| 107 |
+
return " ".join(tokens)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# Fast lookups: exact (casefolded) and normalized.
|
| 111 |
_LOOKUP: dict[str, Marker] = {}
|
| 112 |
+
_NORM_LOOKUP: dict[str, Marker] = {}
|
| 113 |
for _m in MARKERS:
|
| 114 |
_LOOKUP[_m.name.casefold()] = _m
|
| 115 |
+
_NORM_LOOKUP.setdefault(_normalize(_m.name), _m)
|
| 116 |
for _a in _m.aliases:
|
| 117 |
_LOOKUP.setdefault(_a.casefold(), _m)
|
| 118 |
+
_NORM_LOOKUP.setdefault(_normalize(_a), _m)
|
| 119 |
|
| 120 |
|
| 121 |
+
def _resolve_one(name: str) -> Marker | None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
key = name.strip().casefold()
|
| 123 |
+
if not key:
|
| 124 |
+
return None
|
| 125 |
if key in _LOOKUP:
|
| 126 |
return _LOOKUP[key]
|
| 127 |
m = re.search(r"\(([^)]*)\)", key)
|
|
|
|
| 131 |
for cand in (outer, inner):
|
| 132 |
if cand in _LOOKUP:
|
| 133 |
return _LOOKUP[cand]
|
| 134 |
+
norm = _normalize(name)
|
| 135 |
+
if norm and norm in _NORM_LOOKUP:
|
| 136 |
+
return _NORM_LOOKUP[norm]
|
| 137 |
+
return None
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def resolve(name: str) -> Marker | None:
|
| 141 |
+
"""Match an extracted marker name (canonical/alias/variant) to a known Marker.
|
| 142 |
+
|
| 143 |
+
Handles real-report variety: exact name, the text inside/outside parentheses, a normalized
|
| 144 |
+
form that ignores lab qualifiers (Serum/Total/Count/…), punctuation, word order, and British
|
| 145 |
+
spelling, and slash/comma-joined names like "PCV / Hematocrit" or "Total WBC Count / TLC".
|
| 146 |
+
"""
|
| 147 |
+
if not name:
|
| 148 |
+
return None
|
| 149 |
+
for cand in [name, *re.split(r"[/,;|]", name)]:
|
| 150 |
+
marker = _resolve_one(cand)
|
| 151 |
+
if marker is not None:
|
| 152 |
+
return marker
|
| 153 |
return None
|