Configuring Age and Sex-Specific Reference Ranges in LIMS
Problem Statement
A single flat reference interval per analyte is wrong for most patients: a serum alkaline phosphatase that is normal for a 9-year-old is a critical flag for a 40-year-old, and creatinine, hemoglobin, and testosterone intervals all split by biological sex. When the LIMS resolves the wrong cohort — because an age boundary is off by one day, because sex arrives as unknown, or because a leap-day birthdate is subtracted with timezone-naïve arithmetic — the Reference Range Check Implementation classifies against an interval that never applied to this patient, producing false criticals or, worse, silently auto-verifying a dangerous value. This page implements deterministic demographic interval resolution as a pure pre-classification step, with the exact Pydantic v2 models, boundary arithmetic, and audit records an inspector can replay years later.
Prerequisites
Before wiring this into the validation path, confirm the following baseline:
- Runtime: Python 3.11+ (for
datetimeUTC helpers andzoneinfo),pydantic>=2.6,pytest>=8for the verification fixtures. - Canonical input: results already normalized upstream — units harmonized to UCUM and analyte identity resolved to LOINC through the Test Code Taxonomy & Standards service — so this stage keys intervals on a stable
(loinc, specimen, method_version)partition rather than an instrument-local test code. - Interval store: a versioned configuration table populated by Threshold Tuning & Calibration; this page consumes those rows, it does not derive bounds.
- Regulatory baseline: reference intervals established or verified per CLSI EP28-A3c, and an append-only audit sink capable of the retention windows CLIA §493.1105 requires.
Step-by-Step Implementation
Step 1: Model the cohort interval with validation at ingress
Model each interval as a Pydantic v2 CohortInterval so that a malformed row — a negative age, an inverted bound, a non-enumerated sex — fails at load time rather than at the bedside. Age boundaries are stored in days for neonatal and pediatric precision, using inclusive-exclusive semantics [age_low_days, age_high_days) to eliminate off-by-one cohort collisions. Biological sex maps to the HL7 FHIR administrative-gender value set (male, female, other, unknown), with an explicit ALL sentinel for sex-independent analytes.
from __future__ import annotations
import datetime as dt
from enum import Enum
from pydantic import BaseModel, Field, model_validator
class Sex(str, Enum):
male = "male"
female = "female"
other = "other"
unknown = "unknown"
all = "ALL" # sex-independent analyte or explicit fallback cohort
class CohortInterval(BaseModel):
analyte_loinc: str
method_version: str
age_low_days: int = Field(ge=0)
age_high_days: int | None = None # None => open-ended upper bound
sex: Sex
lower: float
upper: float
critical_low: float | None = None
critical_high: float | None = None
priority: int = 0 # higher wins; e.g. gestational cohort > chronological
version: str
@model_validator(mode="after")
def _check_bounds(self) -> "CohortInterval":
if self.age_high_days is not None and self.age_high_days <= self.age_low_days:
raise ValueError(
f"{self.analyte_loinc}: age range [{self.age_low_days}, "
f"{self.age_high_days}) is empty or inverted"
)
if self.upper <= self.lower:
raise ValueError(
f"{self.analyte_loinc}: reference bound upper<=lower"
)
return self
Step 2: Derive the demographic key deterministically
Age must be computed the same way every time or two runs of the same specimen can land in different cohorts. Convert birth_date and collection_date to UTC dates before subtraction, and reject collections that precede birth rather than emitting a negative age. Because both operands are calendar dates, (collection - birth).days is leap-year correct: a patient born on Feb 29 crosses each birthday on Mar 1 in non-leap years without special casing.
def age_in_days(birth_date: dt.date, collection_date: dt.date) -> int:
"""Deterministic, leap-year-safe age in whole days at collection."""
if collection_date < birth_date:
raise ValueError("collection_date precedes birth_date")
return (collection_date - birth_date).days
def normalize_sex(raw: str | None) -> Sex:
"""Map an inbound demographic value to the administrative-gender enum.
Missing or unrecognized values become `unknown` — never a silent
default to male/female, which would fabricate a cohort assignment.
"""
if not raw:
return Sex.unknown
try:
return Sex(raw.strip().lower())
except ValueError:
return Sex.unknown
Step 3: Resolve the interval with a priority-ordered async lookup
The resolver reads candidate cohorts for the partition from the versioned interval store — an async call, since the store is out-of-process — then selects the first match by descending priority. Priority is what lets a gestational-age or pregnancy cohort deterministically override the chronological-age cohort when both would match. A sex='ALL' cohort is the only permitted fallback, and applying it sets fallback_applied so the decision is never invisible.
from typing import Protocol
class IntervalStore(Protocol):
async def fetch(
self, loinc: str, specimen: str, method_version: str
) -> list[CohortInterval]: ...
class ReferenceRangeResolver:
def __init__(self, store: IntervalStore) -> None:
self._store = store
async def resolve(
self,
*,
loinc: str,
specimen: str,
method_version: str,
age_days: int,
sex: Sex,
) -> tuple[CohortInterval | None, bool]:
cohorts = await self._store.fetch(loinc, specimen, method_version)
ordered = sorted(cohorts, key=lambda c: c.priority, reverse=True)
for c in ordered:
upper = c.age_high_days if c.age_high_days is not None else float("inf")
if c.age_low_days <= age_days < upper and c.sex in (sex, Sex.all):
return c, False # exact demographic match, no fallback
fallback = next((c for c in ordered if c.sex is Sex.all), None)
return fallback, fallback is not None
Step 4: Emit an immutable audit record for the selection
Every resolution — matched or unmatched — must produce a replayable record. The RangeResolutionAudit model captures the inputs that drove the choice, the selected interval’s version, and whether a fallback fired, so the interval that was live at collection time is reconstructable independent of later tuning. Round-trippable JSON is what makes it defensible.
class RangeResolutionAudit(BaseModel):
trace_id: str
analyte_loinc: str
specimen: str
method_version: str
age_days: int
sex: Sex
matched_version: str | None
lower: float | None
upper: float | None
fallback_applied: bool
resolved_at: dt.datetime = Field(
default_factory=lambda: dt.datetime.now(dt.timezone.utc)
)
def build_audit(
*, trace_id: str, specimen: str, age_days: int, sex: Sex,
interval: CohortInterval | None, loinc: str, method_version: str,
fallback_applied: bool,
) -> RangeResolutionAudit:
return RangeResolutionAudit(
trace_id=trace_id,
analyte_loinc=loinc,
specimen=specimen,
method_version=method_version,
age_days=age_days,
sex=sex,
matched_version=interval.version if interval else None,
lower=interval.lower if interval else None,
upper=interval.upper if interval else None,
fallback_applied=fallback_applied,
)
Verification & Testing
Prove the boundary semantics with pytest, because the off-by-one at a cohort edge is exactly where a wrong interval hides. A patient who is exactly age_high_days old must land in the next cohort, not the current one. Round-trip the audit model to guarantee the record written to the sink deserializes back identically.
import pytest
class StubStore:
def __init__(self, rows: list[CohortInterval]) -> None:
self._rows = rows
async def fetch(self, loinc, specimen, method_version):
return self._rows
PED = CohortInterval(analyte_loinc="2324-2", method_version="v3",
age_low_days=0, age_high_days=1825, sex=Sex.all,
lower=145, upper=420, priority=0, version="glu-ped-1")
ADULT = CohortInterval(analyte_loinc="2324-2", method_version="v3",
age_low_days=1825, age_high_days=None, sex=Sex.all,
lower=110, upper=240, priority=0, version="glu-adt-1")
@pytest.mark.asyncio
async def test_upper_boundary_is_exclusive():
resolver = ReferenceRangeResolver(StubStore([PED, ADULT]))
interval, fb = await resolver.resolve(
loinc="2324-2", specimen="serum", method_version="v3",
age_days=1825, sex=Sex.male,
)
assert interval.version == "glu-adt-1" # 1825 days -> adult, not pediatric
assert fb is False
def test_audit_round_trips():
audit = build_audit(trace_id="OBR-21-9001", specimen="serum",
age_days=1825, sex=Sex.male, interval=ADULT,
loinc="2324-2", method_version="v3",
fallback_applied=False)
assert RangeResolutionAudit.model_validate_json(
audit.model_dump_json()
) == audit
Expected: both tests pass. test_upper_boundary_is_exclusive confirms the [low, high) contract; test_audit_round_trips confirms the audit payload is byte-stable through model_dump_json() / model_validate_json(). When sex=Sex.unknown and only sex-specific cohorts exist, resolve returns (None, False) — the caller must convert that to a manual-review hold, verified by a third test asserting a None interval.
Compliance Note
CLIA §493.1253 requires that the laboratory establish and verify the reference intervals it reports against, and CAP checklist items GEN.41350 and the automated-verification requirements demand that demographic-specific intervals be documented and that auto-verification logic be reproducible on review. Storing each interval with a version and emitting a RangeResolutionAudit for every result satisfies the 21 CFR Part 11 expectation that an electronic decision be attributable and reconstructable; writing that audit to an append-only sink under the CLIA §493.1105 retention window makes the reconstruction available for the required years.
Troubleshooting
A patient exactly at a cohort boundary lands in the wrong interval.
Root cause: mixing inclusive-inclusive and inclusive-exclusive boundaries across cohorts, so age_high_days of one row equals age_low_days of the next and both match. Fix: enforce [age_low_days, age_high_days) everywhere and let the model validator reject any row where age_high_days <= age_low_days. The test_upper_boundary_is_exclusive fixture pins this behavior.
Leap-day birthdates resolve to the wrong age on non-leap years.
Root cause: age computed from a naïve datetime subtraction that includes time-of-day or a non-UTC offset, so a specimen drawn just after midnight local time rolls a day. Fix: reduce both operands to UTC date objects before subtracting, as age_in_days does — (collection - birth).days is calendar-correct and a Feb 29 birth crosses on Mar 1 without special casing.
Results with unknown or missing sex crash or silently default.
Root cause: casting a missing sex directly to the enum raises, or a try/except swallows it into male. Fix: route through normalize_sex, which maps anything unrecognized to Sex.unknown; the resolver then matches only ALL cohorts and, absent one, returns None so the caller raises a manual-review hold instead of fabricating a demographic assignment.
Two overlapping cohorts match and the wrong one wins nondeterministically.
Root cause: cohorts overlap by design (a pregnancy or gestational-age cohort inside a chronological-age band) but no ordering is applied, so dictionary/query order decides. Fix: assign explicit integer priority and sort descending before the scan; the higher-priority clinical cohort deterministically overrides the chronological one.
Bounds look right but every result flags after an instrument swap.
Root cause: the interval store returned rows for a different method_version, or units differ between the incoming result and the stored bound. Fix: key the fetch on (loinc, specimen, method_version) so a method change selects the correct interval set, and confirm both sides are UCUM-normalized before comparison — unit harmonization belongs upstream in ingestion, not in the resolver.
Related
- Reference Range Check Implementation — the classification node that consumes the resolved interval and emits the
NORMAL/LOW/HIGH/CRITICALverdict. - Handling out-of-range flags without manual intervention — how correctly-resolved cohorts feed automated hold/release routing.
- Implementing Delta Checks for Electrolyte Panels in Python — the longitudinal stage that runs after a value passes its demographic range.
- Threshold Tuning & Calibration — where the versioned intervals this page resolves are derived and promoted.
- Test Code Taxonomy & Standards — the LOINC identity that keys every cohort partition.
Part of: Reference Range Check Implementation.