Parsing lab values
Lab results in clinical text arrive as a value, a unit, and a reference range
("Sodium 132 mmol/L (135–145)"). To act on them you need a structured
abnormal flag — is 132 low, normal, high, or critical? OpenMed's
openmed.clinical lab helpers parse the reference range deterministically and
derive the flag, honoring any explicit flag the originating lab already supplied.
The helpers are unit-agnostic by design: they compare numbers within a stated
range and never convert units, so a mmol/L value is never silently compared
against a mg/dL range.
When to use
- After
extracting-clinical-entities surfaces lab/measurement entities and you
need to classify each as low / normal / high / critical.
- The user asks to parse reference ranges, flag abnormal labs, build a flagged
labs table, or interpret values like
<5, >=10, 0.5 - 1.2.
- You have an originating-lab flag (
H, L, C, HH) and want it honored over
a derived comparison.
Quick start
from openmed.clinical import (
parse_reference_range, derive_abnormal_flag, LAB_FLAG_ADVISORY,
)
# Closed range
rng = parse_reference_range("135-145")
# -> {"low": 135.0, "high": 145.0, "low_inclusive": True, "high_inclusive": True}
derive_abnormal_flag(132, rng) # "low"
derive_abnormal_flag(140, "135-145") # "normal" (raw range string accepted)
derive_abnormal_flag(150, "135 to 145") # "high"
# One-sided bounds
derive_abnormal_flag(7, parse_reference_range("<5")) # "high" (above the cap)
derive_abnormal_flag(3, parse_reference_range(">=10")) # "low"
# Honor the lab's own explicit flag (takes precedence over derived comparison)
derive_abnormal_flag(132, "135-145", explicit_flag="C") # "critical"
derive_abnormal_flag(132, "135-145", explicit_flag="HH") # "critical"
# Unparseable / non-numeric inputs fail safe rather than guessing
derive_abnormal_flag("pending", "135-145") # "unknown"
derive_abnormal_flag(132, "see report") # "unknown"
print(LAB_FLAG_ADVISORY) # surface this disclaimer with derived flags
AbnormalFlag is one of "low" | "normal" | "high" | "critical" | "unknown".
ReferenceRange is a typed mapping of low, high, low_inclusive,
high_inclusive.
Workflow
- Get value + range + (optional) lab flag from extracted lab entities. The
value should be numeric; the range may be a raw string or a parsed mapping.
- Parse the reference range with
parse_reference_range. It handles closed
ranges ("135-145", "0.5 - 1.2", "135 to 145", en/em dashes) and
one-sided bounds ("<5", "<=5", ">10", ">=10"). Contradictory or
unparseable ranges return empty bounds rather than a guess — by design.
- Derive the flag with
derive_abnormal_flag(value, range, explicit_flag=).
Resolution order: an explicit lab flag wins first (H/HIGH, L/LOW,
C/CRIT/CRITICAL, HH/LL → critical, N/NORMAL); an unknown explicit flag
returns "unknown" instead of being silently ignored. With no explicit flag,
it compares the numeric value against the parsed bounds, respecting inclusive
vs. exclusive edges.
- Handle
"unknown" explicitly. Non-numeric values, empty/unparseable
ranges, or unrecognized explicit flags yield "unknown". Treat it as
"needs review," not "normal."
- Attach the advisory. Surface
LAB_FLAG_ADVISORY wherever derived flags
are shown — derived flags are heuristic and do not replace the originating
laboratory's own diagnostic flagging.
Hand-off to / from OpenMed
- From
extracting-clinical-entities: analyze_text lab/measurement
entities give you the value text, unit, and often the reference range; this
skill turns them into structured flags. Parse the numeric value out of the
entity surface before calling derive_abnormal_flag.
- OpenMed calls:
from openmed.clinical import parse_reference_range, derive_abnormal_flag, ReferenceRange, AbnormalFlag, LAB_FLAG_ADVISORY.
- To
reconciling-problem-lists / FHIR grounding: a critical/high/low
flag becomes a FHIR Observation.interpretation code (HL7 v3
ObservationInterpretation: H, L, HH, LL, N). Ground the LOINC code and
UCUM unit out-of-process; OpenMed emits the flag, not the terminology binding.
Edge cases & gotchas
- Unit-agnostic — convert before comparing. The helpers ignore units
entirely. If the value and the range are in different units (mg/dL vs mmol/L),
the flag is wrong. Normalize units before calling, or only compare value
and range that share a unit.
- Inclusive vs. exclusive edges.
"<5" makes 5 the high bound exclusive;
a value of exactly 5 flags high. parse_reference_range records
high_inclusive=False for < and True for <= — respect it.
- Critical needs an explicit flag. Derived comparison yields only low/normal/
high.
"critical" comes from the lab's explicit flag (C, HH, LL); the
helpers do not infer critical thresholds beyond the reference range.
- Empty bounds are intentional. A range with both bounds missing returns
"unknown" from derive_abnormal_flag, not "normal". Don't treat unknown as
in-range.
- Local-first, advisory-only. Runs on-device; flags are decision support, not
a diagnosis. Always carry
LAB_FLAG_ADVISORY.
Standards & references
1---2name: parsing-lab-values3description: Parse laboratory values and reference ranges from clinical text and flag results as low, normal, high, or critical with OpenMed. Use when the user needs to interpret lab results, compute abnormal flags, parse reference ranges like "135-145" or "<5", honor an originating-lab flag (H/L/critical), or turn extracted lab entities into structured high/low/critical signals. Covers openmed.clinical.parse_reference_range, derive_abnormal_flag, ReferenceRange, and AbnormalFlag, with UCUM/LOINC framing. Unit-agnostic — it does not convert units. Pairs after extracting-clinical-entities (lab entities from analyze_text).4license: Apache-2.05---67# Parsing lab values89Lab results in clinical text arrive as a value, a unit, and a reference range10("Sodium 132 mmol/L (135–145)"). To act on them you need a structured11**abnormal flag** — is 132 low, normal, high, or critical? OpenMed's12`openmed.clinical` lab helpers parse the reference range deterministically and13derive the flag, honoring any explicit flag the originating lab already supplied.14The helpers are **unit-agnostic by design**: they compare numbers within a stated15range and never convert units, so a mmol/L value is never silently compared16against a mg/dL range.1718## When to use1920- After `extracting-clinical-entities` surfaces lab/measurement entities and you21 need to classify each as low / normal / high / critical.22- The user asks to parse reference ranges, flag abnormal labs, build a flagged23 labs table, or interpret values like `<5`, `>=10`, `0.5 - 1.2`.24- You have an originating-lab flag (`H`, `L`, `C`, `HH`) and want it honored over25 a derived comparison.2627## Quick start2829```python30from openmed.clinical import (31 parse_reference_range, derive_abnormal_flag, LAB_FLAG_ADVISORY,32)3334# Closed range35rng = parse_reference_range("135-145")36# -> {"low": 135.0, "high": 145.0, "low_inclusive": True, "high_inclusive": True}3738derive_abnormal_flag(132, rng) # "low"39derive_abnormal_flag(140, "135-145") # "normal" (raw range string accepted)40derive_abnormal_flag(150, "135 to 145") # "high"4142# One-sided bounds43derive_abnormal_flag(7, parse_reference_range("<5")) # "high" (above the cap)44derive_abnormal_flag(3, parse_reference_range(">=10")) # "low"4546# Honor the lab's own explicit flag (takes precedence over derived comparison)47derive_abnormal_flag(132, "135-145", explicit_flag="C") # "critical"48derive_abnormal_flag(132, "135-145", explicit_flag="HH") # "critical"4950# Unparseable / non-numeric inputs fail safe rather than guessing51derive_abnormal_flag("pending", "135-145") # "unknown"52derive_abnormal_flag(132, "see report") # "unknown"5354print(LAB_FLAG_ADVISORY) # surface this disclaimer with derived flags55```5657`AbnormalFlag` is one of `"low" | "normal" | "high" | "critical" | "unknown"`.58`ReferenceRange` is a typed mapping of `low`, `high`, `low_inclusive`,59`high_inclusive`.6061## Workflow62631. **Get value + range + (optional) lab flag** from extracted lab entities. The64 value should be numeric; the range may be a raw string or a parsed mapping.652. **Parse the reference range** with `parse_reference_range`. It handles closed66 ranges (`"135-145"`, `"0.5 - 1.2"`, `"135 to 145"`, en/em dashes) and67 one-sided bounds (`"<5"`, `"<=5"`, `">10"`, `">=10"`). Contradictory or68 unparseable ranges return **empty bounds** rather than a guess — by design.693. **Derive the flag** with `derive_abnormal_flag(value, range, explicit_flag=)`.70 Resolution order: an explicit lab flag wins first (`H/HIGH`, `L/LOW`,71 `C/CRIT/CRITICAL`, `HH/LL` → critical, `N/NORMAL`); an *unknown* explicit flag72 returns `"unknown"` instead of being silently ignored. With no explicit flag,73 it compares the numeric value against the parsed bounds, respecting inclusive74 vs. exclusive edges.754. **Handle `"unknown"` explicitly.** Non-numeric values, empty/unparseable76 ranges, or unrecognized explicit flags yield `"unknown"`. Treat it as77 "needs review," not "normal."785. **Attach the advisory.** Surface `LAB_FLAG_ADVISORY` wherever derived flags79 are shown — derived flags are heuristic and do **not** replace the originating80 laboratory's own diagnostic flagging.8182## Hand-off to / from OpenMed8384- **From** `extracting-clinical-entities`: `analyze_text` lab/measurement85 entities give you the value text, unit, and often the reference range; this86 skill turns them into structured flags. Parse the numeric value out of the87 entity surface before calling `derive_abnormal_flag`.88- **OpenMed calls:** `from openmed.clinical import parse_reference_range,89 derive_abnormal_flag, ReferenceRange, AbnormalFlag, LAB_FLAG_ADVISORY`.90- **To** `reconciling-problem-lists` / FHIR grounding: a `critical`/`high`/`low`91 flag becomes a FHIR `Observation.interpretation` code (HL7 v392 ObservationInterpretation: `H`, `L`, `HH`, `LL`, `N`). Ground the LOINC code and93 UCUM unit out-of-process; OpenMed emits the flag, not the terminology binding.9495## Edge cases & gotchas9697- **Unit-agnostic — convert before comparing.** The helpers ignore units98 entirely. If the value and the range are in different units (mg/dL vs mmol/L),99 the flag is wrong. Normalize units **before** calling, or only compare value100 and range that share a unit.101- **Inclusive vs. exclusive edges.** `"<5"` makes 5 the high bound *exclusive*;102 a value of exactly 5 flags `high`. `parse_reference_range` records103 `high_inclusive=False` for `<` and `True` for `<=` — respect it.104- **Critical needs an explicit flag.** Derived comparison yields only low/normal/105 high. `"critical"` comes from the lab's explicit flag (`C`, `HH`, `LL`); the106 helpers do not infer critical thresholds beyond the reference range.107- **Empty bounds are intentional.** A range with both bounds missing returns108 `"unknown"` from `derive_abnormal_flag`, not `"normal"`. Don't treat unknown as109 in-range.110- **Local-first, advisory-only.** Runs on-device; flags are decision support, not111 a diagnosis. Always carry `LAB_FLAG_ADVISORY`.112113## Standards & references114115- LOINC — universal codes for laboratory observations:116 https://loinc.org/117- UCUM — Unified Code for Units of Measure:118 https://ucum.org/119- HL7 v3 ObservationInterpretation (H/L/HH/LL/N abnormal flags):120 https://terminology.hl7.org/CodeSystem-v3-ObservationInterpretation.html121- HL7 FHIR R4 Observation — `referenceRange` and `interpretation`:122 https://hl7.org/fhir/R4/observation.html