Claude Skill

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

LLM Mart · 0 points · 3 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download aperivue-medsci-skills-skills_model-validation-815765c.zip · 22 KB
Part of aperivue/medsci-skills — 47 skills

Install

skills CLI npx skills add https://github.com/Aperivue/medsci-skills/tree/main/skills/model-validation
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aperivue-medsci-skills@llmmart
Git 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-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:

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.

No comments yet.

Reviews (0)

No reviews yet.

Related