Receipts
Projection accuracy
42 matches · 1,312 predictions · 10 May 2026 – 29 May 2026
A projection you can't check is just an opinion. Every match we project is logged before kickoff and, once it has been played, graded against what actually happened. Below is the model's track record: how often each call landed, and its Brier score — the mean squared error of a probability, where lower is better and 0 is perfect.
| Market | N | Predicted | Actual | Brier |
|---|---|---|---|---|
| Home win | 42 | 40% | 33% | 0.239 |
| Draw | 42 | 27% | 36% | 0.240 |
| Away win | 42 | 33% | 31% | 0.217 |
| Both teams score | 42 | 58% | 50% | 0.258 |
| Over 2.5 goals | 42 | 55% | 33% | 0.281 |
| Over 3.5 goals | 42 | 35% | 19% | 0.191 |
| Anytime scorer | 530 | 8% | 5% | 0.051 |
| Anytime assist | 530 | 5% | 4% | 0.037 |
| All markets | 1,312 | — | — | 0.081 |
Calibration
When the model says 30%, does it happen 30% of the time? Each point groups predictions by the probability we gave them; the closer a point sits to the diagonal, the better calibrated the model. Points above the line are events that happened more often than predicted, below the line less often.
Track record for model v3.0.0, v3.1.0. It is young: early figures move a lot as matches settle, and markets or buckets with few predictions are noisy. We publish it anyway, and it updates as results come in.
Data quality
2025–26 season · last checked 14 Jun 2026
Sources
Players
- Goals / gm
- Goals per game across the season.
- Shots on target / gm
- Shots on target per game.
- Assists / gm
- Assists per game across the season.
- Key passes / gm
- Passes that directly create a shot, per game.
- Passes / gm
- Total passes attempted per game.
- Pass accuracy
- Completed passes divided by attempts.
- Dribbles (succ.) / gm
- Successful dribbles per game.
- Dribble success
- Successful dribbles divided by attempts.
- Tackles / gm
- Tackles attempted per game.
- Interceptions / gm
- Interceptions per game.
- Duels won / gm
- Duels won per game (ground + aerial combined).
- Duel win rate
- Duels won divided by duels contested.
- Saves / gm
- Saves per game (GK only).
- Save %
- Saves divided by (saves + goals conceded), 0 to 1 (GK only).
- Goals conceded / gm
- Goals conceded per game (GK only).
- Penalty saves / gm
- Penalty saves per game (GK only).
- Clean sheet %
- Fraction of appearances with zero goals conceded (GK only).
- Shots / gm
- Shots attempted per game (SofaScore where covered — it counts blocked shots, which API-Football omits; API-Football elsewhere).
- xG / 90
- Expected goals per 90.
- xA / 90
- Expected assists per 90.
- Big chances created / 90
- Big chances created per 90. Zero when unmapped.
- Big chances missed / 90
- Big chances missed per 90, an indicator of clinical-finishing failures.
- Progressive carries / 90
- Carries that move the ball significantly toward the opponent's goal, per 90.
- Take-ons attempted / 90
- Take-ons attempted per 90.
- Take-ons succeeded / 90
- Take-ons succeeded per 90.
- Take-on success
- Take-ons won divided by take-ons attempted.
- Touches / 90
- Ball touches per 90.
- Tackles won / 90
- Tackles won per 90 (vs. tackles attempted). More meaningful signal than raw tackles.
- Blocks / 90
- Blocks of opponent shots or passes per 90.
- Clearances / 90
- Clearances per 90.
- Ball recoveries / 90
- Ball recoveries per 90.
- Aerial duels won / 90
- Aerial duels won per 90.
- Aerial win rate
- Aerial duels won divided by aerial duels contested.
- Errors leading to shot / 90
- Errors leading to a shot per 90. Coverage skewed defensive.
- xG per shot
- Total xG divided by total shots. Zero when no Understat coverage.
- Goals − xG / 90
- Goals minus xG per 90. Positive: clinical. Negative: wasteful.
- xGOT / 90
- Expected goals on target per 90, post-shot xG including shot placement.
- DefCon / 90
- FotMob's composite defensive index, per 90.
- Poss. won att. 3rd / 90
- Possessions won in the attacking third per 90, a high-press signal.
- Goals prevented / 90
- Goalkeeper goals-prevented per 90, xG-saved differential (GK only).
Teams
- goals_for
- Goals scored, total across the season.
- goals_against
- Goals conceded, total across the season.
- points
- League points accumulated.
- win_pct
- Share of matches won.
- draw_pct
- Share of matches drawn.
- loss_pct
- Share of matches lost.
- clean_sheet_pct
- Share of matches with zero goals conceded.
- failed_to_score_pct
- Share of matches the team failed to score.
- btts_pct
- Share of matches where both teams scored.
- over_2_5_pct
- Share of matches with three or more total goals.
- form_score
- Last-5-results score (0 to 100), weighted toward the most recent match.
- possession_pct
- Possession percentage (FBref squad `standard__poss`).
- interceptions_per_match
- Interceptions per match (FBref squad scope).
- tackles_won_per_match
- Tackles won per match (FBref squad scope).
- shots_for_per_match
- Shots taken per match (FBref squad scope).
- shots_against_per_match
- Shots conceded per match (FBref opponent scope).
- sot_for_per_match
- Shots on target per match (FBref squad scope).
- sot_against_per_match
- Shots on target conceded per match (FBref opponent scope).
- xg_for_per_match
- Team xG per match, summed from own shots.
- xga_per_match
- xG conceded per match, summed from opponent shots.
- xg_difference_per_match
- xG minus xGA per match. Positive: creating better chances than allowed.
- xg_overperformance
- Goals minus xG. Positive: clinical finishing or luck. Negative: wasteful.
- xga_overperformance
- xGA minus goals conceded. Positive: keeper bailing out. Negative: conceding better chances than xGA suggests.
- xpoints_per_match
- Expected points per match (Justice-table simulation). Big-5 and UEFA cups (2024+) only.
- xpoints_diff_per_match
- xPts minus actual points per match. Positive: unlucky and deserved more. Negative: overperformed actual results.
- xg_against_per_match
- Opposition xG conceded per match. FotMob primary (2024+), Understat fallback (Big-5, 2014+), ASA for MLS.
- xgot_against_per_match
- Opposition shot-quality on target conceded per match. FotMob only — null for older / non-FotMob seasons.
Sources attributed to the upstream feed. Bootroom aggregates per (player or team, league, season) and computes percentiles against the active pool.
A data product is only as good as its data. We check ours against FBref, an independent reference, on every release. Below is how our season totals compare per league — the average difference per player, where lower is better and 0 means we match exactly.
| League | Players | Minutes | Goals | Shots | Intercep. | Tackles |
|---|---|---|---|---|---|---|
| Premier League | 374 | ±11.6 | ±0.01 | ±2.0 | ±1.36 | ±2.45 |
| Bundesliga | 346 | ±13.5 | ±0.01 | ±1.4 | ±0.98 | ±1.64 |
| Primeira Liga | 338 | ±13.6 | ±0.02 | ±2.2 | ±2.22 | ±2.99 |
| Eredivisie | 333 | ±15.5 | ±0.01 | ±0.7 | ±1.05 | ±1.27 |
| Super Lig | 356 | ±16.1 | ±0.03 | ±0.8 | ±1.09 | ±1.24 |
| Championship | 539 | ±16.5 | ±0.01 | ±1.4 | ±1.30 | ±1.67 |
| La Liga | 383 | ±17.6 | ±0.03 | ±2.2 | ±1.46 | ±2.91 |
| Ligue 1 | 341 | ±18.4 | ±0.01 | ±1.4 | ±0.95 | ±1.75 |
| Serie A | 406 | ±20.9 | ±0.01 | ±1.6 | ±1.05 | ±2.07 |
Figures are the mean absolute difference per player (450+ minutes). Goals are near-exact everywhere; minutes land within a few matches; interceptions within about one. Tackles are checked against SofaScore — FBref doesn't publish a season tackles total — and SofaScore is itself validated against FBref's tackles-won. Leagues FBref does not cover aren't shown here; we hold those to our internal consistency checks instead.
League adjustment
Version 1.1 · calibrated 2026-07-01T13:55:45.429Z
Bootroom shows percentiles within a player's league. League adjustment rescales rate stats onto a Big-5-aggregate baseline, so a Championship or Eredivisie prospect can be read on the same axis as a Premier League player. The output is always a 1-SD band, never a single number, and the source-league quality tier is shown next to every adjusted value. Leagues without enough calibration data (tier C) are refused rather than guessed.
Two layers. First, a per-league strength factor λ_L from cross-league UEFA cup results (Bradley-Terry-style goal-difference fit, home advantage removed at −0.3 goals) blended 60% / 40% with a ClubElo per-country prior. EPL is the anchor at λ=1.0. The Big-5 baseline λ is the geometric mean of the five Big-5 lambdas.
Second, a per-stat correction factor cstat, from→to fit on the transition dataset: every player who appeared in two different Big-5 leagues in consecutive seasons, ≥600 minutes in both. Per-pair coefficients are used when n ≥ 20 in the train split; otherwise the stat-level aggregate is the fallback.
adjusted = raw × (λ_target / λ_source) × cstat, from→to
EPL is the 1.0 anchor. Big-5 leagues tier B; everything else tier C (uncalibrated).
| League | λ | Tier |
|---|---|---|
| Premier League | 1.000 | B |
| La Liga | 0.887 | B |
| Bundesliga | 0.877 | B |
| Serie A | 0.859 | B |
| Ligue 1 | 0.850 | B |
| Primeira Liga | 0.844 | B |
| Eredivisie | 0.780 | B |
| Jupiler Pro League | 0.773 | B |
| Süper Lig | 0.749 | B |
| Big-5 baseline (geometric mean) | 0.893 | — |
The fallback correction factor used when per-pair sample is < 20. Read: for a typical Big-5 source, this is the multiplier that the adjustment applies in addition to the λ ratio.
| Stat | Mean | SD | N |
|---|---|---|---|
| Goals / 90 | 1.113 | 0.763 | 261 |
| Assists / 90* | 1.142 | 0.798 | 236 |
| Shots / 90 | 0.964 | 0.503 | 513 |
| Key passes / 90 | 1.108 | 0.596 | 504 |
| Tackles / 90 | 1.105 | 0.498 | 524 |
| Interceptions / 90 | 1.028 | 0.588 | 491 |
| G + A / 90 | 1.127 | 0.729 | 367 |
* Assists/90 is the noisiest stat in the calibration set (hold-out r=0.283). Adjustment is applied but the band is wider for this stat. In the UI, adjusted assists/90 carries a small asterisk to flag the wider uncertainty.
Cell crow → col is the per-pair correction for shots/90 from the row league into the column league, with n in parentheses. Blank cells mean n < 20 in the train split; the aggregate (above) is used as fallback. Every allowlisted stat has its own such matrix in source — see src/lib/radar/league-adjustment.ts.
| From → To | Premier | La | Bundesliga | Serie | Ligue |
|---|---|---|---|---|---|
| Premier League | · | 1.03 (29) | 1.15 (22) | 1.29 (31) | — |
| La Liga | 0.92 (33) | · | — | 1.09 (25) | — |
| Bundesliga | 0.81 (42) | — | · | 1.07 (22) | — |
| Serie A | 0.76 (44) | — | — | · | 0.97 (26) |
| Ligue 1 | 0.82 (55) | 0.86 (24) | 0.92 (21) | 1.01 (32) | · |
- 1401 league transitions — Big-5↔Big-5 plus calibrated non-Big-5→Big-5 sources (≥600 min in both pre and post seasons, 2018–2025 window)
- Train / hold-out: 1090 / 311
- 1004 cross-league UEFA cup fixtures (2023–2025)
- Allowlisted stats: 10
Across our hold-out, the adjusted prediction correlates with post-transfer rate stats at r=0.848, vs r=0.842 for raw extrapolation. The model improves the marginal cases — extreme league gaps, defensive-vs-attacking rate stats, edge transitions — more than it improves the average case. Most transitions are close enough to identity that raw extrapolation already does well. We show the lift honestly rather than overselling it: the value of the model is precisely in the cases where raw extrapolation would mislead.
- Publish a point estimate. The output is always a band.
- Adjust a tier-C source league (Eredivisie, Primeira, Süper Lig, Jupiler Pro, MLS, Brasileirão, Liga MX). The toggle reports the source as uncalibrated and shows the raw percentile.
- Adjust a blocklist stat. Pass-accuracy, dribble-success and aerial-win-rate invert under league pressure; clearances and blocks scale with team tactic. Adjustment would mislead, so the toggle leaves them raw with a small *.
- Claim a counterfactual transfer. The baseline is Big-5 aggregate, not a specific league.
- ADR:
docs/10-decisions/ADR-0005-league-level-adjustment.md - Calibration script:
scripts/calibrate-league-adjustment.ts - Backtest:
scripts/backtest-league-adjustment.ts - Production code:
src/lib/radar/league-adjustment.ts