Skip to content

ML4H 2026 rebuttal: hazard-scored override, channel probes, event pins, GEMINI stages, comparison tools - #274

Merged
amrit110 merged 16 commits into
mainfrom
rebuttal/integration
Sep 29, 2026
Merged

amrit110 merged 16 commits into
mainfrom
rebuttal/integration

Conversation

@amrit110

Copy link
Copy Markdown
Member

What this adds

Everything built for the ML4H 2026 author response (reviews Oct 5, response Oct 5-12). Plan and banked results: docs/ml4h2026_rebuttal_plan.md, section "Results so far".

Inference and analysis

  • odyssey/inference/interventions.py: --hazard-heads scores the label override (none / truth / flip / random) on the per-event hazard heads at the alert protocol's landmark rows, with paired subject-clustered bootstraps of truth minus none, truth minus flip and flip minus none. Off by default; old outputs reproduce byte for byte.
  • scripts/probe_channel.py: fresh readouts on the concept probabilities, the poles and the full bottleneck (plus the CTL leakage probes), subject-split, with a saved feature bank and staged JSON writes.
  • scripts/compare_runs_paired.py: paired hazard AUROC deltas between two runs on identical landmark rows; refuses row-set or label mismatches.
  • scripts/make_hparams_table.py, scripts/cohort_counts.py, scripts/panel_coverage.py.

Fixes

  • Hazard events are now pinned to a checkpoint like concepts (run_pins.json written at training time, legacy table for the pre-Add eICU SOFA support: 26/29 -> 29/29 concept resolution #222 eICU runs). Loading eicu_full_v10 on main failed with a head-width mismatch because Sepsis-3 now resolves on eICU.
  • odyssey/inference/leakage.py: device-safe probes (bank on CPU, model on GPU).
  • scripts/alerts_cis.py: subject counts and a flat summary block.

GEMINI

  • Three scripts/gemini/run.sh steps: alerts-cis, panel-coverage, cohort-counts, each exporting aggregate JSON through the existing whitelist.

Results banked with this code (under research_journal/figure_data/, gitignored): override on hazards is inert on both databases; the forecast runs through the concept embeddings, not the probabilities (poles with k fixed recover everything, k alone recovers 45% on MIMIC-IV and 72% on eICU-CRD); the bottleneck costs under one AUROC point per cell like-for-like; RandInt at full scale gives no lever; GEMINI resolves 15 of the 48 panel signals.

Checks

Pre-commit hooks pass; uv run pytest tests: 1,503 passed, 18 skipped at the integration point (later commits added tests for the probe fix and the comparison script, all passing).

🤖 Generated with Claude Code

amrit110 and others added 14 commits September 25, 2026 06:56
The submission gives no model size or training settings. This script
renders them from each flagship run's banked config.json (MIMIC-IV
full_run_v10, eICU-CRD eicu_full_v10; GEMINI's config was never
exported and prints as such) and appends the GBM comparator's settings
read from the code itself: HistGradientBoostingClassifier, the four
GBM_GRID configurations, the 400-round budget with the best round
picked on a subject-grouped 10% split, and the 609-feature panel with
its 110 count features computed from feature_names/feature_groups.

Parameter counts are supplied through --params (run name -> integer)
and print as tbd until the VMs compute them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
interventions.py gains --hazard-heads: the same streaming pass reads
the per-event hazard heads at the alert protocol's landmark rows under
every mode and reports, per event and horizon, the AUROC against the
landmark outcome and the mean P(event within h), plus paired
subject-clustered bootstrap differences (truth minus none, truth minus
flip, flip minus none) on both, attached to the left mode's entry.

Landmark rows come from alerts.py's own mask and outcome rule
(position_visit_starts is factored out of collect_model_scores so both
callers share it), the AUROC bootstrap is uncertainty.bootstrap_auroc_delta
as in scripts/alerts_cis.py, and the mean-risk bootstrap draws whole
subjects in vectorised blocks. Only landmark rows are kept in memory.

Off by default: the JSON is byte for byte what it was. Tests cover the
unchanged schema, the row set against collect_model_scores, interval
coverage and collapse, and whole-subject resampling.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
scripts/probe_channel.py dumps, on a frozen mixture-bottleneck checkpoint,
the concept probabilities k, the unnamed slot's probability u, the poles
w+/w- and the unnamed embedding z_u at every scored held-out position
(seeded, shard-stratified subsample), then fits fresh linear (or --mlp)
readouts on a split by subject and scores exact next-event top-1 on the
held-out subjects: k_only, k_plus_u, z_named, poles_mean_k, poles_both,
h_bar, and the model's own head as the reference. Each carries a
subject-clustered bootstrap CI and a paired CI on the ratio to the
model's accuracy; the CTL leakage probes run on the same split.

ConceptBottleneck gains mixture_parts(), which forward now uses, so the
poles a probe reads are the poles the forward pass mixes.

Hazard heads at landmarks are not dumped; the docs paragraph says why,
gives the VM command lines and a runtime guess.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
… the ML4H rebuttal (WP5)

Three self-contained, idempotent steps that write only aggregate JSON
into scripts/gemini/out/evals/ through the existing key whitelist:

- alerts-cis <run>: scripts/alerts_cis.py on the run's
  alerts_rows_allshards.parquet (the current GBM refit; GEMINI_ALERTS_TAG
  defaults to _allshards), hazard vs gbm, 1000 paired draws. Prints the
  dump's row and subject counts first; GEMINI_CIS_MAX_SUBJECTS maps to
  --max-subjects. alerts_cis.py gains n_subjects per cell, the dump
  counts, and a flat per-cell summary block.
- panel-coverage <run>: new scripts/panel_coverage.py reports which of
  the 48 panel signals resolve on a source (GEMINI: 15 of 48, no
  non-invasive blood pressure) and, given metadata/codes.parquet, which
  resolved prefixes were charted. Names only.
- cohort-counts <run>: new scripts/cohort_counts.py over the shards a run
  used: subjects, admissions, hospitals from hadm_id_hospital.parquet,
  admission years, LOS, sex and age where charted, and per-event
  prevalence under the alerts leg's own onset definitions. Counts under
  10 leave as "<10". Works for any MEDS source dir.

Tests pin each step's whitelist to the script's output keys so a new
field refuses in CI rather than on the node (the #238 incident).
docs/ml4h2026_rebuttal_plan.md WP5 carries the three commands.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
load_run pinned a checkpoint's concept list but rebuilt its hazard heads
from today's alert registry. On eICU that registry gained sepsis3 with PR
#222 (SOFA source config), so eicu_full_v10 (80 = 5 events x 16 bins)
failed load_state_dict against a 96-row head, with sepsis3 spliced into
the middle of the list so even a width match would have misread rows.

- legacy_concept_pins: pinned_event_names next to pinned_concept_names,
  resolved from the run's own run_pins.json first, then LEGACY_EVENT_PINS
  by run name (eicu_full_v10, eicu_full_L_v10, eicu_full_ADD_v10,
  eicu_full_DEC_v10, eicu_full_DEC_v11, eicu_full_DEC_v12,
  eicu_full_DEC_v12_steer: the pre-#222 eICU runs, all five heads in the
  order printed at their training commits). check_event_count and
  checkpoint_num_events mirror the concept checks and name the run, both
  counts and the fix. The same runs gain the 26-concept pin.
- train.build_model takes an optional event_names override; unset,
  behaviour is unchanged. hazard_event_names_for is the one place the
  unpinned list comes from. Training writes run_pins.json (concepts and
  events, in slot/head order) right after building the model.
- load_run threads the pinned events into build_model and runs
  check_event_count before load_state_dict.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
A --bank-on-cpu run on an A100 streamed all 37 held-out shards and then
died in compute_ctl: the random projection was built on the default
device and multiplied against banks on the other one, and the probe fits
never moved batches to the probe device at all, so any bank/probe device
split failed. 45 minutes of streaming were lost with it.

leakage.py: _random_projection takes a device and compute_ctl builds it
on the bank's device (and follows each bank in flat); the categorical and
masked-multilabel fits move every batch to the probe device and score the
tuning split batch by batch; scoring and ICL logits go through a batched
predictor that returns CPU tensors, so roc_auc_score and numpy never see
a CUDA tensor.

probe_channel.py: the CTL banks stay on the bank device (the flag's whole
point); the streamed bank is saved to <output-json stem>.bank.pt right
after the pass (--bank-path overrides, --from-bank PATH reloads it and
skips the checkpoint); the readout block is written before the CTL probes
run and the ctl block is added at the end; the JSON is append-only (a
file with a ctl block is refused without --overwrite, one without it is
filled in place); the bank size is logged in GB.

Tests: CPU-bank CTL with an explicit probe device and the device-safe
projection helper, batched prediction, the --from-bank round trip (same
readout accuracies, no model load), the fill-ctl path, and the refusals.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
scripts/compare_runs_paired.py inner-joins two alerts_rows.parquet
dumps per event on (subject_id, visit_id, time_hours), refuses when
more than 0.1% of either dump's rows are unmatched (both counts and
the unmatched count in the message), and reports per (event, horizon)
both hazard-head AUROCs with the paired subject-clustered bootstrap
of b minus a (bootstrap_auroc_delta, reused, not re-implemented), the
two GBM refits side by side without a bootstrap, next-event metrics
from each inference_results.json, and a markdown table. Tests cover
shifted scores, identical scores, a 5% row drop, duplicate keys,
label disagreement, a two-subject clustering fixture, and main().

Co-Authored-By: Claude Fable 5.1 <[email protected]>
@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.28796% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.41%. Comparing base (8b88948) to head (44e48b0).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
odyssey/inference/interventions.py 92.54% 17 Missing ⚠️
odyssey/inference/legacy_concept_pins.py 98.27% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #274      +/-   ##
==========================================
+ Coverage   89.89%   90.41%   +0.52%     
==========================================
  Files          67       67              
  Lines       10666    11009     +343     
==========================================
+ Hits         9588     9954     +366     
+ Misses       1078     1055      -23     
Files with missing lines Coverage Δ
odyssey/inference/alerts.py 87.65% <100.00%> (+0.02%) ⬆️
odyssey/inference/leakage.py 70.02% <100.00%> (+2.57%) ⬆️
odyssey/inference/run_inference.py 94.00% <100.00%> (+0.04%) ⬆️
odyssey/models/concept_bottleneck.py 100.00% <100.00%> (ø)
odyssey/training/train.py 88.83% <100.00%> (+0.16%) ⬆️
odyssey/inference/legacy_concept_pins.py 96.77% <98.27%> (+2.17%) ⬆️
odyssey/inference/interventions.py 94.22% <92.54%> (+14.82%) ⬆️

... and 1 file with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@amrit110
amrit110 merged commit 703d566 into main Sep 29, 2026
10 checks passed
@amrit110
amrit110 deleted the rebuttal/integration branch September 29, 2026 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant