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:
+19
-1
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user