"""Regenerate the three foundation rows of research_v2_latest/kfold/kfold_v2_summary.csv under the uniform pooled-ensemble protocol (Table 4.1 source). The six convolutional/CLIP rows were produced on the training VM as a pooled five-fold ensemble (probabilities averaged across the five fold checkpoints, scored once on the fixed 3,208-image test set). The three foundation models were originally summarised by the mean of their per-fold scores instead, a different, non-comparable aggregation that also denied them the same ensemble treatment. This script brings the foundation models into the identical pooled-ensemble protocol so all nine rows are directly comparable and every model carries a single aligned per-image prediction (which also lets the McNemar test in make_mcnemar9.py cover all nine). The six CNN/CLIP rows are kept verbatim from the authoritative VM summary; only the three foundation rows are recomputed from kfold/foundation_fold__preds.json. Confidence intervals use the bootstrap documented in research_v2_latest/code/ensemble_and_stats.py: 2000 resamples, seed 42, 2.5/97.5 percentile. Brier uses the element-wise mean convention of the original summary. Run with the project .venv interpreter: .venv/bin/python thesis_build/make_kfold_summary.py """ import csv import json import numpy as np from pathlib import Path from sklearn.metrics import ( accuracy_score, precision_recall_fscore_support, roc_auc_score, average_precision_score, cohen_kappa_score, ) ROOT = Path(__file__).resolve().parent.parent KF = ROOT / "kfold" CSV = KF / "kfold_v2_summary.csv" FOUNDATION = ["dinov2_l", "swin_b", "retfound"] NUM_CLASSES = 10 LABELS = list(range(NUM_CLASSES)) def pooled_foundation(model): probs, labels = [], None for k in range(5): d = json.load(open(KF / f"foundation_fold{k}_{model}_preds.json")) probs.append(np.array(d["probs"])); labels = np.array(d["labels"]) P = np.mean(probs, axis=0) return labels, P.argmax(1), P def ece(probs, labels, n_bins=15): conf = probs.max(1); pred = probs.argmax(1); correct = (pred == labels).astype(float) bins = np.linspace(0, 1, n_bins + 1); e = 0.0 for i in range(n_bins): m = (conf > bins[i]) & (conf <= bins[i + 1]) if m.sum(): e += m.mean() * abs(correct[m].mean() - conf[m].mean()) return float(e) def boot_ci(fn, labels, preds, n=2000, seed=42): rng = np.random.default_rng(seed); N = len(labels); vals = [] for _ in range(n): idx = rng.integers(0, N, N) vals.append(fn(labels[idx], preds[idx])) return float(np.percentile(vals, 2.5)), float(np.percentile(vals, 97.5)) def foundation_row(model): y, p, pr = pooled_foundation(model) acc = accuracy_score(y, p) * 100 prec, rec, f1, _ = precision_recall_fscore_support(y, p, average="macro", zero_division=0) acc_lo, acc_hi = boot_ci(lambda a, b: accuracy_score(a, b) * 100, y, p) f1_lo, f1_hi = boot_ci( lambda a, b: precision_recall_fscore_support(a, b, average="macro", zero_division=0)[2] * 100, y, p) roc = roc_auc_score(y, pr, multi_class="ovr", average="macro", labels=LABELS) oh = np.eye(NUM_CLASSES)[y] prauc = average_precision_score(oh, pr, average="macro") brier = float(((pr - oh) ** 2).mean()) return [model, "Foundation", round(acc, 2), round(acc_lo, 2), round(acc_hi, 2), round(f1 * 100, 2), round(f1_lo, 2), round(f1_hi, 2), round(prec * 100, 2), round(rec * 100, 2), round(roc, 4), round(prauc, 4), round(ece(pr, y), 4), round(cohen_kappa_score(y, p), 3), round(brier, 3), 5, "five-fold pooled preds"] def main(): with open(CSV) as fh: rdr = csv.reader(fh); header = next(rdr); rows = list(rdr) kept = [r for r in rows if r[1] != "Foundation"] for r in kept: r[16] = "five-fold pooled preds" new_found = [foundation_row(m) for m in FOUNDATION] allrows = kept + new_found allrows.sort(key=lambda r: -float(r[2])) with open(CSV, "w", newline="") as fh: w = csv.writer(fh); w.writerow(header); w.writerows(allrows) with open("/tmp/summary_check.txt", "w") as fh: for r in allrows: fh.write(f"{r[0]:14s} acc {r[2]:6} ({r[3]}-{r[4]}) f1 {r[5]:6} " f"roc {r[10]} ece {r[12]} kappa {r[13]} brier {r[14]} {r[16]}\n") print("wrote", CSV.relative_to(ROOT)) if __name__ == "__main__": main()