Tasks/Health & Medicine/Imaging & Triage

Chest X-ray triage under cross-site distribution shift

Triage chest X-rays under a fixed resource cap

cxr_ood_triage_policy Health & Medicine Imaging & Triage
instruction.mdthis is what the agent is given

You inherit a radiology triage component from a teammate. A fixed, frozen chest-radiograph classifier already emits per-finding probabilities for every study; the teammate's stop-gap refers a study to a radiologist whenever the largest critical-finding probability crosses a fixed threshold. It works acceptably on the home hospital's studies but degrades on a second site whose scanner and population differ, and there is only a limited radiologist-review budget. Your job is to submit a better triage policy that scores, study by study, how urgently each one needs human review, so that under a fixed shared review budget the truly critical studies get seen — at the home site and at the unseen site alike. You do not train the imaging model; you improve the post-hoc policy on top of its frozen scores, and a sealed verifier re-runs your policy on hidden studies and scores it.

Hard Constraints

  1. You may only edit code under /app/methods/main/; you may add sibling .py modules and import them from solver.py. The entrypoint name and signature triage(cases, resources, seed) must not change.
  2. Standard library only — no internet, no model downloads, no external processes. Use only the Python standard library and the data passed to your function. The grading sandbox is stdlib-only, so any third-party import makes the submission score 0.
  3. Do not attempt to read the sealed grading labels or held-out files, and do not re-derive labels from any public dataset. Study identifiers are salted, site labels are permuted, and raw pixels are not provided.
  4. Return exactly one prediction per input study, each a finite risk in [0, 1]. Malformed output, a wrong-length list, a non-finite risk, or a crash scores the whole submission as invalid.
  5. The review budget is fixed and shared across all sites; you cannot refer more studies than the budget allows (the verifier refers only the highest-risk studies up to the budget).
  6. Grading budget (declared so you can size your method): your triage() is re-run once, in a fresh process, under a 1800-second wall-clock cap on 2 CPU cores and 256 MiB of memory, CPU only, no network. The cap is deliberately generous because the grading host may be shared; size your method for roughly 600 seconds of compute on 2 dedicated cores rather than tuning to the cap. The graded run scores 2x as many hidden studies as the visible grade.jsonl sample, with a calibration resource the same size as the visible one. Over-budget, OOM-killed, crashed or wrongly-shaped output scores 0, so leave real margin rather than tuning to the wall.

What You Have

  • /app/data/grade.jsonl: a labeled development sample of study bundles (same schema as the hidden set), with a gold_critical field so you can score yourself locally.
  • /app/data/calibration.jsonl: a labeled sample (each study carries a label, 1 if it has any critical finding) you may use freely to fit/tune your policy.
  • /app/data/score_schema.yaml: the field dictionary — the 18 frozen finding scores and the 8 critical findings.
  • /app/methods/main/solver.py: the weak baseline you must improve (it ranks studies by the raw maximum critical-finding score). This directory is what gets graded.
  • /app/selfcheck.py: a free, unlimited local dry-run (python /app/selfcheck.py) that re-runs your policy on the visible sample and prints the same per-site breakdown the verifier uses. The hidden scores differ; overfitting the visible sample does not transfer.

Each study is:

{
  "study_uid": "...",           # salted id
  "site_id": 0,                  # opaque site label (an in-domain and an unseen site appear)
  "view": "PA"|"AP", "age_bucket": int, "sex": "M"|"F",
  "scores": {"Effusion": 0.83, "Cardiomegaly": 0.04, ...}   # 18 frozen probabilities in [0,1]
}

The calibration sample additionally carries a "label" field (1 if the study has any critical finding, else 0).

What You Submit

Edit /app/methods/main/solver.py, keeping the fixed signature:

def triage(cases, resources, seed):
    """
    cases:     list[dict] of studies as above (no labels).
    resources: {"budget": float, "labels": [<8 critical finding names>],
                "calibration": [ {...study..., "label": 0|1}, ... ],   # labeled, use freely
                "sites": [site ids present], "n_bins": int}
    seed:      int
    returns:   list[dict], one per case, each {"study_uid": str, "risk": float in [0,1]}.
               risk should estimate P(study has a critical finding); the budget is allocated by
               ranking risk across ALL studies pooled into one global ranking.
    """

You may add helper modules (policy.py, helpers.py, ...) beside solver.py. There is no submit step and no per-attempt feedback — iterate against the self-check, then leave your best solver.py in place; it is graded once at the end on the hidden studies.

How It Is Judged

After your run, the grader copies your methods/main/ into a clean stdlib-only sandbox, runs your triage on hidden studies drawn from an in-domain site and an unseen site, refers the top studies by your risk up to the fixed budget, and recomputes the raw metric

triage_utility_pct = 100 * clip( mean_site(sensitivity)
                                 - 1.0 * mean_site(Brier)
                                 - 0.5 * (mean_site(sensitivity) - min_site(sensitivity)),
                                 0, 1)

where sensitivity is the fraction of truly-critical studies referred within budget, and Brier is the mean squared error of your risk against the critical label (a proper scoring rule: a near-constant prediction at the base rate scores poorly, so it rewards accurate, well-resolved probabilities). Higher triage_utility_pct is always better, and you only ever see this raw metric. Matching the inherited weak baseline earns nothing; there is a hidden reference solve well above it, and real headroom above that reference. Any crash, malformed output, or third-party import scores 0.

Metric

triage_utility_pct on the sealed two-site set · higher is better

100*clip(mean_sens - mean_Brier - 0.5*(mean_sens - min_site_sens), 0, 1); top 20% of 1,600 studies referred

anchorvisible setheld-outreward
Bstarter policy, max raw critical score19.6430015.164810.00
Rreference solution24.17225.3310.30
calibration point, R + 0.5*(R - B)30.4140950.60
Ustructural ceiling of the metric1001001.00
normalisation
m <= B0
B < m <= R0.3 * (m - B) / (R - B)
R < m < U1 - 0.7*(e(m) - e(U))/(1 - e(U))
m >= U1 (clamped)

m = this run's held-out metric  ·  B = starter policy, max raw critical score  ·  R = reference solution  ·  U = structural ceiling of the metric

e(x) = exp(-(x - R)/tau). One score, mapped once, no per-case average. tau bisected so 0.6 lands on 30.414095; U = 100 exact.

Rollouts

145 minwall clock
$36.67spend
50.0Mtokens
20versions, 18 kept
20 24 28 32 36 $0 $7.5 $15 $22 $30 cumulative spend on the run self-check triage_utility_pct, 800 visible studies reference solution · visible · 24.172 v0 v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19
keptrevertedno scoreturning point
  1. v0Inherited baseline: rank by the maximum raw critical-finding score19.6431 min · $0.28
  2. v1Per-site ridge logistic on softened logits of the 8 critical scores, pooled fallback28.1297 min · $1.02
  3. v2Rank by within-site percentile of raw max; one pooled cubic probability curvePercentile-normalising inside each site removes scanner-scale competition, so sites compete on rank, not on raw scale.29.09319 min · $2.69
  4. v3Priority becomes the max over eight pooled per-finding cubic evidence curvesOne calibrated curve per critical finding instead of one curve over the maximum; CV mean 26.00 -> 27.99.30.72728 min · $4.15
  5. v4OOF per-site selector: calibrated priority only where it beats raw max 4/5 repeats31.84534 min · $5.41
  6. v5Binary site switch replaced by a calibrated-rank weight equal to its OOF win fraction32.01937 min · $6.01
  7. v6Label-free shrinkage: blend calibrated and raw-max ranks by site field homogeneity32.07638 min · $6.40
  8. v7Per-finding evidence ranks ensembled 80/20 from raw-probability and logit curves32.36844 min · $8.02
  9. v8Within each curve family, 90% strongest-finding rank plus 10% mean of top two32.35747 min · $9.12
  10. v9Learned evidence cut to five findings; 5% raw anchor at the heterogeneous site33.61351 min · $10.42
  11. v1030% within-site rank from a site-specific ridge on all eight softened logits33.56656 min · $12.16
  12. v11Ridge sub-rank becomes a 90/10 ensemble of all-eight (L2 5) and five-score (L2 1)33.5761 min · $14.21
  13. v12Ridge-rank contribution 30% -> 25%, set on 100 stratified visible half-samples34.39364 min · $15.43
  14. v13Per-site cubic reliability curves, log-odds shifted to meet the pooled cutoff34.45475 min · $17.03
  15. v14AP/PA view added to a separate all-finding ridge, blended 30% into that component34.45281 min · $17.92
  16. v15All-finding ridge refit on within-site empirical quantiles of the eight scoresQuantile inputs make the component invariant to monotone per-scanner shifts; CV -0.445, visible +1.034, sensitivities up.35.48696 min · $20.70
  17. v16Pooled final probability-curve penalty 50 -> 100; referral ordering unchanged35.505101 min · $21.83
  18. v17Pneumothorax dropped from the multivariate quantile ridge, kept in the raw maximum36.494120 min · $26.95
  19. v18Edema also dropped from the quantile ridge, still covered by the raw maximum36.493122 min · $27.70
  20. v19Unlabeled prevalence adaptation pivots log-odds slope at the shared budget cutoff36.537131 min · $30.69

20 snapshots, 145 min, $36.67. Selected on repeated 5-fold calibration CV with the visible set as a shift check; only v4/v5 were reverted.

On the hidden set

held-out metricreward
starter policy, max raw critical score15.164810.00
reference solution25.3310.30
calibration point, R + 0.5*(R - B)30.4140950.60
structural ceiling of the metric1001.00
this run27.550.4519
17 minwall clock
$0.79spend
3.9Mtokens
2versions, 2 kept
19.5 21.0 22.5 24.0 25.5 $0 $0.2 $0.4 $0.6 cumulative spend on the run self-check triage_utility_pct, 800 visible studies reference solution · visible · 24.172 v0 v1
keptrevertedno scoreturning point
  1. v0Inherited baseline: rank by the maximum raw critical-finding score19.64317 min · $0.72
  2. v1Per-site logistic on logit features, demographics and probabilistic-OR; CV-picked L2Raw scores become per-site calibrated probabilities; L2 chosen per site by 5-fold Brier CV, 0.1 at site 3 and 0.01 at site 5.26.05217 min · $0.74

2 snapshots, 17 min, $0.79. All work stayed in main/; both snapshots were written in the final minute, so the timeline is not a search history.

On the hidden set

held-out metricreward
starter policy, max raw critical score15.164810.00
reference solution25.3310.30
calibration point, R + 0.5*(R - B)30.4140950.60
structural ceiling of the metric1001.00
this run25.270.2982
52 minwall clock
-spend
-tokens
8versions, 8 kept
20 22 24 26 28 30 0 25 50 75 100 agent step (this harness reports no tokens or timestamps) self-check triage_utility_pct, 800 visible studies reference solution · visible · 24.172 v1 v2 v3 v4 v5 v6 v7 v8
keptrevertedno scoreturning point
  1. v1Inherited baseline: rank by the maximum raw critical-finding score19.643
  2. v2Three L2 logistic rankers averaged as within-site ranks, isotonic rank-to-probabilityRanking inside each site keeps the shared 20% budget from following score-scale shift, and a rank map ports to an unseen site.26.274
  3. v3Threshold ranker dropped; 0.4 raw + 0.6 percentile rank, Platt instead of isotonic28.339
  4. v4Platt L2 1e-3 -> 1.0; unused isotonic and site-matching code removed28.378
  5. v5Learned rankers dropped: risk = Platt of the within-site rank of max critical scoreThe 26-feature LR won calibration OOF but lost leave-one-site transfer; a rank with no weights has nothing to overfit a site with.29.012
  6. v6Hybrid by metadata type, not site id: stripped sites max-crit, natural sites LRSite ids may be permuted in the hidden set, so the switch reads sex, age spread and view mix to decide which ranker a site gets.29.442
  7. v7Natural-site rank = 0.75 LR mix + 0.25 max-crit rank, weight from calibration OOF29.48
  8. v8Same ranks; LR mix applied only where a site is confidently natural, else max-crit29.48

8 snapshots, 52 min; this harness reports no token or cost data. Nothing was reverted, and the rejected candidates never became snapshots.

On the hidden set

held-out metricreward
starter policy, max raw critical score15.164810.00
reference solution25.3310.30
calibration point, R + 0.5*(R - B)30.4140950.60
structural ceiling of the metric1001.00
this run23.780.2543
44 minwall clock
$8.96spend
10.5Mtokens
21versions, 7 kept
20.0 22.5 25.0 27.5 30.0 $0 $2 $4 $6 $8 cumulative spend on the run self-check triage_utility_pct, 800 visible studies reference solution · visible · 24.172 v0 v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 v18 v19 v20
keptrevertedno scoreturning point
  1. v0Inherited baseline: rank by the maximum raw critical-finding score19.6431 min · $0.10
  2. v1Site-specific L2 logistic on all 18 scores plus top-score and union summariesPer-site calibrated probability replaces raw-score ranking; Brier 0.198/0.211 -> 0.172/0.146. Later versions only move features.25.2345 min · $0.70
  3. v2L2 penalty 4 -> 2 with 15 coordinate passes as early shrinkage; demographics dropped28.2658 min · $1.12
  4. v3Prototype ExtraTrees-style probability forests on the same featuresprototypes 21.7-24.911 min · $1.65
  5. v4Prototype Newton gradient-boosted shallow trees, depth 1-3prototypes 20.6-23.811 min · $1.65
  6. v5Prototype site-specific standardised kNN calibration, k = 30/60/120did not complete16 min · $2.35
  7. v6Prototype linear pairwise logistic ranker plus Platt calibration26.86617 min · $2.52
  8. v7Prototype additive quantile-evidence calibrators with 4, 8 and 12 bins26.07318 min · $2.68
  9. v8Features cut to the 18 raw scores plus the top three critical order statisticsOrder statistics carry the same signal with less correlation. Repeated-fold utility 27.85 -> 28.74, spread 3.23 -> 2.88.29.77419 min · $2.92
  10. v9Regularisation and iteration sweep; penalty 2 with 15 passes stayed bestCV only, 27.53-28.5221 min · $3.38
  11. v10Squares, products, logits, threshold counts and AP-view metadata as features28.59321 min · $3.38
  12. v11Partial-pooling global/site blends and explicit site logit offsets28.06224 min · $4.01
  13. v12Coordinate-order sensitivity and forward/reverse early-stop ensemblesreorders 25.23-28.6124 min · $4.01
  14. v13AP/PA view-specific partial pooling and stratified bootstrap baggingviews 25.43, bag 29.2526 min · $4.58
  15. v14Second-stage out-of-fold Platt correction on top of the site modelsnested CV worse26 min · $4.58
  16. v15One global positive-slope OOF Platt map, blended 50/50 with identity29.83833 min · $6.04
  17. v16Global OOF isotonic calibration with PAVA and identity blendsnested Brier worse34 min · $6.27
  18. v17Robustness audit, no policy change; beat the baseline at every budget 0.10-0.3029.83835 min · $6.59
  19. v18Shrinkage LDA on the v8 features plus logistic/LDA blendsstandalone 25.15-27.0436 min · $6.79
  20. v19Within-site empirical-CDF feature normalisation, uniform and logit-percentile28.55636 min · $7.04
  21. v20Leave-one-score-out jackknife of 18 logistic members plus global OOF Platt18 models, each blind to one finding score, averaged then recalibrated: the only capacity increase that survived nested testing.29.87343 min · $8.87

21 snapshots, 43 min, $8.87; 13 were prototypes that never shipped. Five read well on calibration folds and badly on the visible set.

On the hidden set

held-out metricreward
starter policy, max raw critical score15.164810.00
reference solution25.3310.30
calibration point, R + 0.5*(R - B)30.4140950.60
structural ceiling of the metric1001.00
this run23.580.2484
116 minwall clock
$17.27spend
23.0Mtokens
8versions, 8 kept
19.5 21.0 22.5 24.0 25.5 27.0 $0 $4 $8 $12 $16 cumulative spend on the run self-check triage_utility_pct, 800 visible studies reference solution · visible · 24.172 v0 v1 v2 v3 v4 v5 v6 v7
keptrevertedno scoreturning point
  1. v0Inherited baseline: rank by the maximum raw critical-finding score19.6433 min · $0.77
  2. v1Site-standardised logit features, global ridge logistic (IRLS), per-site PlattA calibrated probability over all 18 findings replaces one raw score. Brier 0.207 -> 0.157, but it opens a site gap.25.7883 min · $0.70
  3. v2OOF Platt plus transductive per-site offsets that equalise sensitivityThe metric charges 0.5 per point of worst-site gap, so the optimum is where the sites meet. Offsets come from the cases handed in.26.48629 min · $4.09
  4. v3Features cut to a quadratic expansion of the two critical-block summariesWith equalisation on, the feature ranking inverted: the 18 per-finding squares had been reading site imbalance, not fit.26.00249 min · $5.75
  5. v4L2 2.0 -> 4.0; site normalisation, isotonic and hierarchical slopes left off25.58872 min · $9.29
  6. v5Platt map becomes one shared global slope with a per-site intercept25.57875 min · $10.05
  7. v6Finding set discovered as a union over sampled rows; optional multi-rotation OOF25.57884 min · $11.95
  8. v7Shipped rewrite: default-off experimental paths removed from the graded module25.578113 min · $16.30

8 snapshots, 113 min, $16.31. The shipped log still prints a stale 29.53 for v3; 26.002, measured twice in the transcript, is charted.

On the hidden set

held-out metricreward
starter policy, max raw critical score15.164810.00
reference solution25.3310.30
calibration point, R + 0.5*(R - B)30.4140950.60
structural ceiling of the metric1001.00
this run22.920.2289