Dimitris commited on
Commit
8959201
·
2 Parent(s): 3eb3fdc6686bd2

Merge pull request #9 from r0m4k/feat/marker-matching

Browse files
Files changed (2) hide show
  1. eval/data/real/labels_train.jsonl +6 -0
  2. 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
- # Fast lookup: any alias or canonical name (casefolded) -> Marker.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 resolve(name: str) -> Marker | None:
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