fundus-9model-benchmark / code /make_mcnemar9.py
DoB24's picture
Add pooled-protocol provenance scripts (regenerate summary, 9-model McNemar, per-class, ensemble from fold predictions)
63fc39b verified
Raw
History Blame Contribute Delete
3.57 kB
"""Regenerate research_v2_latest/kfold/cnn_clip/mcnemar.json for ALL NINE models.
Under the uniform pooled-ensemble protocol every model carries one aligned
per-image prediction on the fixed 3,208-image test set: the five convolutional
networks and CLIP from kfold/cnn_clip/<model>_test_preds.json, the three
foundation models from the probability average of their five
kfold/foundation_fold<k>_<model>_preds.json checkpoints. Because all nine share
the same test images in the same order, the paired McNemar test now covers every
pair (36 in total).
Test: exact binomial two-sided on the discordant pairs (b, c), identical to
research_v2_latest/code/ensemble_and_stats.py. The file keeps the original
"<a>_vs_<b>": {"b","c","p"} layout used by thesis_build/figures/make_fig4_5.py;
a sibling key "_meta" records the model order, pair count and Bonferroni factor.
Run with the project .venv interpreter:
.venv/bin/python thesis_build/make_mcnemar9.py
"""
import json
import numpy as np
from pathlib import Path
from scipy.stats import binom
ROOT = Path(__file__).resolve().parent.parent
KF = ROOT / "kfold"
OUT = KF / "cnn_clip" / "mcnemar.json"
# accuracy-descending order (matches Table 4.1)
ORDER = ["inception_v3", "clip_openai", "vgg19", "resnet101", "dinov2_l",
"densenet121", "resnet50", "swin_b", "retfound"]
CNN_CLIP = {"inception_v3", "clip_openai", "vgg19", "resnet101", "densenet121", "resnet50"}
def preds(model):
if model in CNN_CLIP:
d = json.load(open(KF / "cnn_clip" / f"{model}_test_preds.json"))
return np.array(d["labels"]), np.array(d["preds"])
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"])
return labels, np.mean(probs, axis=0).argmax(1)
def main():
P = {m: preds(m) for m in ORDER}
L = P[ORDER[0]][0]
for m in ORDER:
assert np.array_equal(P[m][0], L), f"{m} label order differs"
out = {}
pairs = []
for i in range(len(ORDER)):
for j in range(i + 1, len(ORDER)):
a, b = ORDER[i], ORDER[j]
ca = P[a][1] == L; cb = P[b][1] == L
bb = int(np.sum(ca & ~cb)); cc = int(np.sum(~ca & cb)); n = bb + cc
p = 1.0 if n == 0 else float(min(1.0, 2 * binom.cdf(min(bb, cc), n, 0.5)))
out[f"{a}_vs_{b}"] = {"b": bb, "c": cc, "p": p}
pairs.append((a, b, bb, cc, p))
npairs = len(pairs)
out["_meta"] = {"models": ORDER, "n_pairs": npairs, "bonferroni_factor": npairs,
"test": "exact binomial two-sided McNemar"}
with open(OUT, "w") as fh:
json.dump(out, fh, indent=1)
lines = []
nsig = 0
for a, b, bb, cc, p in sorted(pairs, key=lambda x: x[4]):
adj = min(1.0, p * npairs); sig = adj < 0.05; nsig += sig
lines.append(f"{'*' if sig else ' '} {a:13s} vs {b:13s} b={bb:4d} c={cc:4d} p={p:.3g} adj={adj:.3g}")
top = [x for x in pairs if x[0] not in {"swin_b", "retfound"} and x[1] not in {"swin_b", "retfound"}]
with open("/tmp/mcnemar9_check.txt", "w") as fh:
fh.write(f"wrote {OUT.relative_to(ROOT)} n_pairs={npairs} significant(Bonferroni)={nsig}\n")
fh.write(f"top-7 pairs={len(top)} min raw p among top-7={min(x[4] for x in top):.4f} "
f"significant among top-7={sum(1 for x in top if x[4]*npairs<0.05)}\n\n")
fh.write("\n".join(lines) + "\n")
print("wrote", OUT.relative_to(ROOT))
if __name__ == "__main__":
main()