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 datetime UTC helpers and zoneinfo), pydantic>=2.6, pytest>=8 for 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.

python
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.

python
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.

python
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.

python
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,
    )
Demographic reference-interval resolution flow for one result A top-to-bottom flow for resolving one result. Result inputs (birth_date, collection_date, sex, and the loinc/specimen/method_version key) feed a derivation step that computes age_in_days with UTC-safe arithmetic and normalizes sex, mapping missing values to unknown. The derived key drives a priority-ordered cohort scan, which reads candidate cohorts from the versioned interval store on the right. The scan branches three ways: an exact demographic match returns the interval with no fallback; an ALL-sex fallback returns a cohort with fallback_applied true; and when only sex-specific cohorts exist the result gets no match and is held for manual review. All three branches converge to emit an immutable RangeResolutionAudit record. Result inputs birth_date · collection_date · sex (loinc, specimen, method_version) age_in_days · normalize_sex UTC-safe whole-day age missing sex → unknown (never a default) Priority-ordered cohort scan sort priority desc first match wins Versioned interval store (loinc, specimen, method) out-of-process · async candidate cohorts age∈band & sex✓ no exact cohort sex-specific only Exact match applied cohort interval → (interval, False) ALL-sex fallback fallback_applied = True → (cohort, True) No match → Hold manual review → (None, False) RangeResolutionAudit emitted immutable · versioned · replayable

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.

python
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.

Part of: Reference Range Check Implementation.