Our Model vs PELE
PELE (“Predictive Elo with Lineup Equilibria”) is Nate Silver's international soccer model at Silver Bulletin. This page scores our stacked-ensemble forecasts against PELE's own published pre-kickoff numbers, match by match, with proper scoring rules and paired significance tests. The honest headline: on the 21 knockout matches both published live, ours scored 0.139 RPS to PELE's 0.149; over all 102 matches PELE scored 0.149 to our 0.155. Neither gap is statistically distinguishable. Every number below is recomputed in your browser and can be traced to its source.
Head To Head
Ranked Probability Score over win / draw / loss: lower is better. Two tracks, never mixed.
21 knockout matches
Every forecast public before kickoff: ours as a git commit, PELE as a timestamped Datawrapper version, bookmakers from the same snapshot as ours. Ranked Probability Score, lower is better.
102 matches, groups to final
PELE's real pre-kickoff numbers vs our model re-run with training data frozen at the opening match. On the live matches the replay is within 1.1 probability points of what we actually published.
Is The Gap Real?
Rerun the significance tests yourself, on any subset, metric and comparison.
Statistics lab: is the gap real, or noise?
Pick any comparison. The page scores every match from the raw probabilities, then runs a paired bootstrap (10,000 resamples) and a two-sided sign-flip randomization test on the per-match score differences, live, with a seeded generator so results are reproducible.
Resampling matches with replacement. Highlighted bars fall inside the 95% interval; the dashed line is zero (no difference).
If the two forecasters were interchangeable, each match's difference is equally likely to be + or −. Highlighted bars are at least as extreme as the observed ±0.0053; their share is the p-value.
Blend & Calibrate
Forecast combination and temperature scaling, with leave-one-out cross-validation.
Model lab: can we fix our model with PELE, honestly?
Two standard post-processing tools, fit live on the 102 replay matches. Forecast blendingmixes our probabilities with PELE's (w·ours + (1−w)·PELE). Temperature scaling sharpens or flattens ours (pᵢ^(1/T), renormalized). Tuning a knob on the same matches you score is optimistic, so every fit is also scored with leave-one-out cross-validation: for each match, the knob is chosen on the other 101 and applied to the one held out.
Lower is better. Solid line: your setting. Dashed: in-sample optimum. The LOO folds chose 0.05 (3×), 0.10 (2×), 0.15 (9×), 0.20 (67×), 0.25 (18×), 0.30 (2×), 0.35 (1×), so the optimum is not perfectly stable across folds.
All 306 outcome probabilities (3 per match; n counts probabilities, not matches). Hollow points: n < 10, low confidence. Hover for n.
Read this honestly: LOO removes the optimism of picking the knob on the scored match, but all 102 matches come from one tournament, so the folds are not independent. Treat any improvement here as a hypothesis to test on another tournament, not a result. ECE uses equal-width bins and moves with the bin count; that is why the bin selector is exposed.
Every Match
Forecasts, results, and the provenance of each number.
Match explorer
Pick a match to see all three forecasts against what happened, the per-match score, and the provenance of each number: PELE's exact Datawrapper version (open the CSV yourself) and, for live matches, the git commit that published our forecast before kickoff.
39 published versions listed this match. Vertical line: the deadline used (15:00 UTC on the local match date, earlier than any 2026 kickoff). Only versions before it can count.
Two Rating Systems
Ratings: where PELE and our Elo disagree
Pre-tournament strength of every team: PELE's last published rating before the opening match vs our Elo on the same day. Spearman rank correlation (tie-averaged) is 0.952, so they mostly agree. The two scales are not interchangeable point for point: our Elo spreads teams wider. Compare ranks, and click a team to see how each system moved during the tournament.
PELE 1952 (#8) · our Elo 1946 (#19) · reached Quarterfinal. Both series are plotted as change from their own starting value, so one axis is fair to both.
| Largest rank disagreements | PELE | PELE # | Our Elo | Our # | Reached |
|---|---|---|---|---|---|
| Iran | 1728 | 34 | 1902 | 23 | Group |
| Norway | 1952 | 8 | 1946 | 19 | Quarterfinal |
| Bosnia and Herzegovina | 1707 | 37 | 1653 | 44 | Round of 32 |
| Senegal | 1895 | 15 | 1906 | 22 | Round of 32 |
| Sweden | 1780 | 28 | 1790 | 35 | Round of 32 |
| Ghana | 1663 | 40 | 1623 | 46 | Round of 32 |
| Japan | 1870 | 18 | 1995 | 12 | Round of 32 |
| Morocco | 1869 | 19 | 1976 | 13 | Quarterfinal |
What One Tournament Can Tell You
Power calculator: how many World Cups would settle it?
Uses the observed standard deviation of the per-match RPS difference (ours − PELE, 102 matches, SD = 0.0744) and the normal-approximation sample size for a two-sided paired test: n = ((z₁₋α/₂ + z_power) · SD / gap)². Move the gap to ask “what if the true difference were bigger?”
World Cups needed (log scale) against the true gap. At 80% power and α = 0.05, one 104-match tournament can only detect a gap of about 0.0204 RPS or more, 3.9× the gap we observed.
Audit
The bug we found, what was excluded, and the claims we withdrew.
The leakage bug
For matches without a known kickoff time, the first version of the benchmark took PELE's last forecast published before 15:00 UTC on PELE's own date label. PELE labels matches by US Eastern date, so a late Pacific kickoff carries the next day's label. For Australia–Turkey (played 2026-06-13 local time), that rule picked v34, published 2026-06-14 14:28UTC, after the match. The fix anchors the deadline on the earlier of PELE's label and the local match date, which picks v32. 3 matches carried a next-day label; 1actually received a post-match version under the old rule (the others had no newer version in between). The fix made PELE's full-tournament RPS worse (0.1479 → 0.1492): the bug had favored the benchmark, not our model. A regression test pins it.
Match accounting
- 104 World Cup results; 103 fixtures forecast by PELE. The third-place match (2026-07-18 France v England) has no PELE forecast.
- 102 scored on the replay track. Excluded: Canada–South Africa, where PELE published only an “advance” probability, never win/draw/loss.
- 21 on the live track. 9 early knockout matches are excluded because our pipeline had not yet committed a forecast before kickoff.
- Team-name mismatches: 0. The parser raises on any unknown team code instead of dropping the row.
Three claims that did not survive significance testing
| Withdrawn claim | Why the data doesn't support it |
|---|---|
| PELE is better calibrated than our model | Decile ECE is higher for ours by 0.018, but the bootstrap 95% CI of that difference, [−0.021, +0.059], includes 0. |
| Our model is too timid on favorites | We gave favorites 59.0% on average and they won 64.7%, but the 95% CI of that gap is [−3.1, +14.7] points, which includes 0. |
| Blending helps because the models make different mistakes | Per-match RPS correlates at 0.83, so they mostly miss the same matches. The 50/50 blend beats ours only on log loss (p = 0.045; RPS p = 0.19) and does not beat PELE (log loss p = 0.43). |
What PELE models that our model does not
| Aspect | PELE (per its published methodology) | Our model |
|---|---|---|
| Rating core | Zero-sum Elo over ~50k matches since 1872; harmonic goal margin (2nd goal worth 1/2, 3rd 1/3...) | Zero-sum Elo since 1872 with an eloratings-style goal-difference multiplier; fed to a stacked ensemble alongside rolling form and FIFA points |
| Player information | Transfermarkt squad values (top-23, position-weighted, ~30% UEFA-club discount), official WC rosters, injuries/suspensions | None -- results-only features |
| Priors / mean reversion | Slow reversion toward a prior built from log GDP (PPP), footballing legacy year and region | None -- new teams start at 1500 |
| Home advantage | Era-, distance-, altitude- and team-specific; venue-level for co-hosts | Learned 'neutral' flag; every World Cup match scored as neutral in production |
| Match model | Negative-binomial score matrix with a correlation term + team 'tilt' (attacking/defensive style) -> W/D/L | Direct 3-class probabilities from an XGBoost + Elo-logistic + FIFA-heuristic stack with a logistic meta-learner |
| Knockout resolution | Extra time modeled (~40% of draws resolved) then skill-weighted penalties; knockout rating gap x1.1 | P(advance) = P(win) + 0.5 x P(draw) |
| Tournament simulation | 100,000 sims incl. group tiebreakers, cards (fair play), 'hot' in-sim rating updates | 10,000 sims of the actual remaining bracket from the knockout stage |
One PELE idea was tested directly on our model: scoring the 14 co-host matches as home games instead of neutral changed our RPS by +0.0002 (p = 0.83): no improvement.
How It Works
Data sources, pipeline, statistics, the in-browser engine, and the frontend, exactly as implemented.
From a public chart CDN to statistics running in your browser
The expensive, model-dependent work (retrieving PELE's history, re-running our model, scoring, significance tests) runs offline in Python, and its outputs are committed to the repository. The page is a statically generated Next.js route that imports one JSON file at build time. Everything interactive (every test, fit and chart on this page) is recomputed in the browser from per-match probabilities by a small TypeScript engine. There is no server, API or database behind this page, and no large language model anywhere in it.
Getting PELE's numbers as they were before kickoff
Silver Bulletin publishes PELE's match forecasts as a Datawrapper table (chart 3bTOr) and team ratings as another (pS7DN). Datawrapper keeps every published version at an immutable URL and serves each with an HTTP Last-Modified header. scripts/fetch_pele_forecasts.py walks version numbers until it hits a gap, and recovers 127 forecast-table versions and 133 ratings versions. It keeps only World Cup rows, maps each team code to our naming, and fails loudly on any unknown code rather than dropping the row.
For each match the benchmark uses PELE's last version published before a deadline: the exact kickoff when our fixture feed recorded it, otherwise 15:00 UTC on the earlier of PELE's US-Eastern date label and the venue-local match date. Every 2026 kickoff was at or after 16:00 UTC on its local date, so that is always pre-kickoff. The label-only version of this rule leaked one post-match forecast (see the audit above); it is fixed and pinned by a regression test in tests/test_pele_benchmark.py.
Our side: two tracks that are never mixed
Live (21 matches). Our daily pipeline committed its forecasts to dashboard/data/upcoming_matches.json. The comparison reads every historical version with git log / git show and uses the newest commit made before kickoff. Local log files are deliberately ignored: they are gitignored, so their timestamps prove nothing to anyone else.
Replay (102 matches). Our Layer 1 model is re-run with the same code and features, but trained once, only on matches from 1992-01-01 to 2026-06-11 (production retrained daily). It is a scikit-learn stacking classifier over three members (XGBoost with 200 trees, depth 4, learning rate 0.1; a logistic regression on Elo difference and venue; a FIFA-ranking heuristic), blended by a logistic meta-learner with 5-fold stacking. Its six features (Elo difference, rolling goals for/against and win-rate differences, FIFA points difference, neutral venue) (including FIFA ranking points) are looked up strictly before each match date, so no match can see its own result. On the 21 live matches the replay lands within 1.1 probability points of what was actually published, which is the fidelity check for the replay.
Scoring: proper rules for three ordered outcomes
Every forecast is a probability vector p = (home win, draw, away win) and every result is one of those three, so draws are a real outcome, not dropped or split. Knockout results use the recorded score including extra time; a shootout counts as a draw. The headline metric is the Ranked Probability Score, which respects the ordering (calling a home win a draw costs less than calling it an away win):
All three are strictly proper: a forecaster minimizes its expected score only by reporting its true beliefs.
Statistics: paired, distribution-free, and powered
Both forecasters are scored on the same matches, so every test works on per-match differences d = score(A) − score(B). The p-value comes from a two-sided sign-flip randomization test: under the null that A and B are interchangeable, each dᵢ is equally likely to be +dᵢ or −dᵢ. 10,000 random sign vectors give the null distribution of the mean, and p = (1 + #|flipped mean| ≥ |observed mean|) / 10,001. The 95% interval is a percentile bootstrap over matches (10,000 resamples). A paired t-test was rejected because upsets make the differences heavily skewed and some subsets have only 14 matches.
Caveats stated up front: no multiple-comparison correction is applied; matches share teams, so they are not fully independent; p-values are Monte Carlo estimates (about ±0.002 near p = 0.05).
The in-browser engine, and how it was verified
dashboard/src/lib/pele/stats.ts is a dependency-free TypeScript port of src/benchmarks/scoring.py: the three scoring rules, the paired bootstrap and sign-flip test, decile reliability with expected calibration error, Spearman correlation with tie-averaged ranks, an inverse-normal (Acklam) for the power formula, and the grid-search / leave-one-out fitter used by the Model Lab. Randomness comes from a seeded Mulberry32 generator, so the same settings always show the same numbers.
Before any UI was built, the compiled engine was run against the same data as the Python benchmark. Mean score differences, ECE, and the leave-one-out blend and temperature fits matched to floating-point precision; bootstrap interval ends and p-values differed only by Monte Carlo error (for example p = 0.484 in the browser vs 0.479 in Python for the full-tournament RPS test), because the two languages use different random generators. One 10,000-resample test took roughly 50 ms on a laptop, so it runs synchronously on each control change without a Web Worker.
The ML in the Model Lab, and why it is cross-validated
Forecast combination is a linear opinion pool, w·ours + (1−w)·PELE, over w ∈ {0, 0.05, …, 1}. Temperature scaling maps each probability to pᵢ^(1/T) and renormalizes, over T ∈ {0.50, 0.55, …, 1.50}. Both are fit by grid search on the selected metric.
Choosing a knob on the same 102 matches you then score is optimistic, so the lab also reports leave-one-out cross-validation: for each match i, the knob is chosen on the other 101 matches and match i is scored with it; the reported LOO score is the mean over all 102 held-out matches. The scores for every grid value are computed once, and each fold reuses them by subtracting the held-out match, so the whole LOO fit takes a few milliseconds. The lab also lists which values the folds chose, which shows how stable the optimum is.
Frontend implementation
The page is a Next.js 16 App Router route (src/app/pele/page.tsx), prerendered as static HTML at build time. It reuses the site's own Nav, SectionHeading and Footer, the existing design tokens and glass cards, Recharts for every chart and framer-motion for the same entrance animations the rest of the site uses. Sections are client components with local React state and memoized derivations; nothing is fetched at runtime.
The PELÉ tab sits next to the existing sport tabs but is deliberately not an entry in sports_config.json: PELE is a benchmark, not a sport, and that config also drives sport routing, the completed-sports showcase and a backend retrospective script. The tab is a separate link rendered after the config-driven sport tabs, with the same styling.
Charts follow a validated palette (forecaster colors and the home/away outcome pair pass a colorblind-separation check against the dark card), draws are dashed gray so identity never relies on color alone, low-confidence calibration bins (n < 10) are drawn hollow, and controls are native buttons, ranges and selects with ARIA roles.
Reproduce everything
python scripts/compare_vs_pele.py # replay + scoring + tests (~1 min)
python scripts/export_pele_page.py # build dashboard/data/pele_page.json
python -m pytest tests/test_pele_benchmark.py
Every PELE number on the page links to the exact Datawrapper CSV it came from, and every live forecast of ours links to the commit that published it. A one-page summary with the test behind each p-value is in docs/pele-benchmark-summary.md.
Limitations: one tournament, so no gap between the models is detectable; PELE's method column above paraphrases its published methodology; the replay is a faithful re-run of our model, not a record of what we published for the group stage; team-strength inputs come from results only.