Accuracy panel has no scoring branch and goes quieter once it has data #11
Labels
No labels
architecture
cleanup
dependencies
enhancement
needs-info
needs-triage
performance
priority: high
priority: medium
ready-for-agent
ready-for-human
reliability
security
testing
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
solvreven/FPL#11
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
backend/web/calibration.pydefinesbrier_score,log_loss,calibration_curveanddisagreement_tally, all unit-tested.accuracy_reportcalls none of them.
Below
MIN_RESOLVED(20) it writes a refusal and a reason on each metric. At orabove it, the function does nothing at all — so the metrics keep
value=None,keep the
AWAITING_RESULTSbanner, and theirdetailstring goes empty.The panel gets quieter at the moment it first has enough data to say
something. Ten fixtures are resolved as of 2026-08-27, so this is roughly one
gameweek away from being visible.
Found because the self-dating gate in
tests/web/test_calibration.pycame due:it asserted
resolved_fixtures == 0with the note "a fixture has resolved —update this test", and GW1 resolved it. The gate worked exactly as designed.
test_the_panel_has_no_scoring_branch_yetnow pins this state and is written tofail the moment scoring is wired, so the gap is asserted rather than
rediscovered.
Not a quick fix: scoring needs stored predictions to compare against, which
is what
data/pit/snapshots hold. That is a real build, and it should not bebolted onto an unrelated change.
The scoring helpers are ready, but the prediction/outcome join needs a declared contract. Please specify:
MIN_RESOLVED = 20counted in fixtures, player-fixtures, or another observation unit?These choices materially change the Brier/log-loss sample and cannot be recovered safely from the current placeholder branch.
Answering all five. These are the right questions — the join is where a scoring harness silently grades the wrong thing.
1. The prediction is the latest PIT snapshot before the DEADLINE, not before kickoff.
The deadline is when the squad locks and the decision is made; that is the moment the prediction was acted on. A snapshot taken between the deadline and kickoff has seen team news the decision could not use, and grading against it flatters the model. This is the vaastav
ep_thistrap the snapshots exist to prevent — their historical file carries a field scraped after the gameweek, so any backtest touching it sees the future.data/pit/<stamp>/manifest.jsonalready carriescaptured_atandnext_gw, so the selection is: newest snapshot wherenext_gw == Nandcaptured_at <= deadline(N). There are 14 snapshots; GW1 has seven and GW2 six, so a "latest before deadline" rule is not hypothetical — it picks a different one from "any snapshot for that gameweek".Record WHICH snapshot each score came from in the report. A number that cannot name its input is the thing this repo keeps finding.
2. Canonical fields.
backtest_rpsmetric): predicted = the three-way from the snapshot's solved market/model lambdas; observed =team_h_score/team_a_scorefrom the resolved fixture. Note the snapshot stores our fit AND the market — score ours, and report the market alongside as the benchmark, never blend them.cs_h/cs_a; observed = opponent score == 0 and the fixture finished.start_prob; observed fromevent/{gw}/live/minutes. This already has a harness (backtest_minutes.py, Brier 0.0894 against a 0.1926 base rate) — the panel should read the same definition rather than inventing a second one.p_score_sim, against realised goals.scripts/score_goalscorer.pyalready does this and the chore runner calls it; the panel should consume its output, not re-derive.3. Postponed and doubles.
4. Aggregate to FIXTURE level for the match metrics; keep player-fixture for minutes and goalscorer. They are different claims: "Arsenal 2-1" is one prediction, "Saka starts" is 22 per fixture. Mixing them into one Brier gives the player metrics ~20x the weight of the match ones and the number stops meaning anything.
5.
MIN_RESOLVED = 20in FIXTURES, for the match metrics. At 10 fixtures a gameweek that is two rounds, which is the right order for "enough to say anything". For player-level metrics the floor should be separate and much higher —backtest_minutesruns on 23,884 rows for a reason.One thing to fix while you are in there.
accuracy_reportcurrently writes a refusal on each metric below the floor and does nothing at or above it, so the metrics keepvalue=None, keep the AWAITING_RESULTS banner, and theirdetailgoes empty — the panel gets quieter the moment it has enough data to speak.tests/web/test_calibration.py::test_the_panel_has_no_scoring_branch_yetpins that gap and is written to fail the moment scoring is wired, so it will tell you when this issue is done.Partial progress in
bf7e50a(pushed):NOT_SCOREDwith the count ("50 results are in; nothing scores them against the snapshots yet"). Before, they kept theAWAITING_RESULTSdataclass default with an empty detail, so the panel said "forecasts not yet scored" with 50 results in.scripts/score_goalscorer.pybanks indata/scores, throughscore_history.verdict. A round whose market was priced after its deadline therefore stays out here, as it does in the verdict. On screen, for GW5: 0.2439 log loss on 237 appearances, beside the corrected market (0.2508, Norsk Tipping with the margin removed) and the base rate (0.2792) on the same round. The tile says how many more valid rounds are needed before one source is preferred. It is bound to its metric by name, not by list position. Tests read an emptydata/scoresunless they pass their own, so no result depends on whether a round has been scored on the machine running them.accuracy_reportstill computes no score itself, andtest_the_panel_has_no_scoring_branch_yetstill pins that.Leaving this open for those two metrics.
🤖 Generated with Claude Code
Both remaining metrics are wired and pushed. Commits
1e3a4cb..ff6e6d1; the design is indocs/superpowers/specs/2026-09-26-match-scoring-design.md.The contract from 30 Aug assumed something the snapshots don't hold. Answer 2 said the snapshot stores "our fit AND the market". It stores neither per fixture:
p_csis a player's chance of clean-sheet points, minutes included.engine.project_fixtureand thrown away.Offered a choice between storing forecasts from GW6 on and refitting, the user chose to refit:
scripts/score_matches.pyrefits today's model throughpipeline.strengths_depson eachpit.choose_snapshotsnapshot's frozen results and team list. The path is always named, becauseNonewould read the live file.data/scores/matches_gw{N}.jsonwith the scorer's code digest.A scheduler job (
match_scores) rescores whenever a played round lacks a bank from the code on disk, andafter_gameweek.pyruns it as step 4/4. The panel says "refit" on every tile and no longer promises "never recomputed". The rest of your five answers stand:pit.choose_snapshot);A fresh-context review found no Critical issues and five Important ones, all fixed in
60ab678:The market-age rule (
44159af). GW1's consensus was priced 22 days before its deadline, with the transfer window open. A market is now the benchmark only if it was priced in the week before the deadline; it is kept for the record either way. This was decided after the first run, and it narrowed our lead.Results (lower is better; GW1, GW2, GW5; GW3 and GW4 have no snapshot that can stand for them):
All paired bootstrap intervals span zero, so the reading is level with the market, as for the goalscorer.
Still not wired (they were out of the spec's scope):
calibration_curveanddisagreement_tally, and the minutes metric from your answer 2. As far as the two metrics this issue names go, #11 is done. Closing it is the user's call.🤖 Generated with Claude Code
The rest of the panel is done, in
e5c1f77(pushed). It now scores five things, not three, and every comparison carries a verdict.scripts/score_players.pyreads the forecast each snapshot stored before its deadline and scores it against what each player scored. FPL's ownep_nextfrom the same frozen bootstrap is scored beside it, and used only when that bootstrap was for the round. GW1/2/5, 1,267 player-rounds: MAE 1.737 v FPL 1.758, level (95% interval −0.078 to +0.038). Ranking is ahead of FPL in all three rounds (rank correlation 0.53/0.55/0.68 v 0.40/0.41/0.63). The 80% range held 85%.start_prob× availability), against "started last round". GW2 and GW5, 1,269 players: Brier 0.0968 v 0.1151, ahead (−0.032 to −0.005). GW1 has no round before it and says so.calibration.paired_verdict). A difference is called only when the 95% interval excludes zero. The match tiles now read "level with the market".test_the_panel_computes_no_score_itselfstill holds.The scheduler keeps both new banks current, and
after_gameweek.pyruns the player scorer as step 5/5. The commit message records sixteen mutation cuts, all red.Still open, deliberately: accuracy of the fit 2–5 rounds ahead (the window beyond the imminent round) is queued, not built. GW3 and GW4 stay out of every total, because no snapshot stands for them.
🤖 Generated with Claude Code