v3 · Colab run · part 6 of 7
Scenarios
The notebook exactly as it ran on Colab's T4 GPU, with its real outputs.
Download the executed notebook
Left: what & whyRight: the code and its real output
6. Scenarios: every combination, honestly scored
Outputs (H3, H5): single models, a plain average, a weight-optimised average (weights minimise log-loss), probabilities mixed with the regression score, and the stacker.
Decoders (how an output becomes a tier):
- A. class weights: multiply the 4 probabilities by tuned weights and take the largest (v1's method),
- B. cut-points (H4): expected tier Σ p·tier (or the regression score), cut at 3 tuned thresholds,
- C. per-metro cut-points (H7): one set of thresholds per metro (tried on the 4 best B scenarios).
Scoring (H6): every decoder is tuned with sample weights that make the train metro mix look like the test metro mix. It is then cross-fitted: tuned on 4 folds' out-of-fold predictions and scored on the 5th, so no scenario wins by tuning on its own answers. The main column, "test-weighted", estimates the score on the test set's metro mix. "plain" is the unweighted version.
prog.start("scenario table") TIERS = np.arange(1, K + 1) share_tr = np.bincount(MT_tr, minlength=MT_tr.max() + 1) / n_tr share_te = np.bincount(MT_te, minlength=MT_tr.max() + 1) / len(MT_te) W_tr = (share_te / np.maximum(share_tr, 1e-12))[MT_tr] print("metro weights (test share / train share):", np.round(share_te / np.maximum(share_tr, 1e-12), 2)) def fit_cw(P, yy, w, mt): W = np.ones(K) for _ in range(3): for k in range(1, K): grid = W[k] * np.exp(np.linspace(-0.4, 0.4, 41)) sc = [fast_f1(yy, (P * np.r_[W[:k], g_, W[k + 1:]]).argmax(1), w) for g_ in grid] W[k] = grid[int(np.argmax(sc))] return W def apply_cw(P, W, mt): return (P * W).argmax(1) def fit_cuts(s, yy, w, mt=None): cuts = np.quantile(s, np.cumsum(np.bincount(yy, minlength=K) / len(yy))[:-1]) for _ in range(3): for i in range(K - 1): lo = cuts[i - 1] + 1e-6 if i else s.min() hi = cuts[i + 1] - 1e-6 if i < K - 2 else s.max() grid = np.linspace(max(lo, cuts[i] - 0.4), min(hi, cuts[i] + 0.4), 61) sc = [fast_f1(yy, np.digitize(s, np.r_[cuts[:i], g_, cuts[i + 1:]]), w) for g_ in grid] cuts[i] = grid[int(np.argmax(sc))] return cuts def apply_cuts(s, cuts, mt): return np.digitize(s, cuts) def fit_metro_cuts(s, yy, w, mt): g0 = fit_cuts(s, yy, w) C = np.tile(g0, (MT_tr.max() + 1, 1)) pred = np.digitize(s, g0) cm = conf(yy, pred, w) for _ in range(2): for m_ in np.unique(mt): ix = mt == m_ for i in range(K - 1): rest = cm - conf(yy[ix], pred[ix], w[ix]) lo = C[m_, i - 1] + 1e-6 if i else s.min() hi = C[m_, i + 1] - 1e-6 if i < K - 2 else s.max() grid = np.linspace(max(lo, C[m_, i] - 0.5), min(hi, C[m_, i] + 0.5), 41) best_g, best_s = C[m_, i], -1 for g_ in grid: p_ = np.digitize(s[ix], np.r_[C[m_, :i], g_, C[m_, i + 1:]]) sc = f1_from_conf(rest + conf(yy[ix], p_, w[ix])) if sc > best_s: best_g, best_s = g_, sc C[m_, i] = best_g pred[ix] = np.digitize(s[ix], C[m_]) cm = rest + conf(yy[ix], pred[ix], w[ix]) return C def apply_metro_cuts(s, C, mt): out = np.empty(len(s), int) for m_ in np.unique(mt): ix = mt == m_ out[ix] = np.digitize(s[ix], C[m_]) return out def crossfit(o, fit, apply): pred = np.empty(n_tr, int) for f in range(FOLDS): m_ = fold_id == f prm = fit(o[~m_], y[~m_], W_tr[~m_], MT_tr[~m_]) pred[m_] = apply(o[m_], prm, MT_tr[m_]) return fast_f1(y, pred, W_tr), fast_f1(y, pred) def opt_weights(models): Ps = [OOF[m] for m in models] def loss(z): w = np.exp(z) / np.exp(z).sum() return log_loss(y, np.clip(sum(wi * p for wi, p in zip(w, Ps)), 1e-7, 1)) z = minimize(loss, np.zeros(len(Ps)), method="Nelder-Mead", options={"maxiter": 300}).x return np.exp(z) / np.exp(z).sum() candidates = {} # name -> (oof output, test output, kind) for m_ in PROB_MODELS + ["stack"]: candidates[m_] = (OOF[m_], TEST[m_], "prob") for m_ in REG_MODELS: candidates[m_] = (OOF[m_][:, 0], TEST[m_][:, 0], "score") if len(PROB_MODELS) > 1: candidates["avg(" + "+".join(PROB_MODELS) + ")"] = (np.mean([OOF[m_] for m_ in PROB_MODELS], 0), np.mean([TEST[m_] for m_ in PROB_MODELS], 0), "prob") wb = opt_weights(PROB_MODELS) print("optimised blend weights:", dict(zip(PROB_MODELS, wb.round(3)))) candidates["opt_blend"] = (sum(wi * OOF[m_] for wi, m_ in zip(wb, PROB_MODELS)), sum(wi * TEST[m_] for wi, m_ in zip(wb, PROB_MODELS)), "prob") if REG_MODELS: reg_o = np.mean([OOF[m_][:, 0] for m_ in REG_MODELS], 0) reg_t = np.mean([TEST[m_][:, 0] for m_ in REG_MODELS], 0) if len(REG_MODELS) > 1: candidates["avg(" + "+".join(REG_MODELS) + ")"] = (reg_o, reg_t, "score") reg_opts = {m_: (OOF[m_][:, 0], TEST[m_][:, 0]) for m_ in REG_MODELS} if len(REG_MODELS) > 1: reg_opts["avg_reg"] = (reg_o, reg_t) for src in [c for c in ("opt_blend", "stack") if c in candidates]: for rname, (ro, rt) in reg_opts.items(): for a in (0.3, 0.5, 0.7): candidates[f"{src} + {rname} (a={a})"] = (a * (candidates[src][0] @ TIERS) + (1 - a) * ro, a * (candidates[src][1] @ TIERS) + (1 - a) * rt, "score") rows = [] for name, (o, t, kind) in candidates.items(): s_o, s_t = (o @ TIERS, t @ TIERS) if kind == "prob" else (o, t) if kind == "prob": rows.append(dict(scenario=name, decoder="A class weights", out=(o, t), fit=fit_cw, apply=apply_cw, score=crossfit(o, fit_cw, apply_cw))) rows.append(dict(scenario=name, decoder="B cut-points", out=(s_o, s_t), fit=fit_cuts, apply=apply_cuts, score=crossfit(s_o, fit_cuts, apply_cuts))) print(f" scored: {name}", flush=True) for r in sorted([r for r in rows if r["decoder"].startswith("B")], key=lambda r: -r["score"][0])[:4]: rows.append(dict(scenario=r["scenario"], decoder="C per-metro cut-points", out=r["out"], fit=fit_metro_cuts, apply=apply_metro_cuts, score=crossfit(r["out"][0], fit_metro_cuts, apply_metro_cuts))) print(f" scored per-metro: {r['scenario']}", flush=True) table = pd.DataFrame([{"scenario": r["scenario"], "decoder": r["decoder"], "CV macro F1 (test-weighted)": round(r["score"][0], 4), "CV macro F1 (plain)": round(r["score"][1], 4)} for r in rows]).sort_values("CV macro F1 (test-weighted)", ascending=False).reset_index(drop=True) prog.finish() display(table)
▶ scenario table [███████████████████████████░░░ 19/21 (90%)]
metro weights (test share / train share): [1.09 0.85 1.16 0.97 0.95 1.14 0.69 1.09 1.04 0.69 0.91 0.85 1. 0.87
2.24 1.12]
optimised blend weights: {'xgb': np.float64(0.551), 'cat': np.float64(0.449)}
scored: xgb
scored: cat
scored: stack
scored: xgb_reg
scored: avg(xgb+cat)
scored: opt_blend
scored: opt_blend + xgb_reg (a=0.3)
scored: opt_blend + xgb_reg (a=0.5)
scored: opt_blend + xgb_reg (a=0.7)
scored: stack + xgb_reg (a=0.3)
scored: stack + xgb_reg (a=0.5)
scored: stack + xgb_reg (a=0.7)
scored per-metro: opt_blend + xgb_reg (a=0.7)
scored per-metro: opt_blend + xgb_reg (a=0.5)
scored per-metro: stack
scored per-metro: stack + xgb_reg (a=0.7)
✓ scenario table: took 6.7 min | [████████████████████████████░░ 20/21 (95%)] | elapsed 35.9 min | ETA ~0 min + 1 steps not timed yet
| scenario | decoder | CV macro F1 (test-weighted) | CV macro F1 (plain) | |
|---|---|---|---|---|
| 0 | stack | A class weights | 0.7214 | 0.7221 |
| 1 | opt_blend + xgb_reg (a=0.7) | B cut-points | 0.7199 | 0.7205 |
| 2 | opt_blend + xgb_reg (a=0.5) | B cut-points | 0.7198 | 0.7205 |
| 3 | stack | B cut-points | 0.7194 | 0.7200 |
| 4 | opt_blend + xgb_reg (a=0.5) | C per-metro cut-points | 0.7193 | 0.7199 |
| 5 | stack + xgb_reg (a=0.7) | B cut-points | 0.7190 | 0.7197 |
| 6 | opt_blend + xgb_reg (a=0.7) | C per-metro cut-points | 0.7188 | 0.7193 |
| 7 | stack + xgb_reg (a=0.5) | B cut-points | 0.7186 | 0.7193 |
| 8 | opt_blend + xgb_reg (a=0.3) | B cut-points | 0.7185 | 0.7193 |
| 9 | stack + xgb_reg (a=0.7) | C per-metro cut-points | 0.7185 | 0.7193 |
| 10 | stack | C per-metro cut-points | 0.7183 | 0.7190 |
| 11 | opt_blend | B cut-points | 0.7178 | 0.7185 |
| 12 | avg(xgb+cat) | A class weights | 0.7177 | 0.7184 |
| 13 | stack + xgb_reg (a=0.3) | B cut-points | 0.7177 | 0.7183 |
| 14 | opt_blend | A class weights | 0.7175 | 0.7182 |
| 15 | avg(xgb+cat) | B cut-points | 0.7174 | 0.7181 |
| 16 | xgb | B cut-points | 0.7169 | 0.7176 |
| 17 | xgb | A class weights | 0.7158 | 0.7168 |
| 18 | cat | A class weights | 0.7144 | 0.7152 |
| 19 | cat | B cut-points | 0.7139 | 0.7146 |
| 20 | xgb_reg | B cut-points | 0.7138 | 0.7146 |