From 17ec56bceb2b7f683ce66d17996fc7a79f6c4571 Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Fri, 12 Jun 2026 01:19:37 +0200 Subject: [PATCH] Model diary: a transparency log of every model update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/features/methodology/MethodologyPage.tsx | 49 +++++++++++++++++++- src/lib/i18n/de.ts | 4 ++ src/lib/i18n/en.ts | 4 ++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/features/methodology/MethodologyPage.tsx b/src/features/methodology/MethodologyPage.tsx index b987193..eaa4f68 100644 --- a/src/features/methodology/MethodologyPage.tsx +++ b/src/features/methodology/MethodologyPage.tsx @@ -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 ( + + + + {t.methodology.diary.title} + + +
    + {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 ( +
  • + {kickoffDay(e.t)} {kickoffTime(e.t)} + {e.label} + {mover && Math.abs(mover.delta) >= 0.002 && ( + 0 ? 'text-accent' : 'text-danger'}`}> + {teamFlag(mover.team)} {mover.team} {mover.delta > 0 ? '+' : ''}{(mover.delta * 100).toFixed(1)} pp + + )} +
  • + ); + })} +
+

{t.methodology.diary.note}

+
+
+ ); +} + 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() { + + {t.methodology.limits.heading} diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts index 1951f18..f279af7 100644 --- a/src/lib/i18n/de.ts +++ b/src/lib/i18n/de.ts @@ -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.", diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index f90477c..520542a 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -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.",