Isotonic calibration layer — validated leave-one-fold-out, shipped

The CV harness gained a calibration experiment: per-outcome isotonic
maps (pool-adjacent-violators) fit walk-forward on the five folds and
judged leave-one-fold-out against a pre-registered bar. It cleared it:
LOFO ECE improves 16% (0.0146 → 0.0122) with RPS slightly better too
(0.1724 → 0.1722). The final maps (fit on all folds) are committed as
a research artifact, embedded into ratings.json by buildRatings when
the shipped config matches, and applied in matchMatrix by scaling the
score-matrix outcome regions — scorelines, Monte Carlo and displayed
probabilities all stay mutually consistent. Without the field, output
is bit-identical (golden tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:08:20 +02:00
parent 005b4d72ca
commit 88afbfad74
6 changed files with 1714 additions and 5 deletions
+19 -1
View File
@@ -3,7 +3,7 @@
// World-Football-Elo rating per team. Then calibrates the goals model
// (goals-per-Elo slope, mean goals) on the modern era and MLE-fits Dixon-Coles
// rho. The runtime re-uses these ratings + params to predict and simulate.
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { canonicalTeam, ALL_TEAMS } from '../src/lib/teams';
@@ -160,6 +160,23 @@ function main(): void {
teamDc.mu = Math.round(teamDc.mu * 1e4) / 1e4;
teamDc.homeMult = Math.round(teamDc.homeMult * 1e4) / 1e4;
// Isotonic calibration maps — research artifact from scripts/cvSearch.ts
// (committed; only valid while the shipped config matches its fit config).
let calibration: unknown = null;
const calFile = join(ROOT, 'data', 'calibration-maps.json');
if (existsSync(calFile)) {
const cal = JSON.parse(readFileSync(calFile, 'utf8')) as {
config: { xi: number; k: number; w: number; m: number; beta: number };
maps: unknown;
};
const c = cal.config;
if (c.xi === TEAMDC_XI && c.k === TEAMDC_SHRINK_K && c.w === ENSEMBLE_W && c.m === FAST_M && c.beta === FAST_BETA) {
calibration = cal.maps;
} else {
console.warn('calibration-maps.json was fit for a different config — skipping (rerun scripts/cvSearch.ts)');
}
}
const out = {
generatedAt: new Date().toISOString(),
asOf: lastDate,
@@ -178,6 +195,7 @@ function main(): void {
fastBeta: FAST_BETA,
fastM: FAST_M,
teamDcFit: { xi: TEAMDC_XI, shrinkK: TEAMDC_SHRINK_K, windowYears: FIT_WINDOW_YEARS },
...(calibration ? { calibration } : {}),
ratings,
};