""" Calibration Report — are the model's probabilities "kusursuz"? ============================================================= "Flawless probability" has a precise technical meaning: CALIBRATION. When the model says 60%, the event must happen ~60% of the time. This measures exactly that for the leak-free MS (1X2) model, and shows how much isotonic calibration improves it. Metrics: * Reliability table: bin predicted prob -> avg predicted vs ACTUAL frequency. Calibrated = avg_pred ≈ actual in every bin (gap ≈ 0). * ECE (Expected Calibration Error): weighted mean |pred - actual|. Lower=better. * Brier score, Log-loss: overall probability accuracy. Lower=better. Time-split (no leakage): train 70% -> fit isotonic on next 15% -> test last 15%. Usage: python scripts/calibration_report.py """ from __future__ import annotations import os, sys import numpy as np, pandas as pd, xgboost as xgb from sklearn.isotonic import IsotonicRegression if sys.stdout and hasattr(sys.stdout, "reconfigure"): try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv") META = {"match_id","home_team_id","away_team_id","league_id","mst_utc", "score_home","score_away","ht_score_home","ht_score_away"} LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals", "squad_diff","home_squad_quality","away_squad_quality", "referee_home_bias","referee_avg_goals"} PARAMS = {"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05, "subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0} def reliability(probs, y, nbins=10): """Pool one-vs-rest predictions; bin by predicted prob; compare to actual freq.""" P = probs.reshape(-1) hit = np.zeros((len(y), probs.shape[1])) hit[np.arange(len(y)), y] = 1.0 H = hit.reshape(-1) edges = np.linspace(0, 1, nbins + 1) rows, ece, N = [], 0.0, len(P) for i in range(nbins): lo, hi = edges[i], edges[i+1] m = (P >= lo) & (P < hi) if i < nbins-1 else (P >= lo) & (P <= hi) if m.sum() == 0: continue ap, af, n = P[m].mean(), H[m].mean(), int(m.sum()) rows.append((f"{int(lo*100)}-{int(hi*100)}%", n, ap, af, af-ap)) ece += (n / N) * abs(ap - af) return rows, ece def brier(probs, y): oh = np.zeros_like(probs); oh[np.arange(len(y)), y] = 1.0 return float(np.mean(np.sum((probs - oh) ** 2, axis=1))) def logloss(probs, y): p = np.clip(probs[np.arange(len(y)), y], 1e-9, 1) return float(-np.mean(np.log(p))) def main(): df = pd.read_csv(CSV, low_memory=False).sort_values("mst_utc").reset_index(drop=True) sh = pd.to_numeric(df["score_home"], errors="coerce") sa = pd.to_numeric(df["score_away"], errors="coerce") ok = sh.notna() & sa.notna() df, sh, sa = df[ok].reset_index(drop=True), sh[ok.values].values, sa[ok.values].values y = np.where(sh > sa, 0, np.where(sh == sa, 1, 2)) feats = [c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY] X = df[feats].apply(pd.to_numeric, errors="coerce").fillna(0.0).values n = len(df); a, b = int(n*0.70), int(n*0.85) Xtr, ytr = X[:a], y[:a] Xca, yca = X[a:b], y[a:b] Xte, yte = X[b:], y[b:] print(f"{n:,} matches | train {len(ytr):,} / calib {len(yca):,} / test {len(yte):,} (time-split)") bst = xgb.train(PARAMS, xgb.DMatrix(Xtr, label=ytr), num_boost_round=300) raw_ca = bst.predict(xgb.DMatrix(Xca)) raw_te = bst.predict(xgb.DMatrix(Xte)) # isotonic per class (fit on calib), apply to test, renormalize isos = [] for k in range(3): ir = IsotonicRegression(out_of_bounds="clip", y_min=0, y_max=1) ir.fit(raw_ca[:, k], (yca == k).astype(float)) isos.append(ir) cal_te = np.column_stack([isos[k].predict(raw_te[:, k]) for k in range(3)]) cal_te = np.clip(cal_te, 1e-6, 1) cal_te = cal_te / cal_te.sum(axis=1, keepdims=True) for name, P in (("RAW (kalibrasyonsuz)", raw_te), ("ISOTONIC KALİBRELİ", cal_te)): rows, ece = reliability(P, yte) print(f"\n{'='*64}\n{name}\n{'='*64}") print(f" {'tahmin bandı':<12}{'n':>7}{'ort.tahmin':>12}{'gerçek':>9}{'fark':>8}") for band, nn, ap, af, gap in rows: print(f" {band:<12}{nn:>7}{100*ap:>11.1f}%{100*af:>8.1f}%{100*gap:>+7.1f}") print(f" ECE={100*ece:.2f}% Brier={brier(P,yte):.4f} LogLoss={logloss(P,yte):.4f}") print("\nOKUMA: 'fark' ≈ 0 ise olasılıklar KUSURSUZ (söylediği %X gerçekten %X).") print("ECE/Brier/LogLoss düştüyse kalibrasyon işe yaradı. Bu kalibre olasılıklar,") print("maçın olası sonuçlarını dürüstçe gösterir — kayıp-minimizasyonun temeli budur.") if __name__ == "__main__": main()