model-validation
Design or audit the clinical-validation study for an engineer-built medical-imaging model (segmentation, classification, or detection) before the validation report or manuscript is written. Covers patient-level split disjointness and the data-leakage taxonomy, tuning-on-test, int
Install
npx skills add https://github.com/Aperivue/medsci-skills/tree/main/skills/model-validation
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aperivue-medsci-skills@llmmart
git clone https://github.com/Aperivue/medsci-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole aperivue/medsci-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Model-Validation Skill
Purpose
This skill pressure-tests the validation study for an engineer-built medical-imaging model — the
common case where a clinical team receives a trained segmentation / classification / detection model
from an engineering collaborator and must validate it and write it up. It is the imaging-model
specialization of /design-study: where design-study covers general validity and
design-ai-benchmarking covers AI-versus-human-expert reader studies, this skill owns the
partition, leakage, reproducibility, and metric-selection mechanics that decide whether a reported
Dice / AUROC / sensitivity is trustworthy.
It is advisory and deterministic-audit only. It writes decision notes and runs a stdlib gate on the split table; it never builds, trains, or alters the model, and it never replaces MONAI / nnU-Net / TorchIO — those produce the model, this validates and publishes it.
When to use
- A trained imaging model (in-house, vendor, or open-weights) needs a clinical-validation study designed or audited before submission.
- You have, or can produce, the split-assignment table (which patient went to train / val / test).
When NOT to use
- Building or training the model → out of scope (integrate MONAI / nnU-Net).
- AI-versus-human-expert reader study →
/design-ai-benchmarking. - LLM / MLLM evaluation →
/mllm-eval(when available). - General study/validity review →
/design-study. - Statistical execution (DeLong, ICC, bootstrap CIs, calibration tables) →
/analyze-stats. - Item-by-item reporting-guideline audit of a finished manuscript →
/check-reporting. - Reviewing a finished manuscript →
/self-reviewor/peer-review(which load the MD0–MD11 reviewer-side probe).
Workflow
The design/audit rationale behind Phases 2–7 — the full data-leakage taxonomy, the
internal-vs-genuine-external validation ladder, comparator design, single-run vs multi-seed
variance, test-set sizing, and the CLAIM 2024 / TRIPOD+AI / STARD-AI reporting map — is in
${CLAUDE_SKILL_DIR}/references/validation_design.md (load on demand). The patient-disjointness
verdict itself is proven by scripts/check_split_leakage.py (Phase 2), not from that prose.
Phase 1 — Reconstruct the task, the intended-use horizon, and the analysis unit
State the model's task (segmentation / classification / detection), its intended-use horizon (screening, triage, pre-procedure, post-hoc), the single headline metric the conclusion leans on, and the analysis unit the metric must respect (per-patient vs per-lesion vs per-image). Everything downstream is read against this.
Phase 2 — Leakage audit (the deterministic gate, run first)
The most metric-inflating defect is a split that is not disjoint at the patient level. Produce the
emitted split-assignment table (patient_id,split) and run the gate:
python3 ${CLAUDE_SKILL_DIR}/scripts/check_split_leakage.py \
--splits <split_assignment.csv> --out qc/split_leakage.json --strict
PATIENT_OVERLAP (a patient in ≥ 2 partitions) and MISSING_SEED (an unreproducible split) are
proven by set arithmetic — not heuristics. Then walk the rest of the leakage taxonomy (Kapoor &
Narayanan, Patterns 2023) that the table cannot show: preprocessing-before-split (normalisation,
resampling, foundation-model embeddings, or ComBat harmonisation fit on the whole cohort before
partitioning), site / scanner / burned-in-label shortcuts, and temporal leakage (a random split
where future and past coexist). The decisive question: could any value used in training have been
computed only with knowledge of a test case?
Phase 3 — Validation tier (internal split vs genuine external)
Classify the evidence honestly: apparent → internal random split → cross-validation → temporal → geographic / external (different site, scanner, vendor) → multi-site external. Cross-validation and bootstrap are development-time optimism corrections, not external validation. Flag a generalisability or deployment claim that outruns an internal-only design, and "developed with external validation" where the single external set was used for tuning. Also confirm the test set was touched once — no architecture search, hyperparameter sweep, early-stopping, or operating-point / threshold choice read the test set.
Phase 4 — Comparator design
Decide what the model is compared against: clinical-only baseline, incremental value over an existing
score, or reader comparison. For a reader comparison, hand the rubric / inter-rater design to
/design-ai-benchmarking.
Phase 5 — Metric selection (task-correct, prevalence-aware)
Match the metric to the task and the prevalence (Metrics Reloaded — Maier-Hein & Reinke et al.,
Nat Methods 2024): segmentation pairs an overlap metric (Dice / IoU) with a boundary metric
(HD95 / Normalised Surface Distance), per-structure not only global; classification under imbalance
reports AUROC and AUPRC with CIs plus sensitivity / specificity and prevalence-dependent PPV / NPV,
never bare accuracy on a balanced set; detection reports FROC / mAP with the IoU match criterion
stated. Require the headline metric as mean ± SD across ≥ 3 seeds / runs, or a fixed reported seed
with the determinism caveat. The per-case metric computation + the deterministic metric-reporting gate
live in /model-evaluation (which emits the per-case table for /analyze-stats). For interactive /
promptable segmentation (SAM2 / MedSAM2 / nnInteractive) the metric set adds the interaction axis —
number of clicks / interactions-to-threshold (NoC), initial-vs-converged Dice, and per-case
interaction / inference time (/model-evaluation --task interactive). When the evaluation runs two arms
(simulated prompting + human-operator validation), record protocol fidelity across arms — identical
prompt types, stopping rule, target threshold, and seeds — as an explicit validity item: arm-to-arm
comparability is the precondition for reading the human-operator arm as validating the simulated one,
and the human-operator arm design is in /design-study.
Phase 6 — Test-set sizing
Check the events per class in the test set, not the cohort total — a metric on a sparse positive set
has a confidence interval spanning much of the usable range, and calibration needs roughly ≥ 100 events.
Hand the formal sizing (diagnostic-accuracy / AUC precision / agreement) to /calc-sample-size.
Phase 6.5 — Prospective evaluation & deployment-monitoring horizon
Retrospective external validation shows accuracy transfers; it does not show the model
is safe and useful in the clinical workflow. If the claim is clinical use — not just
discrimination — design the higher tier explicitly: silent / shadow deployment (live
cases, no care impact, prospective performance + calibration targets) → prospective
comparative / impact study or RCT on a clinical endpoint → post-deployment monitoring
for performance / dataset-shift / calibration drift with recalibration-or-withdrawal
triggers and ongoing subgroup-performance audit. See references/validation_design.md §2b.
Scope the claim to the tier reached — a retrospective external study must not claim
deployment readiness or clinical-outcome benefit.
Phase 7 — Reporting-guideline fit
Map the study to its reporting standard via /check-reporting: CLAIM 2024 (diagnostic imaging AI),
TRIPOD+AI (prediction model), STARD-AI (diagnostic accuracy), PROBAST+AI (risk of bias),
and — for a prospective/live evaluation (Phase 6.5) — DECIDE-AI (early clinical evaluation of
decision-support AI) or CONSORT-AI / SPIRIT-AI (full AI trials / protocols).
Phase 8 — Handoffs
Carry the audited design into /write-paper (Methods), /calc-sample-size (sizing), /check-reporting
(compliance), and — for the reviewer-side audit of the finished draft — /self-review, which loads the
model_development.md (MD0–MD11) probe.
Deterministic gate
scripts/check_split_leakage.py — proves patient-level split disjointness + seed presence on the emitted
split-assignment table (stdlib, network-free). Verdicts: PATIENT_OVERLAP (Major), MISSING_SEED
(Major), SINGLE_PARTITION (Minor). Reproducible challenge:
bash ${CLAUDE_SKILL_DIR}/scripts/check_split_leakage_challenge/verify.sh.
Anti-Hallucination
- Never fabricate performance metrics, split assignments, event counts, or seeds. Every number comes from the engineer's executed code, the supplied split table, or a re-run of the deterministic gate — never invented. A reported Dice / AUROC / overlap count with no underlying record is the failure mode this skill exists to prevent.
- Never report a split-audit "pass" without running
check_split_leakage.py. The patient-disjointness verdict is proven by the script, not asserted from prose. - Never invent references, reporting-guideline items, or metric-selection rules. Verify citations via
/search-lit(confirmed DOI / PMID); mark unverified ones[UNVERIFIED - NEEDS MANUAL CHECK]. If a CLAIM 2024 / TRIPOD+AI / Metrics-Reloaded item is uncertain, flag[VERIFY]and ask the user rather than guessing. - Do not claim external validation, generalisability, or deployment readiness the design does not support — classify the validation tier honestly and let the evidence cap the claim.
Boundaries — which skill to use, in what order
design-study (general validity)
└─ model-validation (this skill: leakage, split, comparator, metric, sizing handoff)
├─ check_split_leakage.py (deterministic patient-disjointness gate)
├─ calc-sample-size (test-set / event sizing)
├─ design-ai-benchmarking (reader-comparison rubric / IRR)
├─ check-reporting (CLAIM 2024 / TRIPOD+AI / STARD-AI)
└─ write-paper -> self-review / peer-review (MD0–MD11 reviewer probe)
It does not build the model (integrate MONAI / nnU-Net), compute publication statistics (/analyze-stats
owns DeLong / ICC / calibration tables), or evaluate an LLM / MLLM (/mllm-eval).
Files (medsci-skills)
-
references
-
validation_design.md 13.5 KB
# Validation-design reference (model-validation) Load-on-demand backbone for Phases 2–7 — the leakage taxonomy, the internal-vs-external tier ladder, comparator design, run variance, test-set sizing, and the reporting map. Anchored to **Kapoor & Narayanan** (*Patterns* 2023, leakage taxonomy), **Varoquaux & Cheplygina** (*npj Digital Medicine* 2022, medical-imaging ML failure modes), **Metrics Reloaded** (Maier-Hein & Reinke et al., *Nature Methods* 2024), **CLAIM 2024**, **TRIPOD+AI** (*BMJ* 2024), and **STARD-AI** (*Nature Medicine* 2025). It explains *what to check and advise*; the patient-disjointness verdict is proven by `scripts/check_split_leakage.py`, not by this prose. ## 1. The data-leakage taxonomy Leakage = any information about a test case that could influence training. Organise the audit by the three Kapoor-Narayanan (*Patterns* 2023) categories; the deterministic gate covers only the first row. | Category (Kapoor-Narayanan) | Imaging-specific leak | How it inflates the metric | How to catch | |---|---|---|---| | **No clean train/test separation** | **Patient-level overlap** — the same patient's images straddle train and test | Model memorises patient anatomy, not pathology | `check_split_leakage.py` → `PATIENT_OVERLAP` (set arithmetic on IDs) | | ″ | **Near-duplicate / repeated-acquisition** — repeat scans, follow-ups, augmented copies, overlapping patches of one volume across splits | Test cases are not independent of training | Split on the **patient**, not the image/slice/series; dedup by patient before partitioning | | ″ | **Preprocessing-before-split** — normalisation stats, intensity windowing, resampling, feature selection, ComBat harmonisation, or foundation-model embeddings fit on the **whole cohort** | Test statistics bleed into the training pipeline | Fit every transform on the **training fold only**; the test set is touched only at scoring time | | ″ | **Temporal leakage** — a random split where future and past coexist, under a prognostic/surveillance claim | Model peeks at later-era data | Use a **temporal split** (train on earlier, test on later) when the claim is temporal | | **Illegitimate features** | **Site / scanner / burned-in-label shortcut** — the model keys on acquisition site, vendor signature, a laterality token, or a body-part marker rather than the finding | Discrimination collapses off-site | Standalone confound check (shortcut-learning, Geirhos et al. *Nat Mach Intell* 2020; DeGrave et al. *Nat Mach Intell* 2021; Zech et al. *PLOS Med* 2018); subgroup-by-site slice | | **Test set ≠ population of interest** | **Spectrum / selection bias** — test cases curated, enriched, or selected on an optional modality | Reported accuracy does not transfer to deployment | Confirm the test set reflects the intended-use population; see §2 / §6 | The decisive question to ask of every preprocessing and selection step: **could any value used in training have been computed only with knowledge of a test case?** If yes, it is leakage even when the split table itself looks disjoint. ### Tuning-on-test (the test set must be touched once) Architecture search, hyperparameter sweeps, early-stopping, **and operating-point / threshold selection** that read the test set are all forms of the first category — the test set has become a development set, and the headline metric is optimistic. Fix the threshold and select the model on the **training/validation folds**, then evaluate the frozen model on the test set exactly once. "Developed with external validation" where the single external set was also used for tuning is no longer external validation (§2). ## 2. Internal vs genuine external validation Classify the evidence honestly and let the tier cap the claim. Cross-validation and bootstrap are development-time **optimism corrections** (apparent-performance debiasing), **not** external validation — this is the long-standing TRIPOD / prediction-model distinction (Collins et al., TRIPOD 2015; TRIPOD+AI, *BMJ* 2024). ``` apparent (train=test, never sufficient) → internal random split → k-fold cross-validation / bootstrap ← still internal (optimism correction) → temporal split (later era held out) → geographic / external (different site, scanner, vendor) → multi-site / prospective external ← retrospective evidence ends here → silent / shadow deployment ┐ → prospective comparative (impact) RCT ├ early clinical evaluation + use → live deployment + monitoring ┘ (DECIDE-AI; see §2b) ``` - A **generalisability or deployment-readiness** claim needs at least a genuine external tier (different site/scanner/vendor), not an internal split. - Single-centre external validation supports a narrower claim than multi-site; say which. - Reusing the external set for any tuning demotes it back to internal — flag the contradiction. - A **clinical-use, workflow-impact, or patient-outcome** claim needs the prospective tiers in §2b (silent trial → impact study), not retrospective external accuracy. ## 2b. Beyond retrospective external — prospective evaluation & deployment monitoring Retrospective external validation establishes that accuracy transfers; it does **not** establish that the model is safe and useful *in the clinical workflow*. That is a separate, higher tier the lane previously stopped short of. Design it explicitly when the claim is clinical use, not just discrimination: - **Silent / shadow deployment.** The model runs on live prospective cases without acting on care; compare its outputs to the real-time reference and to clinician decisions. Catches prospective performance drop, input/workflow mismatch, and edge-case failures before any patient is affected. Pre-specify the prospective performance and calibration targets. - **Prospective comparative (impact) study / RCT.** To claim a workflow or patient-outcome benefit, randomise or prospectively compare the AI-assisted pathway against standard care on a **clinical** endpoint (time-to-diagnosis, recall rate, downstream outcome), not just standalone accuracy. This is the top of the evidence hierarchy for clinical AI. - **Post-deployment monitoring.** A deployed model is not a finished artifact: pre-specify a monitoring plan for **performance drift, dataset / population shift, and calibration drift** over time, with trigger thresholds for recalibration or withdrawal, and an audit of subgroup performance over time (equity does not hold automatically post-deployment). **Reporting.** Early-stage live evaluation of decision-support AI is reported against **DECIDE-AI** (Stage 1–2a, early clinical evaluation); full prospective trials against **CONSORT-AI / SPIRIT-AI**. Route via `/check-reporting` (Phase 7). State which tier the study reaches and scope the claim to it — a retrospective external study must not claim deployment readiness, monitoring adequacy, or clinical-outcome benefit. ## 3. Comparator design A standalone metric rarely answers the clinical question; decide what the model is measured *against*, evaluated on the **same** test set. CLAIM 2024 and TRIPOD+AI both ask for comparison to current practice / an existing model. | Comparator | When | Hand-off | |---|---|---| | **Clinical / no-model baseline** | "does the model beat current standard of care?" | — | | **Incremental value over an existing score** | model added on top of an established risk score / radiologist read | added-value statistics (NRI / IDI / decision curve) → `/analyze-stats` | | **Reader comparison (standalone or AI-assisted)** | model vs / with radiologists | rubric, reader panel, inter-rater design → `/design-ai-benchmarking` | Name whether the claim is **standalone** (model alone) or **assistive** (clinician + model); they need different comparators and different reporting. ## 4. Single-run vs multi-seed variance A single training run overstates precision: deep-model metrics move with the random seed (initialisation, data order, augmentation), and some GPU ops are non-deterministic even with cuDNN deterministic flags set (Varoquaux & Cheplygina, *npj Digit Med* 2022; reproducibility crisis, Kapoor & Narayanan 2023). Require the headline metric as **mean ± SD over ≥ 3 seeds / runs**, or a **single fixed reported seed with the determinism caveat stated**. A point estimate from one run, presented as if exact, is a reporting defect. ## 5. Test-set sizing Check **events per class in the test set**, not the cohort total. A metric computed on a sparse positive set has a confidence interval spanning much of the usable range, so a headline AUROC / sensitivity can be statistically uninformative even when the cohort is large. - Size the test set for the **CI width** of the headline metric and for **per-subgroup** estimates you intend to report. - **Calibration** in particular is data-hungry — prediction-model validation guidance uses a rule of thumb of roughly ≥ 100 events and ≥ 100 non-events before a reliability assessment is stable (treat as a rule of thumb, not a hard cutoff; confirm for the specific design). - Hand the formal calculation (diagnostic-accuracy precision, AUC precision, agreement, calibration sample size) to `/calc-sample-size`. Metric **selection** (Dice + boundary metric; AUROC + AUPRC under imbalance; FROC/mAP with the IoU criterion) is owned by `/model-evaluation` (`references/metric_guide.md`, anchored to Metrics Reloaded); this skill only checks that the chosen metric is task- and prevalence-correct. ## 6. Reporting-guideline fit Map the study to its standard via `/check-reporting`, and **name both the base instrument and the AI extension**, citing each at its actual maturity (published guideline vs protocol-stage), never beyond it. The four standards below are cross-checked against the repository's `check-reporting` verified checklists. | Study framing | Primary standard (extension) | Base instrument | Risk of bias | |---|---|---|---| | Diagnostic / triage **imaging-AI** study (standalone or assistive) | **CLAIM 2024** update (Tejani et al., *Radiology: AI* 2024) | CLAIM 2020 (Mongan, Moy, Kahn, *Radiology: AI* 2020) | PROBAST+AI | | **Prediction model** (diagnostic or prognostic; regression or ML) | **TRIPOD+AI** (Collins, Moons et al., *BMJ* 2024) | TRIPOD 2015 (Collins, Reitsma, Altman, Moons) | **PROBAST+AI** (Moons et al., *BMJ* 2025; replaces PROBAST-2019) on base PROBAST (Wolff et al., *Ann Intern Med* 2019) | | **Diagnostic accuracy** study (index test vs reference standard; sens/spec) | **STARD-AI** (Sounderajah et al., *Nature Medicine* 2025) | STARD 2015 (Bossuyt et al., *BMJ* 2015) | QUADAS-2 / QUADAS-C | Tie the partition, leakage controls, validation tier, comparator, run variance, and test-set sizing above to the specific items these standards request (data partition, sample size, model evaluation, comparison to current practice, reproducibility). ## Hand-offs - Patient-disjointness proof → `scripts/check_split_leakage.py` (Phase 2, run first). - Test-set / event sizing → `/calc-sample-size`. - Reader-comparison rubric + inter-rater design → `/design-ai-benchmarking`. - Per-case metric computation + reporting gate → `/model-evaluation` → `/analyze-stats`. - Item-by-item compliance → `/check-reporting`; Methods write-up → `/write-paper`; reviewer-side audit of the finished draft → `/self-review` (MD0–MD11 `model_development.md` probe). ## Verification notes (what each claim is grounded on) - **Leakage taxonomy / three categories, reproducibility crisis** — Kapoor & Narayanan, "Leakage and the reproducibility crisis in machine-learning-based science," *Patterns* 2023. Imaging-specific failure modes (patient-level split, preprocessing-before-split) — Varoquaux & Cheplygina, *npj Digital Medicine* 2022. Both already cited in `check_split_leakage.py`. - **Site/scanner/shortcut leakage** — shortcut learning, Geirhos et al., *Nature Machine Intelligence* 2020; radiographic shortcut example, DeGrave et al., *Nature Machine Intelligence* 2021; cross-site generalisation failure, Zech et al., *PLOS Medicine* 2018. Used as named examples of the "illegitimate features" / spectrum-bias rows, not as numeric claims. - **Internal vs external, optimism correction, CV ≠ external** — TRIPOD 2015 (Collins, Reitsma, Altman, Moons) and TRIPOD+AI (*BMJ* 2024). The tier ladder mirrors the skill's Phase 3. - **Metric selection deferral** — Metrics Reloaded (Maier-Hein & Reinke et al., *Nature Methods* 2024); detail lives in `/model-evaluation`. - **Reporting map** — CLAIM 2024 update (Tejani et al., *Radiology: AI* 2024) on base CLAIM 2020 (Mongan, Moy, Kahn); TRIPOD+AI (*BMJ* 2024); STARD-AI (Sounderajah et al., *Nature Medicine* 2025) on base STARD 2015 (Bossuyt et al., *BMJ* 2015); PROBAST+AI (Moons et al., *BMJ* 2025) on base PROBAST (Wolff et al., *Ann Intern Med* 2019). All four are cross-checked against this repository's `check-reporting` verified checklists (CLAIM 2024 = e240300; TRIPOD+AI = e078378; STARD-AI = DOI 10.1038/s41591-025-03953-8, PMID 40954311; PROBAST+AI = e082505). - **Numbers deliberately not asserted**: the only quantitative figure is the ~100 events/non-events calibration rule of thumb, flagged as a rule of thumb to confirm per design — no dataset names, thresholds, or performance numbers are invented here. The reporting-standard DOIs/PMIDs above are carried from the repository's verified `check-reporting` checklists; still re-confirm the exact identifier via `/search-lit` before quoting any of them in a manuscript, and mark any uncertain item `[VERIFY]`.
-
-
scripts
-
check_split_leakage_challenge
-
expected
-
clean.txt 358 B
========================================= Split-Leakage Gate (model-validation) ========================================= rows=13 subjects=12 partitions={'test': 4, 'train': 6, 'val': 3} seed=42 | Check | Severity | Detail | |---|---|---| | (none) | — | patient-disjoint split with a recorded seed | OK: patient-disjoint split with a recorded seed. -
leak.txt 470 B
========================================= Split-Leakage Gate (model-validation) ========================================= rows=14 subjects=12 partitions={'test': 4, 'train': 6, 'val': 4} seed=42 | Check | Severity | Detail | |---|---|---| | PATIENT_OVERLAP | Major | 2 of 12 subjects appear in >= 2 partitions (e.g. 'P03' in test/train); the same patient in train and test inflates every metric. Offenders: P03, P07 | MAJOR candidate: 1 split-integrity issue(s).
-
-
fixture
-
splits_clean.csv 150 B · in bundle
-
splits_leak.csv 145 B · in bundle
-
split_seed.txt 3 B
42
-
-
problem.md 2.3 KB
# Challenge card — split-leakage gate (model-validation) ## Problem A clinical team receives a trained medical-imaging model from an engineering collaborator and reports a head-line metric (Dice, AUROC, sensitivity). The single most common — and most metric-inflating — defect is a data split that is **not disjoint at the patient level**: the same patient contributes images to both the training and the test partition. The model then memorises patient-specific anatomy rather than pathology, and every reported number is optimistic (Kapoor & Narayanan, *Patterns* 2023; Varoquaux & Cheplygina, *npj Digit Med* 2022; CLAIM 2024 data-partition items). A reviewer cannot see this from the prose — only from the split itself. ## What the gate does `scripts/check_split_leakage.py` reads the **emitted split-assignment table** (`patient_id, split`) and **proves**, by set arithmetic on the IDs, whether any patient crosses partitions. This is not a heuristic prose lint — it is a fully decidable data check on the produced artifact. It also confirms the split is **reproducible** (a recorded random seed), because a split with no seed cannot be regenerated or re-verified. An ID appearing several times *within one* partition (multiple images per patient) is fine and does not fire; only an ID spanning ≥ 2 partitions does. Train/training, val/validation, and test/testing/holdout synonyms are collapsed so a labelling variant never registers as a false overlap. ## Fixture (synthetic only — no real patients, no PII) - `fixture/splits_leak.csv` — 12 synthetic subjects; **P03** is in train + test and **P07** is in train + val → 2 patient-overlap leaks. - `fixture/splits_clean.csv` — each subject in exactly one partition, with repeat images and the `training` / `validation` / `holdout` synonyms, to prove neither a within-split repeat nor a label variant trips the gate. - `fixture/split_seed.txt` — `42`, auto-detected as the recorded split seed. ## Expected - `expected/leak.txt` — `PATIENT_OVERLAP` (Major), naming P03 (test/train) and the two offenders; exit 1 under `--strict`. - `expected/clean.txt` — `(none)` / OK; exit 0 under `--strict`. `verify.sh` diffs both stdout outputs against `expected/` and asserts the exit-code contract (leak → 1, clean → 0). Network-free, torch-free, stdlib-only. -
verify.sh 1.9 KB
#!/usr/bin/env bash # Deterministic verifier for the split-leakage challenge card. # Runs check_split_leakage.py on two synthetic split-assignment CSVs and diffs the # stdout against expected/. No network, no torch — the leak is decided by set # arithmetic on the patient IDs. Exit 0 = both match and exit codes are correct. # # Fixtures (synthetic only — no real patients, no PII): # splits_leak.csv — patient P03 in train+test and P07 in train+val (2 leaks). # splits_clean.csv — each patient in exactly one partition; the synonyms # training/validation/holdout collapse, so they do NOT # register as extra partitions (no false overlap). set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" DET="$HERE/../check_split_leakage.py" leak="$(python3 "$DET" --splits "$HERE/fixture/splits_leak.csv")" clean="$(python3 "$DET" --splits "$HERE/fixture/splits_clean.csv")" ok=1 if ! diff -u "$HERE/expected/leak.txt" <(printf '%s\n' "$leak"); then echo "FAIL: leak-fixture output drifted from expected/leak.txt" >&2; ok=0 fi if ! diff -u "$HERE/expected/clean.txt" <(printf '%s\n' "$clean"); then echo "FAIL: clean-fixture output drifted from expected/clean.txt" >&2; ok=0 fi # Exit-code contract under --strict: leak -> 1 (Major), clean -> 0. python3 "$DET" --splits "$HERE/fixture/splits_leak.csv" --strict --quiet >/dev/null 2>&1 && rc_leak=0 || rc_leak=$? python3 "$DET" --splits "$HERE/fixture/splits_clean.csv" --strict --quiet >/dev/null 2>&1 && rc_clean=0 || rc_clean=$? [ "${rc_leak:-0}" -eq 1 ] || { echo "FAIL: leak fixture should exit 1 under --strict (got ${rc_leak:-0})" >&2; ok=0; } [ "$rc_clean" -eq 0 ] || { echo "FAIL: clean fixture should exit 0 under --strict (got $rc_clean)" >&2; ok=0; } if [ "$ok" -eq 1 ]; then echo "PASS: split-leakage gate flags the 2 patient-overlap leaks (P03, P07) and clears the disjoint split." else exit 1 fi
-
-
check_split_leakage.py 11.4 KB
#!/usr/bin/env python3 """Train/validation/test split-leakage gate for a medical-imaging model (model-validation). The single most common — and most metric-inflating — defect in an engineer-built imaging model is a data split that is NOT disjoint at the patient level: the same patient contributes images to both the training and the test partition, so the model memorises patient-specific anatomy and every reported metric is optimistic (Kapoor & Narayanan, Patterns 2023; Varoquaux & Cheplygina, npj Digit Med 2022; CLAIM 2024 data-partition items). Unlike a prose hygiene linter, this is a fully *decidable* data check: it reads the emitted split-assignment table and proves, by set arithmetic on the patient/subject IDs, whether any ID crosses partitions. It also checks that the split is REPRODUCIBLE — a recorded random seed — because a split with no seed cannot be regenerated and its disjointness cannot be re-verified. CHECKS (verdicts): 1. PATIENT_OVERLAP (Major) one or more IDs appear in >= 2 distinct partitions (after collapsing train/training, val/validation, test/testing/holdout synonyms). The decisive leak. 2. MISSING_SEED (Major) no split seed found via --seed, --seed-file, an auto-detected split_seed.txt next to the CSV, or a seed / random_state column. Pass --no-require-seed to downgrade to a flag when the seed is genuinely external. 3. SINGLE_PARTITION (Minor) fewer than two distinct partitions — the file is not a train/val/test split (informational, not a leak). An ID appearing multiple times WITHIN one partition (several images per patient) is NOT a leak and does not fire; only an ID spanning >= 2 partitions does. INPUTS --splits split-assignment CSV (required). Columns auto-detected: id : patient_id / subject_id / case_id / id / pid / mrn / studyid split : split / partition / set / subset / fold / phase / assignment Override with --id-col / --split-col. --seed the split's random seed (int/str), to record reproducibility. --seed-file path to a file holding the seed (default: auto-detect split_seed.txt alongside --splits). OUTPUT A reconciliation table (stdout) and, with --out, a JSON artifact: {splits, id_col, split_col, n_rows, n_subjects, partitions{name:count}, seed, claims[{verdict, severity, detail, where}], summary} PATIENT_OVERLAP / MISSING_SEED are Major candidates. Stdlib-only (csv / json / re / argparse / pathlib). Exit codes: 0 clean (or report-only), 1 Major claim(s) found (with --strict), 2 input/usage error. """ from __future__ import annotations import argparse import csv import json import re import sys from pathlib import Path ID_HINTS = ("patient_id", "subject_id", "case_id", "patientid", "subjectid", "caseid", "patient", "subject", "id", "pid", "eid", "uid", "mrn", "studyid", "study_id") SPLIT_HINTS = ("split", "partition", "set", "subset", "fold", "phase", "assignment", "split_assignment", "data_split", "group_split") # Synonym collapse so "train"/"training" do not register as two partitions (a # conservative choice: real leakage is train-vs-test, not a labelling variant). SPLIT_CANON = { "train": "train", "training": "train", "trn": "train", "val": "val", "valid": "val", "validation": "val", "dev": "val", "development": "val", "test": "test", "testing": "test", "holdout": "test", "hold-out": "test", "hold_out": "test", "eval": "test", "evaluation": "test", } def _norm(s: str) -> str: return re.sub(r"[^a-z0-9]+", "", (s or "").strip().lower()) def _canon_split(v: str) -> str: n = _norm(v) return SPLIT_CANON.get(n, n) def _pick(header: list[str], hints: tuple[str, ...]): norm = [_norm(h) for h in header] for hint in hints: # exact match first h = _norm(hint) for i, col in enumerate(norm): if col == h and h: return header[i] for hint in hints: # then substring h = _norm(hint) for i, col in enumerate(norm): if h and h in col: return header[i] return None def _find_seed(splits_path: Path, rows: list[dict], header: list[str], seed: str | None, seed_file: str | None) -> str | None: if seed is not None and str(seed).strip() != "": return str(seed).strip() # explicit or auto-detected seed file candidates = [] if seed_file: candidates.append(Path(seed_file)) candidates.append(splits_path.with_name("split_seed.txt")) candidates.append(splits_path.parent / "split_seed.txt") for c in candidates: if c.is_file(): txt = c.read_text(encoding="utf-8").strip() m = re.search(r"-?\d+", txt) if m: return m.group(0) # a seed / random_state column col = _pick(header, ("seed", "random_state", "random_seed", "rng_seed")) if col: for r in rows: v = (r.get(col) or "").strip() if v: return v return None def analyze(splits: str, id_col: str | None, split_col: str | None, seed: str | None, seed_file: str | None, require_seed: bool) -> dict: p = Path(splits) if not p.is_file(): sys.stderr.write(f"ERROR: --splits not found: {splits}\n") sys.exit(2) with p.open(encoding="utf-8-sig", newline="") as f: rows = [r for r in csv.DictReader(f)] if not rows: sys.stderr.write(f"ERROR: --splits has no rows: {splits}\n") sys.exit(2) header = list(rows[0].keys()) if id_col and id_col not in header: sys.stderr.write(f"ERROR: --id-col '{id_col}' not in {header}\n") sys.exit(2) if split_col and split_col not in header: sys.stderr.write(f"ERROR: --split-col '{split_col}' not in {header}\n") sys.exit(2) idc = id_col or _pick(header, ID_HINTS) spc = split_col or _pick(header, SPLIT_HINTS) if idc is None: sys.stderr.write(f"ERROR: no ID column found (looked for {ID_HINTS[:6]}…); pass --id-col\n") sys.exit(2) if spc is None: sys.stderr.write(f"ERROR: no split column found (looked for {SPLIT_HINTS[:6]}…); pass --split-col\n") sys.exit(2) # ID -> set of canonical partitions it appears in. id_to_splits: dict[str, set[str]] = {} part_counts: dict[str, int] = {} for r in rows: sid = (r.get(idc) or "").strip() part = _canon_split(r.get(spc) or "") if not sid or not part: continue id_to_splits.setdefault(sid, set()).add(part) part_counts[part] = part_counts.get(part, 0) + 1 n_subjects = len(id_to_splits) overlapping = sorted(sid for sid, parts in id_to_splits.items() if len(parts) > 1) seed_val = _find_seed(p, rows, header, seed, seed_file) claims: list[dict] = [] if overlapping: shown = ", ".join(overlapping[:8]) + ("…" if len(overlapping) > 8 else "") # a representative offender with its partitions ex = overlapping[0] ex_parts = "/".join(sorted(id_to_splits[ex])) claims.append({ "verdict": "PATIENT_OVERLAP", "severity": "Major", "detail": (f"{len(overlapping)} of {n_subjects} subjects appear in >= 2 partitions " f"(e.g. '{ex}' in {ex_parts}); the same patient in train and test " f"inflates every metric. Offenders: {shown}"), "where": f"--splits id column '{idc}', split column '{spc}'", }) if seed_val is None and require_seed: claims.append({ "verdict": "MISSING_SEED", "severity": "Major", "detail": ("no split seed found (--seed / --seed-file / split_seed.txt / a seed " "column); the partition cannot be regenerated or re-verified"), "where": f"--splits {p.name}", }) if len(part_counts) < 2: claims.append({ "verdict": "SINGLE_PARTITION", "severity": "Minor", "detail": (f"only {len(part_counts)} distinct partition(s) " f"({', '.join(sorted(part_counts)) or 'none'}); this is not a " f"train/val/test split"), "where": f"--splits split column '{spc}'", }) n_major = sum(1 for c in claims if c["severity"] == "Major") return { "splits": str(p), "id_col": idc, "split_col": spc, "n_rows": len(rows), "n_subjects": n_subjects, "partitions": dict(sorted(part_counts.items())), "seed": seed_val, "claims": claims, "summary": { "n_claims": len(claims), "n_major": n_major, "n_minor": len(claims) - n_major, "n_overlapping_subjects": len(overlapping), "verdict": "MAJOR_CANDIDATE" if n_major else "OK", }, } def render(result: dict) -> str: lines = ["| Check | Severity | Detail |", "|---|---|---|"] for c in result["claims"]: lines.append(f"| {c['verdict']} | {c['severity']} | {c['detail']} |") if len(lines) == 2: lines.append("| (none) | — | patient-disjoint split with a recorded seed |") return "\n".join(lines) def main() -> int: ap = argparse.ArgumentParser(description="Train/val/test split-leakage gate (model-validation).") ap.add_argument("--splits", required=True, help="split-assignment CSV (id column + split column)") ap.add_argument("--id-col", help="patient/subject ID column (auto-detected if omitted)") ap.add_argument("--split-col", help="split/partition column (auto-detected if omitted)") ap.add_argument("--seed", help="the split's random seed, to record reproducibility") ap.add_argument("--seed-file", help="file holding the seed (default: auto-detect split_seed.txt)") ap.add_argument("--no-require-seed", dest="require_seed", action="store_false", help="downgrade a missing seed from Major to no-finding") ap.add_argument("--out", help="write JSON artifact to this path") ap.add_argument("--strict", action="store_true", help="exit 1 if any Major claim exists") ap.add_argument("--quiet", action="store_true", help="suppress stdout table") ap.set_defaults(require_seed=True) args = ap.parse_args() result = analyze(args.splits, args.id_col, args.split_col, args.seed, args.seed_file, args.require_seed) if not args.quiet: print("=" * 41) print(" Split-Leakage Gate (model-validation)") print("=" * 41) print(f" rows={result['n_rows']} subjects={result['n_subjects']} " f"partitions={result['partitions']} seed={result['seed']}") print(render(result)) print() s = result["summary"] if s["n_major"]: print(f"MAJOR candidate: {s['n_major']} split-integrity issue(s).") else: print("OK: patient-disjoint split with a recorded seed.") if args.out: Path(args.out).parent.mkdir(parents=True, exist_ok=True) Path(args.out).write_text(json.dumps({"detector": "check_split_leakage", **result}, indent=2), encoding="utf-8") if not args.quiet: print(f"\nwrote {args.out}") return 1 if (args.strict and result["summary"]["n_major"]) else 0 if __name__ == "__main__": sys.exit(main())
-
-
tests
-
fixtures
-
leak_subject.csv 62 B · in bundle
-
noseed_clean.csv 50 B · in bundle
-
single_partition.csv 55 B · in bundle
-
-
test_split_leakage.sh 3.9 KB
#!/usr/bin/env bash # Regression test for the split-leakage gate (model-validation). # Synthetic, PII-free fixtures reproduce: (a) a patient that crosses train/test, # (b) column auto-detection (subject_id / partition), (c) a missing split seed, # (d) the --no-require-seed / --seed downgrades, (e) a single-partition file, and # (f) a seed read from a column. Stdlib-only (python3). set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT="$HERE/../scripts/check_split_leakage.py" F="$HERE/fixtures" CHF="$HERE/../scripts/check_split_leakage_challenge/fixture" OUT="$(mktemp -t spl_XXXX).json" trap 'rm -f "$OUT"' EXIT fail=0 check() { local label="$1"; shift if "$@" >/dev/null 2>&1; then printf ' PASS %s\n' "$label" else printf ' FAIL %s\n' "$label"; fail=$((fail+1)); fi } has_verdict() { python3 -c " import json d=json.load(open('$OUT')) assert any(c['verdict']=='$1' for c in d['claims']), '$1 not found' "; } no_verdict() { python3 -c " import json d=json.load(open('$OUT')) assert not any(c['verdict']=='$1' for c in d['claims']), '$1 unexpectedly present' "; } [[ -f "$SCRIPT" ]] || { echo "ENV-ERR: script missing" >&2; exit 2; } # (1) challenge leak fixture: P03/P07 cross partitions, seed auto-detected -> PATIENT_OVERLAP, exit 1 python3 "$SCRIPT" --splits "$CHF/splits_leak.csv" --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 1 under --strict (patient overlap)" test "$?" -eq 1 check "PATIENT_OVERLAP detected" has_verdict PATIENT_OVERLAP check "two overlapping subjects reported" python3 -c " import json; d=json.load(open('$OUT')) assert d['summary']['n_overlapping_subjects']==2, d['summary']" check "seed auto-detected from split_seed.txt" python3 -c " import json; d=json.load(open('$OUT')); assert d['seed']=='42', d['seed']" # (2) clean challenge fixture: synonyms collapse, disjoint -> exit 0, no overlap python3 "$SCRIPT" --splits "$CHF/splits_clean.csv" --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 0 on disjoint split (synonyms collapsed)" test "$?" -eq 0 check "no PATIENT_OVERLAP on clean split" no_verdict PATIENT_OVERLAP # (3) column auto-detection (subject_id / partition) + --seed isolates the overlap python3 "$SCRIPT" --splits "$F/leak_subject.csv" --seed 1 --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 1 with auto-detected subject_id/partition columns" test "$?" -eq 1 check "PATIENT_OVERLAP via auto-detect (S1)" has_verdict PATIENT_OVERLAP check "no MISSING_SEED when --seed supplied" no_verdict MISSING_SEED # (4) explicit --id-col / --split-col also resolve python3 "$SCRIPT" --splits "$F/leak_subject.csv" --id-col subject_id --split-col partition --seed 1 --out "$OUT" --quiet >/dev/null 2>&1 check "explicit --id-col/--split-col" has_verdict PATIENT_OVERLAP # (5) missing seed on an otherwise-disjoint split -> MISSING_SEED (Major), exit 1 python3 "$SCRIPT" --splits "$F/noseed_clean.csv" --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 1 when seed missing" test "$?" -eq 1 check "MISSING_SEED detected" has_verdict MISSING_SEED check "no PATIENT_OVERLAP on disjoint split" no_verdict PATIENT_OVERLAP # (6) --no-require-seed downgrades the missing seed -> exit 0 python3 "$SCRIPT" --splits "$F/noseed_clean.csv" --no-require-seed --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 0 with --no-require-seed" test "$?" -eq 0 check "MISSING_SEED suppressed by --no-require-seed" no_verdict MISSING_SEED # (7) single-partition file with a seed column -> SINGLE_PARTITION (Minor only), exit 0 python3 "$SCRIPT" --splits "$F/single_partition.csv" --out "$OUT" --strict --quiet >/dev/null 2>&1 check "exit 0 on single-partition (Minor only)" test "$?" -eq 0 check "SINGLE_PARTITION detected" has_verdict SINGLE_PARTITION check "seed read from column" python3 -c " import json; d=json.load(open('$OUT')); assert d['seed']=='7', d['seed']" echo "fail=$fail"; [[ "$fail" -eq 0 ]] && echo "ALL PASS" || echo "FAILURES: $fail" exit "$fail"
-
-
SKILL.md 11.3 KB
--- name: model-validation description: > Design or audit the clinical-validation study for an engineer-built medical-imaging model (segmentation, classification, or detection) before the validation report or manuscript is written. Covers patient-level split disjointness and the data-leakage taxonomy, tuning-on-test, internal versus genuine external validation, comparator design, single-run versus multi-seed variance, task-correct metric selection, test-set sizing, and CLAIM 2024 / TRIPOD+AI / STARD-AI reporting fit. Ships a deterministic split-leakage gate that proves patient disjointness by set arithmetic on the emitted split-assignment table. Does not build or train models — it integrates with MONAI / nnU-Net, it does not replace them. triggers: model validation, validate AI model, imaging model validation, data leakage, split leakage, train test split, patient-level split, internal validation, external validation, validation design, leakage audit, segmentation model validation, classification model validation, detection model validation, nnU-Net validation, deep learning validation, CLAIM 2024, generalizability, held-out test set tools: Read, Write, Edit, Bash, Grep, Glob model: inherit --- # Model-Validation Skill ## Purpose This skill pressure-tests the **validation study for an engineer-built medical-imaging model** — the common case where a clinical team receives a trained segmentation / classification / detection model from an engineering collaborator and must validate it and write it up. It is the imaging-model specialization of `/design-study`: where `design-study` covers general validity and `design-ai-benchmarking` covers AI-versus-human-expert reader studies, this skill owns the **partition, leakage, reproducibility, and metric-selection mechanics** that decide whether a reported Dice / AUROC / sensitivity is trustworthy. It is **advisory and deterministic-audit only**. It writes decision notes and runs a stdlib gate on the split table; it never builds, trains, or alters the model, and it never replaces MONAI / nnU-Net / TorchIO — those produce the model, this validates and publishes it. ## When to use - A trained imaging model (in-house, vendor, or open-weights) needs a clinical-validation study designed or audited before submission. - You have, or can produce, the **split-assignment table** (which patient went to train / val / test). ## When NOT to use - Building or training the model → out of scope (integrate MONAI / nnU-Net). - AI-versus-human-expert reader study → `/design-ai-benchmarking`. - LLM / MLLM evaluation → `/mllm-eval` (when available). - General study/validity review → `/design-study`. - Statistical execution (DeLong, ICC, bootstrap CIs, calibration tables) → `/analyze-stats`. - Item-by-item reporting-guideline audit of a finished manuscript → `/check-reporting`. - Reviewing a finished manuscript → `/self-review` or `/peer-review` (which load the MD0–MD11 reviewer-side probe). ## Workflow The design/audit rationale behind Phases 2–7 — the full data-leakage taxonomy, the internal-vs-genuine-external validation ladder, comparator design, single-run vs multi-seed variance, test-set sizing, and the CLAIM 2024 / TRIPOD+AI / STARD-AI reporting map — is in `${CLAUDE_SKILL_DIR}/references/validation_design.md` (load on demand). The patient-disjointness verdict itself is proven by `scripts/check_split_leakage.py` (Phase 2), not from that prose. ### Phase 1 — Reconstruct the task, the intended-use horizon, and the analysis unit State the model's task (segmentation / classification / detection), its **intended-use horizon** (screening, triage, pre-procedure, post-hoc), the **single headline metric** the conclusion leans on, and the **analysis unit** the metric must respect (per-patient vs per-lesion vs per-image). Everything downstream is read against this. ### Phase 2 — Leakage audit (the deterministic gate, run first) The most metric-inflating defect is a split that is **not disjoint at the patient level**. Produce the emitted split-assignment table (`patient_id,split`) and run the gate: ```bash python3 ${CLAUDE_SKILL_DIR}/scripts/check_split_leakage.py \ --splits <split_assignment.csv> --out qc/split_leakage.json --strict ``` `PATIENT_OVERLAP` (a patient in ≥ 2 partitions) and `MISSING_SEED` (an unreproducible split) are proven by set arithmetic — not heuristics. Then walk the rest of the **leakage taxonomy** (Kapoor & Narayanan, *Patterns* 2023) that the table cannot show: **preprocessing-before-split** (normalisation, resampling, foundation-model embeddings, or ComBat harmonisation fit on the whole cohort before partitioning), **site / scanner / burned-in-label shortcuts**, and **temporal leakage** (a random split where future and past coexist). The decisive question: *could any value used in training have been computed only with knowledge of a test case?* ### Phase 3 — Validation tier (internal split vs genuine external) Classify the evidence honestly: apparent → internal random split → cross-validation → temporal → geographic / external (different site, scanner, vendor) → multi-site external. Cross-validation and bootstrap are development-time optimism corrections, **not** external validation. Flag a generalisability or deployment claim that outruns an internal-only design, and "developed with external validation" where the single external set was used for tuning. Also confirm the test set was touched **once** — no architecture search, hyperparameter sweep, early-stopping, or operating-point / threshold choice read the test set. ### Phase 4 — Comparator design Decide what the model is compared against: clinical-only baseline, incremental value over an existing score, or reader comparison. For a reader comparison, hand the rubric / inter-rater design to `/design-ai-benchmarking`. ### Phase 5 — Metric selection (task-correct, prevalence-aware) Match the metric to the task and the prevalence (Metrics Reloaded — Maier-Hein & Reinke et al., *Nat Methods* 2024): **segmentation** pairs an overlap metric (Dice / IoU) with a **boundary metric** (HD95 / Normalised Surface Distance), per-structure not only global; **classification under imbalance** reports **AUROC and AUPRC with CIs** plus sensitivity / specificity and prevalence-dependent PPV / NPV, never bare accuracy on a balanced set; **detection** reports FROC / mAP with the IoU match criterion stated. Require the headline metric as **mean ± SD across ≥ 3 seeds / runs**, or a fixed reported seed with the determinism caveat. The per-case metric computation + the deterministic metric-reporting gate live in `/model-evaluation` (which emits the per-case table for `/analyze-stats`). For **interactive / promptable segmentation** (SAM2 / MedSAM2 / nnInteractive) the metric set adds the interaction axis — number of clicks / interactions-to-threshold (NoC), initial-vs-converged Dice, and per-case interaction / inference time (`/model-evaluation --task interactive`). When the evaluation runs two arms (simulated prompting + human-operator validation), record **protocol fidelity** across arms — identical prompt types, stopping rule, target threshold, and seeds — as an explicit validity item: arm-to-arm comparability is the precondition for reading the human-operator arm as validating the simulated one, and the human-operator arm design is in `/design-study`. ### Phase 6 — Test-set sizing Check the **events per class** in the test set, not the cohort total — a metric on a sparse positive set has a confidence interval spanning much of the usable range, and calibration needs roughly ≥ 100 events. Hand the formal sizing (diagnostic-accuracy / AUC precision / agreement) to `/calc-sample-size`. ### Phase 6.5 — Prospective evaluation & deployment-monitoring horizon Retrospective external validation shows accuracy *transfers*; it does **not** show the model is safe and useful *in the clinical workflow*. If the claim is clinical use — not just discrimination — design the higher tier explicitly: **silent / shadow deployment** (live cases, no care impact, prospective performance + calibration targets) → **prospective comparative / impact study or RCT** on a clinical endpoint → **post-deployment monitoring** for performance / dataset-shift / calibration drift with recalibration-or-withdrawal triggers and ongoing subgroup-performance audit. See `references/validation_design.md` §2b. Scope the claim to the tier reached — a retrospective external study must not claim deployment readiness or clinical-outcome benefit. ### Phase 7 — Reporting-guideline fit Map the study to its reporting standard via `/check-reporting`: **CLAIM 2024** (diagnostic imaging AI), **TRIPOD+AI** (prediction model), **STARD-AI** (diagnostic accuracy), **PROBAST+AI** (risk of bias), and — for a prospective/live evaluation (Phase 6.5) — **DECIDE-AI** (early clinical evaluation of decision-support AI) or **CONSORT-AI / SPIRIT-AI** (full AI trials / protocols). ### Phase 8 — Handoffs Carry the audited design into `/write-paper` (Methods), `/calc-sample-size` (sizing), `/check-reporting` (compliance), and — for the reviewer-side audit of the finished draft — `/self-review`, which loads the `model_development.md` (MD0–MD11) probe. ## Deterministic gate `scripts/check_split_leakage.py` — proves patient-level split disjointness + seed presence on the emitted split-assignment table (stdlib, network-free). Verdicts: `PATIENT_OVERLAP` (Major), `MISSING_SEED` (Major), `SINGLE_PARTITION` (Minor). Reproducible challenge: `bash ${CLAUDE_SKILL_DIR}/scripts/check_split_leakage_challenge/verify.sh`. ## Anti-Hallucination - **Never fabricate performance metrics, split assignments, event counts, or seeds.** Every number comes from the engineer's executed code, the supplied split table, or a re-run of the deterministic gate — never invented. A reported Dice / AUROC / overlap count with no underlying record is the failure mode this skill exists to prevent. - **Never report a split-audit "pass" without running `check_split_leakage.py`.** The patient-disjointness verdict is proven by the script, not asserted from prose. - **Never invent references, reporting-guideline items, or metric-selection rules.** Verify citations via `/search-lit` (confirmed DOI / PMID); mark unverified ones `[UNVERIFIED - NEEDS MANUAL CHECK]`. If a CLAIM 2024 / TRIPOD+AI / Metrics-Reloaded item is uncertain, flag `[VERIFY]` and ask the user rather than guessing. - **Do not claim external validation, generalisability, or deployment readiness the design does not support** — classify the validation tier honestly and let the evidence cap the claim. ## Boundaries — which skill to use, in what order ``` design-study (general validity) └─ model-validation (this skill: leakage, split, comparator, metric, sizing handoff) ├─ check_split_leakage.py (deterministic patient-disjointness gate) ├─ calc-sample-size (test-set / event sizing) ├─ design-ai-benchmarking (reader-comparison rubric / IRR) ├─ check-reporting (CLAIM 2024 / TRIPOD+AI / STARD-AI) └─ write-paper -> self-review / peer-review (MD0–MD11 reviewer probe) ``` It does not build the model (integrate MONAI / nnU-Net), compute publication statistics (`/analyze-stats` owns DeLong / ICC / calibration tables), or evaluate an LLM / MLLM (`/mllm-eval`). -
skill.yml 3 KB
schema_version: 2 name: model-validation layer: D owner_domain: model_validation maturity: official when_to_use: "Design or audit the clinical-validation study for an engineer-built medical-imaging model (segmentation / classification / detection): patient-level split disjointness and the data-leakage taxonomy, internal vs genuine external validation, comparator design, test-set sizing handoff, seed / run variance, task-correct metric selection, and CLAIM 2024 / TRIPOD+AI / STARD-AI reporting fit — before the validation report or manuscript is written." when_NOT_to_use: "Building or training the model (out of scope — do not rebuild MONAI / nnU-Net); benchmarking AI against a human-expert panel (use design-ai-benchmarking); evaluating an LLM / MLLM (use mllm-eval when available); general study/validity review (use design-study); statistical execution such as DeLong / ICC / bootstrap CIs (use analyze-stats); item-by-item reporting-guideline audit of a finished manuscript (use check-reporting); reviewing a finished manuscript (use self-review or peer-review)." inputs: - "trained-model description (task, architecture provenance, intended use)" - "split-assignment table (patient/subject ID + train/val/test partition) for the leakage gate" - "validation-study draft or methods notes (optional)" outputs: - "validation-design / audit review (decision notes: leakage, split, comparator, validation tier, metric selection)" - "split-leakage audit JSON (deterministic)" - "reporting-guideline fit recommendation (CLAIM 2024 / TRIPOD+AI / STARD-AI) + test-set sizing handoff to calc-sample-size" deterministic_scripts: - scripts/check_split_leakage.py side_effects: - writes_decision_notes downstream_consumers: - calc-sample-size - check-reporting - self-review - write-paper forbidden_actions: - fabricate_performance_metrics_or_split_assignments - approve_a_validation_design_with_patient_overlap_across_splits - report_a_split_audit_pass_without_running_the_detector # v2.1 quality card purpose: "Catch the structural validity failures of an engineer-built imaging model's evaluation — patient-level leakage, tuning on the test set, an internal split sold as external validation, a single-run headline metric, and metric-on-imbalanced — before they reach a clinical-validation manuscript." safety_boundaries: - "Advisory plus deterministic-audit only: never alters predictions, splits, or metrics." - "The leakage verdict is reproduced by a stdlib script (set arithmetic on the patient IDs), never asserted from prose." known_limitations: - "Audits the evaluation design, not the model's clinical safety; a clean audit is necessary, not sufficient." - "The split-leakage gate sees only the split table it is given; it cannot detect leakage hidden in upstream preprocessing it never sees." validation_commands: - "python3 scripts/check_split_leakage.py --splits <split_assignment.csv> --strict" - "bash scripts/check_split_leakage_challenge/verify.sh # deterministic, network-free" evidence_surface: ci_validator
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.