Clinical Decision Agreement (CDA)#
Does the model’s measurement error change the clinical decision?
The benchmark metrics (see the overview) report how far a model’s measurement is from the truth — MAE, MRE, IoU. That tells you the size of the error, not whether it matters. A 3 mm miss on a 90 mm tumour is noise; the same 3 mm miss on a tumour sitting at 40 mm moves it from stage T1a to T1b and changes the treatment plan.
The CDA suite (script/analyze/clinical-decision-analysis/) re-scores existing
parsed benchmark outputs through that lens. Each measurement is pushed through a
published clinical cutoff table to become a category (a tumour stage, a skeletal
class), and the question becomes whether the model’s number lands in the
same category as the ground truth. The headline statistic is
Cohen’s kappa — agreement above what chance alone would produce.
Nothing is re-run: CDA only reads the parsed/*.jsonl records that
parsing already produced. No GPU, no inference, no
medvision_bm import — seconds per model.
The clinical proxies#
Proxy |
Data |
Measurement becomes |
Statistic |
|---|---|---|---|
ANB skeletal class (primary) |
Ceph-Biometrics-400 |
Class I (≤ 4°) vs Class II (> 4°) |
Cohen’s κ |
SNA / SNB (secondary) |
Ceph-Biometrics-400 |
retrusive / normal / protrusive, around Steiner’s 82°±2 and 80°±2 |
weighted κ |
Renal T category |
KiTS23, KiPA22 |
T1a / T1b / T2a / T2b at 4, 7, 10 cm |
weighted κ |
Cutoffs come from Steiner (Am J Orthod, 1953) and the AJCC Cancer Staging
Manual, 8th ed. (2017). The authoritative numbers live in cda_config.py; the
table above is a summary.
Warning
The ANB proxy is binary, not the textbook three classes. The benchmark
defines its angle as arccos(|A·B| / (‖A‖‖B‖)), and that absolute value folds
every angle into [0°, 90°] — a Class III patient at −3° is indistinguishable
from a Class I patient at +3°. Class III cannot be recovered and is not
reported; read this proxy as “agreement on the 4° decision”, not as a full
skeletal classification.
The two tracks#
Track 1 — self-consistent. Category of the prediction vs category of the ground-truth measurement. Both sides go through the same cutoff table, so any disagreement is caused by measurement error alone. Available for every proxy.
Track 2 — renal true-label. Category of the prediction vs the pathologic T stage recorded in the KiTS23 clinical table — a real, non-imaging reference. Stronger evidence, but only KiTS23 ships pathologic stage, and pT3/pT4 are defined by tissue invasion rather than size, so no size-based rule can ever produce them. The report therefore gives both the full 6-class picture and an organ-confined pT1–pT2 stratum where size genuinely is the staging axis, plus two reference rows showing how a perfect measurement scores (from the true 3D size, and from the GT 2D slices).
Both tracks are followed by an uncertainty pass: bootstrap 95% confidence intervals and a one-sided p-value for κ > 0. The bootstrap resamples whole imaging volumes, not slices — the 1,064 renal records come from 121 volumes, and slices of one tumour are not independent observations. An i.i.d. per-slice bootstrap gave intervals five times too narrow.
Quick start#
From the repo root:
REMOVED_SAMPLES_DIR=$PWD/Data/Datasets bash script/analyze/clinical-decision-analysis/run_CDA_analysis.sh
That runs both tracks, the uncertainty pass and the report over the canonical
result directories. The headline output is CDA_REPORT.md in the script’s
folder — every leaderboard, with confidence intervals, in one Markdown file.
All CDA outputs are generated, never checked in; the pipeline reads Results/
and Data/, both gitignored, so a bare clone cannot reproduce the numbers.
REMOVED_SAMPLES_DIR makes CDA drop the same multi-cluster T/L slices the
published benchmark drops (renal 1,064 → 1,025 samples; see the
T/L exclusion),
and marks every T/L output filename with _filtered so filtered and unfiltered
runs sit side by side. It affects the T/L task only — A/D measures landmarks,
not masks, so there is no multi-cluster slice to drop.
Warning
REMOVED_SAMPLES_DIR must point at a real directory. A path that does not
exist silently yields unfiltered numbers inside _filtered-named files.
Scoring the LLM-judge re-parse#
Each model folder can hold several parsed sets: parsed/ from the regex parser
plus one llm-parsed*/ folder per
LLM-judge re-parse. Select one with CDA_PARSED_DIR:
REMOVED_SAMPLES_DIR=$PWD/Data/Datasets CDA_PARSED_DIR=llm-parsed_gemma-4-31b \
bash script/analyze/clinical-decision-analysis/run_CDA_analysis.sh
The llm-parsed prefix is what tells CDA the prediction lives in
LLM_filtered_resps rather than filtered_resps; a name matching no known
prefix is rejected outright rather than guessed at. Outputs carry the source in
their filenames (CDA_REPORT_llm-parsed-gemma-4-31b.md beside CDA_REPORT.md),
so runs never overwrite each other.
The individual scripts (summarize_CDA_task.py for Track 1,
analyze_CDA_renal_truelabel.py for Track 2, cda_uncertainty.py,
build_CDA_report.py) can also be run one at a time — see
the suite’s README
for the per-script invocations and the flag pairings they require.
Reading the output#
Start with CDA_REPORT.md. The per-task .txt reports underneath it carry the
per-model detail (full confusion matrices, per-dataset breakdowns) the final
report leaves out. Columns you will see:
Acc — fraction of parsed samples landing in the right category.
Kappa / wKappa — chance-corrected agreement: 0 is “no better than guessing with the same marginals”, 1 is perfect.
wKappa(ordinal proxies) penalises a T1a-vs-T2b confusion more than a T1a-vs-T1b one.Flip — 1 − Acc, the decision-flip rate.
AccCov — accuracy with unparseable predictions counted as wrong; compare against Acc to see how much of a good score is really coverage.
Nparsed / Ntotal — a model that answers 14 of 120 prompts can post a flattering Acc;
Nparsedis how you catch that.n / vols (uncertainty report) — records scored, and the independent imaging volumes they came from. For tumour proxies
volsis the honest sample size.
Before quoting a number:
κ is not comparable across proxies. Track-1 agreement depends mostly on how close the cohort’s values sit to a cutoff, so a proxy whose cutoff falls in a dense part of the distribution scores lower for the same measurement accuracy. Compare models within a proxy, never proxies against each other.
Check
Nparsedand the majority class. On the organ-confined renal stratum, always answering “T1a” already scores ≈ 0.56 accuracy; κ corrects for that, raw accuracy does not.Small n. Rows with under 10 scored records still print a κ, a CI and a p-value — arithmetically valid, practically meaningless. The uncertainty report marks them
low_n; the Track-1 text report does not, so readNparsedthere.Track-1 renal staging is per-slice, not per-tumour — each scored record is one 2D slice’s measurement pushed through the staging table.
Further reading#
script/analyze/clinical-decision-analysis/README.md— full user guide: per-script invocations, filtered runs, output file map.script/analyze/clinical-decision-analysis/DESIGN.md— implementation notes, statistical choices (clustered bootstrap, the absent permutation test), invariants and known limitations.