"""Generate the smoke dataset. Deterministic — rerunning reproduces the file byte for byte, so the committed CSV can always be regenerated and diffed. Signal is real but modest: the label depends on age/iop/cdr with noise, so a working pipeline lands around 0.85-0.95 AUC and a broken one sits near 0.5. A perfectly separable toy would hide bugs by scoring 1.0 no matter what. """ import csv, math, random from pathlib import Path rng = random.Random(20260813) N_PATIENTS, EYES = 200, 2 SITES = ["skovde", "gothenburg", "linkoping"] out = Path(__file__).resolve().parents[1] / "data" / "smoke" / "labels.csv" rows = [] for p in range(N_PATIENTS): # Patient-level traits: both samples from one patient share them, which is # what makes grouped splitting matter — a per-row split would leak. base_age = rng.gauss(62, 11) site = rng.choice(SITES) frailty = rng.gauss(0, 1) for e in range(EYES): age = base_age + rng.gauss(0, 1.5) iop = rng.gauss(16 + 2.2 * frailty, 2.8) # intraocular pressure cdr = min(0.95, max(0.05, rng.gauss(0.45 + 0.09 * frailty, 0.11))) # cup/disc rnfl = rng.gauss(95 - 7 * frailty, 9) # retinal nerve fibre layer noise = rng.gauss(0, 1) logit = -6.0 + 0.045 * age + 0.20 * iop + 4.2 * cdr - 0.035 * rnfl + 0.5 * noise pos = rng.random() < 1 / (1 + math.exp(-logit)) rows.append({ "sample_id": f"S{p:03d}_{e}", "patient_id": f"P{p:03d}", "age": round(age, 1), "iop": round(iop, 1), "cdr": round(cdr, 3), "rnfl": round(rnfl, 1), "noise_feat": round(noise, 3), "site": site, "notes": "routine screening" if not pos else "referred", "diagnosis": "glaucoma" if pos else "healthy", }) out.parent.mkdir(parents=True, exist_ok=True) with out.open("w", newline="") as fh: w = csv.DictWriter(fh, fieldnames=list(rows[0])) w.writeheader() w.writerows(rows) pos = sum(r["diagnosis"] == "glaucoma" for r in rows) print(f"wrote {out} rows={len(rows)} patients={N_PATIENTS} positives={pos} ({pos/len(rows):.0%})")