Model diary: a transparency log of every model update
The Methodology page lists each recompute (the finished match that triggered it) with the biggest title-odds move it caused — the model's thinking made visible, feeding the openness story. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,59 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { Activity, Dices, Gauge, TrendingUp } from 'lucide-react';
|
||||
import { Activity, BookOpen, Dices, Gauge, TrendingUp } from 'lucide-react';
|
||||
import { PageHeader } from '@/components/ui/PageHeader';
|
||||
import { Card, CardBody, CardHeader } from '@/components/ui/Card';
|
||||
import { Term } from '@/components/ui/Term';
|
||||
import { ReliabilityDiagram } from '@/components/ReliabilityDiagram';
|
||||
import { teamFlag } from '@/lib/teams';
|
||||
import { useTournamentStore } from '@/stores/tournamentStore';
|
||||
import { fmt, fmtSplit, useFormat, useT } from '@/lib/i18n';
|
||||
import type { BacktestReport } from '@/lib/types';
|
||||
|
||||
const STEP_ICONS = [TrendingUp, Activity, Gauge, Dices] as const;
|
||||
|
||||
/** Transparency log: every model recompute and the title odds it shifted. */
|
||||
function ModelDiary() {
|
||||
const { t, kickoffDay, kickoffTime } = useFormat();
|
||||
const history = useTournamentStore((s) => s.model?.history ?? []);
|
||||
if (history.length < 2) return null;
|
||||
const entries = history.slice(-9).reverse();
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center gap-2">
|
||||
<BookOpen size={15} className="text-accent" />
|
||||
<span className="font-display font-bold text-ink">{t.methodology.diary.title}</span>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ul className="space-y-2">
|
||||
{entries.map((e, idx) => {
|
||||
const prev = history[history.length - 1 - (idx + 1)];
|
||||
let mover: { team: string; delta: number } | null = null;
|
||||
if (prev) {
|
||||
const before = new Map(prev.top.map((x) => [x.team, x.champion]));
|
||||
for (const x of e.top) {
|
||||
const d = x.champion - (before.get(x.team) ?? 0);
|
||||
if (!mover || Math.abs(d) > Math.abs(mover.delta)) mover = { team: x.team, delta: d };
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li key={e.t} className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<span className="w-32 shrink-0 text-xs text-faint">{kickoffDay(e.t)} {kickoffTime(e.t)}</span>
|
||||
<span className="truncate text-ink-soft">{e.label}</span>
|
||||
{mover && Math.abs(mover.delta) >= 0.002 && (
|
||||
<span className={`tnum ml-auto shrink-0 text-xs ${mover.delta > 0 ? 'text-accent' : 'text-danger'}`}>
|
||||
{teamFlag(mover.team)} {mover.team} {mover.delta > 0 ? '+' : ''}{(mover.delta * 100).toFixed(1)} pp
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<p className="mt-3 border-t border-line pt-2 text-xs text-faint">{t.methodology.diary.note}</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RpsBar({ label, rps, best, highlight }: { label: string; rps: number; best: number; highlight?: boolean }) {
|
||||
const t = useT();
|
||||
// shorter bar = better (lower RPS). Scale relative to the worst (uniform ~0.24).
|
||||
@@ -138,6 +183,8 @@ export function MethodologyPage() {
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<ModelDiary />
|
||||
|
||||
<Card>
|
||||
<CardHeader><span className="font-display font-bold text-ink">{t.methodology.limits.heading}</span></CardHeader>
|
||||
<CardBody>
|
||||
|
||||
@@ -314,6 +314,10 @@ export const de: Dict = {
|
||||
pointTooltip: "vorhergesagt {predicted}% → eingetreten {observed}% ({count})",
|
||||
caption: "Die Punkte liegen nah an der Diagonale. Das heißt, das Modell ist ehrlich: Sagt es 30%, tritt das Ergebnis in rund 30% der Fälle ein. Der Kalibrierungsfehler (ECE) liegt bei {ece} — niedriger ist besser, unter 0,05 ist hervorragend.",
|
||||
},
|
||||
diary: {
|
||||
title: "Modell-Tagebuch",
|
||||
note: "Jedes beendete Spiel bewertet die Teams neu und startet die Simulation erneut. Dieses Log zeigt jede Aktualisierung und die größte Verschiebung der Titelchancen.",
|
||||
},
|
||||
limits: {
|
||||
heading: "Ehrliche Grenzen",
|
||||
notAdvice: "Das sind Modell-Wahrscheinlichkeiten, keine Wettempfehlung. Das Modell nutzt keine Buchmacherquoten, also behaupten wir nicht, den Markt zu schlagen. Scharfe Wettmärkte sind nach wie vor die besten Prognosen, die es gibt.",
|
||||
|
||||
@@ -313,6 +313,10 @@ export const en = {
|
||||
pointTooltip: "predicted {predicted}% → happened {observed}% ({count})",
|
||||
caption: "The points hug the diagonal. That means the model is honest: when it says 30%, the outcome happens about 30% of the time. The calibration error (ECE) is {ece} — lower is better, and under 0.05 is excellent.",
|
||||
},
|
||||
diary: {
|
||||
title: "Model diary",
|
||||
note: "Every finished match re-rates the teams and re-runs the simulation. This log shows each update and the biggest title-odds move it caused.",
|
||||
},
|
||||
limits: {
|
||||
heading: "Honest limits",
|
||||
notAdvice: "These are model probabilities, not betting advice. The model does not use bookmaker odds, so we make no claim to beat the market. Sharp betting markets are still the best forecasters there are.",
|
||||
|
||||
Reference in New Issue
Block a user