fix(concepts): complete KDIGO AKI staging with the RRT and urine-output legs - #212
Merged
Conversation
…ut legs
The acute_kidney_injury / aki_stage_2 / aki_stage_3 concepts were
creatinine-only. KDIGO stages AKI on three legs, and two were missing
entirely -- both flagged "not implemented -- see 'still open'" in the
concept docstrings. An incomplete gold standard is a correctness problem
wherever the label is used, and it is used twice: as the concept
bottleneck's supervised target and as the alert-event hazard head's onset
definition.
1. Renal replacement therapy is an automatic Stage 3, whatever creatinine
and urine output say. Added as one more OR-branch of aki_stage_3, not a
separate named concept: the clinical fact is "RRT implies Stage 3", not
a concept the bottleneck needs its own head for. The item ids are
mimic-code's own rrt.sql dialysis_active = 1 procedureevents set
(225441 IHD, 225802 CRRT, 225803 CVVHD, 225805 peritoneal, 225809
CVVHDF, 225955 SCUF) and its two deliberate exclusions are excluded
here too (224270 dialysis catheter placement, 225436 CRRT filter
change). Matched with the existing CodeOccurrenceRule machinery, whose
first-occurrence semantics are exactly what an onset needs, over an
alternation anchored to the PROCEDURE//START//{itemid} code shape that
SOFA_SOURCE_CONFIG's ventilation ids already use.
2. Urine output stages AKI independently of creatinine. New
sofa.urine_output_rate generalizes urine_output_24h to an arbitrary
trailing window and to KDIGO's own weight-normalized mL/kg/h form;
urine_output_24h is now its 24 h absolute-volume special case, so
SOFA's renal component and the oliguria concept are untouched. New
DerivedUrineRateRule / CanonicalUrineRate express the three stage
criteria declaratively, like every other rule in the file: Stage 1
< 0.5 mL/kg/h over 6 h, Stage 2 < 0.5 over 12 h, Stage 3 < 0.3 over
24 h OR 0 mL over 12 h (true anuria, absolute volume, so it needs no
weight at all).
Weight is the real constraint and is handled by abstaining, not guessing.
Only ~10-17% of subjects in the MIMIC-IV extraction have any weight
reading, so each window is asof-joined backward to the most recent daily
weight (224639) at or before it, falling back to admission weight
(226512) before any daily weight exists -- daily weight is the current
weight a mL/kg/h rate means, admission weight is the best available early
in a stay. A window with neither, or a non-positive charted weight, is
dropped rather than scored against an invented default: it stays
unobserved (masked out of supervision) instead of becoming a silent
negative, the same observability convention sepsis3 uses.
Thresholds follow the existing sign conventions: the rate criteria are
strict "below" because KDIGO says "less than 0.5 mL/kg/h" (exactly 0.5
does not stage), and anuria is "at_or_below" 0 mL, mirroring Stage 3's
existing at_or_above 4.0 creatinine trigger.
No experiment-registry row: this changes the label definition, so it only
gets one once something is retrained/rescored against it.
Tests: tests/odyssey/data/test_aki_kdigo.py, 29 cases -- each of the six
RRT item ids individually, both exclusions, pattern anchoring, first-
occurrence onsets, OR composition with the creatinine legs, mL/kg/h
arithmetic at all three windows, partial-window exclusion, the weight
fallback order (both weights / admission only / daily only / none / later
only / non-positive), the unobserved-not-negative contract, threshold
boundaries, the weight-free anuria branch, a mixed five-subject cohort,
the per-source expansion, and the source-config contract of the two
urine-output forms. Coverage on the two touched modules: concepts.py 98%,
sofa.py 97% (every uncovered line pre-existing and defensive).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #212 +/- ##
==========================================
+ Coverage 88.96% 89.03% +0.07%
==========================================
Files 57 57
Lines 8446 8491 +45
==========================================
+ Hits 7514 7560 +46
+ Misses 932 931 -1
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
The
acute_kidney_injury/aki_stage_2/aki_stage_3concepts were creatinine-only. KDIGO stages AKI on three legs; two of them were missing entirely, and both were flaggednot implemented -- see 'still open'in the concept docstrings. This label is used twice (concept-bottleneck target and alert-event hazard onset), so an incomplete gold standard is a correctness problem wherever it appears.1. Renal replacement therapy = automatic Stage 3. Added as one more OR-branch of
aki_stage_3, not as a new named concept: the clinical fact is "RRT initiation implies Stage 3", not a separate concept the bottleneck needs its own head for. Expressed with the existingCodeOccurrenceRulemachinery (its first-occurrence semantics are exactly an onset) over an alternation anchored to thePROCEDURE//START//{itemid}code shapeSOFA_SOURCE_CONFIG's ventilation ids already use, withobserved_families=("PROCEDURE",)so "procedures were charted and none was dialysis" is a real negative.2. Urine output stages AKI independently of creatinine. New
sofa.urine_output_rategeneralizesurine_output_24hto an arbitrary trailing window and, optionally, to KDIGO's own weight-normalized mL/kg/h form.urine_output_24his now its 24 h absolute-volume special case, so SOFA's renal component and theoliguriaconcept are behaviourally untouched (asserted by a test comparing the two frames). NewDerivedUrineRateRule/CanonicalUrineRateexpress the criteria declaratively, parallel toDerivedSofaSignalRule/CanonicalSofaSignal, so AKI's urine logic stays inside the same "OR of typed component rules" machinery as every other concept:acute_kidney_injury(Stage 1)aki_stage_2aki_stage_3KDIGO's Stage 1 range is 6-12 h; it collapses to "assessable from 6 h onward" for a single-instant rule, and Stage 2 is the >= 12 h escalation of the same rate.
mimic-code itemid verification
The dialysis itemids are mimic-code's own
mimic-iv/concepts/treatment/rrt.sqldialysis_active = 1set:225441(IHD),225802(CRRT),225803(CVVHD),225805(peritoneal),225809(CVVHDF),225955(SCUF). That query's two deliberate exclusions are excluded here too and have their own test:224270(Dialysis Catheter, line placement, not therapy) and225436(CRRT Filter Change, maintenance on an already-active circuit whose therapy start has its own row). Urine output is not hardcoded: it resolves throughprefixes_for_loinc("9187-6"), i.e. all nine already-mapped collection routes (Foley, void, condom cath, suprapubic, straight cath, both ureteral stents, both nephrostomies).The weight-coverage caveat
Weight coverage is genuinely poor: ~10-17% of subjects in the real MIMIC-IV extraction have any weight reading at all. The rate criteria therefore abstain rather than guess:
join_asof-backward to the most recent daily weight (LAB//224639//kg) at or before its end instant, falling back to admission weight (LAB//226512//kg) only where no daily weight has been charted yet -- daily weight is the current weight a mL/kg/h rate actually means, admission weight is the best available estimate early in a stay;sepsis3documents;The two weight itemids live in
SOFA_SOURCE_CONFIGrather than the LOINC layer on purpose: they carry the same body-weight LOINC, so a LOINC lookup could not tell daily from admission, and the fallback order needs exactly that distinction.Conventions chosen where the brief left them open
DerivedUrineRateRule(threshold, direction, window_hours, source, weight_normalized=True)plus canonicalCanonicalUrineRate, rather than wideningDerivedSofaSignalRule'ssignalliteral -- the window and the weight normalization are per-rule parameters, and the existingurine_24hsignal keeps its exact meaning for SOFA/oliguria.urine_output_rate(..., window_hours, weight_normalized)returning(key, time, value), the same three-column shapepf_ratio_readings/urine_output_24hreturn, so_derived_reading_idshandles both uniformly.window_hoursis not returned: the rule that asked already knows it.below(KDIGO says "less than 0.5 mL/kg/h", so exactly 0.5 does not stage), and anuria isat_or_below0 mL. Both mirror the existingat_or_aboveon Stage 3's creatinine >= 4.0.aki_stage_3_observednow also turns on for subjects with procedure data (the RRT leg is assessable for them) or with a weight-backed urine window, followinglabel_concepts' existing "observed if any leg was assessable" OR. This widens supervision slightly on subjects with no creatinine; it is the same trade-offon_vasopressorsalready makes.SOFA_SOURCE_CONFIG, so they are dropped on eICU/GEMINI likesepsis3's rule, and the concepts survive on their creatinine legs. Concept names and counts per task set are unchanged.Tests and coverage
tests/odyssey/data/test_aki_kdigo.py, 29 new cases in the existing synthetic-event-sequence style: each of the six RRT itemids individually (parametrized, so a broken alternation branch cannot hide), both exclusions, pattern anchoring (PROCEDURE//END//, a longer itemid, a suffixed segment), first-occurrence onsets, OR composition with the creatinine legs in both orders, mL/kg/h arithmetic at all three windows, multi-route summation, partial-window exclusion, the weight fallback order (both / admission only / daily only / none / later-only / non-positive), the unobserved-is-not-a-negative contract, threshold boundaries at exactly 0.5, the weight-free anuria branch, a mixed five-subject cohort exercising creatinine-only / urine-only / RRT-only / combined, and the per-source expansion.test_value_binning.py's rule-type exhaustiveness check was extended to name the two derived-signal rule types (their thresholds are on derived signals, so noCLINICAL_RANGESbin edge corresponds to them).ruff format/ruff check: clean.mypy odyssey: clean (tests are excluded from mypy by.pre-commit-config.yaml).typos: clean.pytest tests/odyssey/data --cov=odyssey.data.concepts --cov=odyssey.data.sofa): concepts.py 98%, sofa.py 97%, 423 passed / 1 skipped. Every uncovered line is a pre-existing defensiveraiseor an unrelated branch; no new code is uncovered.Known limitation carried over (flagged, not changed)
A trailing window is summed over whatever urine rows it contains, so sparse charting is indistinguishable from low output: a key charted once a day reads as oliguric on one row, and as anuric if that row is 0 mL. This is exactly the property the existing
oliguriaconcept and SOFA's renal component already have, so it is documented inurine_output_rate's docstring rather than fixed here -- a minimum-readings-per-window guard would change the existing concept's behaviour too and is a modelling decision worth making deliberately, not slipping into this PR.Not included
No
docs/experiments.mdrow: this is a label-definition correctness fix, and the registry only gets a row when something is actually retrained or rescored against the new label.README.md's "Known concept-rule limitations" paragraph is updated, since it claimed urine-output-based AKI staging was not implemented.🤖 Generated with Claude Code