Our Model vs PELE

2026 World Cup · 102 matches · pre-kickoff forecasts only

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.

The Result

Head To Head

Ranked Probability Score over win / draw / loss: lower is better. Two tracks, never mixed.

127
PELE forecast-table versions recovered
each with its publish timestamp
102
matches scored head-to-head
21 verified pre-kickoff by both
~1,545
matches needed to separate the models
≈ 15 World Cups at 80% power
1
post-kickoff PELE forecast caught
by audit, then excluded
Live · zero hindsight

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.

Our model
0.1386best
PELE
0.1491
Bookmakers
0.1563
Ours − PELE: ΔRPS −0.0105 · 95% CI [−0.0385, +0.0129] · p = 0.47
not distinguishable at n = 21
Full tournament · leakage-safe replay

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.

Our model
0.1545
PELE
0.1492best
Group (n=72): 0.163 vs 0.154, p = 0.35Knockout (n=30): 0.135 vs 0.138, p = 0.84
Ours − PELE: ΔRPS +0.0053 · 95% CI [−0.0093, +0.0193] · p = 0.48
not distinguishable at n = 102
Interactive

Is The Gap Real?

Rerun the significance tests yourself, on any subset, metric and comparison.

Runs in your browser

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.

Track
Matches
Comparison (A vs B)
Metric
102
matches
Ours better on 43, PELE on 59
+0.0053
mean RPS: Ours − PELE
0.1545 vs 0.1492 (lower is better)
[−0.009, +0.020]
95% bootstrap CI
includes 0
0.484
p-value (two-tailed)
not significant
~1,545
matches for 80% power
≈ 14.9 World Cups
Bootstrap distribution of the mean difference

Resampling matches with replacement. Highlighted bars fall inside the 95% interval; the dashed line is zero (no difference).

Null distribution (random sign flips)

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.

Python benchmark (scripts/compare_vs_pele.py, Mersenne Twister, seed 7): mean +0.0053, CI [−0.0093, +0.0193], p = 0.479. Means match exactly; CI ends and p differ only by Monte Carlo error.
Resamples
Interactive

Blend & Calibrate

Forecast combination and temperature scaling, with leave-one-out cross-validation.

Machine learning on the forecasts themselves

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.

Tool
Fit & score on
0.1545
our model alone (RPS)
0.1492
PELE alone
0.1496
at w = 0.50
in-sample
0.1488
best in-sample (w = 0.20)
optimistic
0.1520
leave-one-out CV score
beats ours alone; does not beat PELE
Mean RPS across the weight on our model (w)

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.

Reliability, recomputed for your setting
Bins

All 306 outcome probabilities (3 per match; n counts probabilities, not matches). Hollow points: n < 10, low confidence. Hover for n.

Ours · ECE 0.073 PELE · ECE 0.055 Adjusted · ECE 0.062 Perfect calibration

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.

Receipts

Every Match

Forecasts, results, and the provenance of each number.

Every match, with receipts

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.

Show
Sort
Group · 2026-06-19 · co-host match
United States 2-0 Australia
Result: Home win
Forecaster Home win Draw Away winRPS
Ours (replay)0.360
PELE0.108
Full-opacity segment = what happened. RPS: lower is better (0 = perfect).
PELE provenance
Version v39 (CSV), published 2026-06-19 13:38 UTC. Deadline 2026-06-19 15:00 UTC.
Our provenance
No public pre-kickoff commit for this match (our first public forecast commit was on 2026-07-01, US Eastern), so only the leakage-safe replay is scored.
PELE's forecast for this fixture, version by version

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.

United States win Draw Australia win version used
Team Strength

Two Rating Systems

Two rating systems, 48 teams

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 rating vs our Elo (one dot per team)
Norway: rating change since the opening match

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.

Our Elo (updates after each match) PELE (each published ratings version)
Largest rank disagreementsPELEPELE #Our EloOur #Reached
Iran172834190223Group
Norway19528194619Quarterfinal
Bosnia and Herzegovina170737165344Round of 32
Senegal189515190622Round of 32
Sweden178028179035Round of 32
Ghana166340162346Round of 32
Japan187018199512Round of 32
Morocco186919197613Quarterfinal
Sample Size

What One Tournament Can Tell You

Sample size

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?”

observed gap: 0.0053
Power
α (two-sided)
1,546
matches with both forecasts needed
14.9
World Cups (104 matches each)
we have 102 matches

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.

Integrity

Audit

The bug we found, what was excluded, and the claims we withdrew.

Found in audit, fixed

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.

Nothing silently dropped

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.
Tested, then withdrawn

Three claims that did not survive significance testing

Withdrawn claimWhy the data doesn't support it
PELE is better calibrated than our modelDecile 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 favoritesWe 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 mistakesPer-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).
Different machines

What PELE models that our model does not

AspectPELE (per its published methodology)Our model
Rating coreZero-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 informationTransfermarkt squad values (top-23, position-weighted, ~30% UEFA-club discount), official WC rosters, injuries/suspensionsNone -- results-only features
Priors / mean reversionSlow reversion toward a prior built from log GDP (PPP), footballing legacy year and regionNone -- new teams start at 1500
Home advantageEra-, distance-, altitude- and team-specific; venue-level for co-hostsLearned 'neutral' flag; every World Cup match scored as neutral in production
Match modelNegative-binomial score matrix with a correlation term + team 'tilt' (attacking/defensive style) -> W/D/LDirect 3-class probabilities from an XGBoost + Elo-logistic + FIFA-heuristic stack with a logistic meta-learner
Knockout resolutionExtra time modeled (~40% of draws resolved) then skill-weighted penalties; knockout rating gap x1.1P(advance) = P(win) + 0.5 x P(draw)
Tournament simulation100,000 sims incl. group tiebreakers, cards (fair play), 'hot' in-sim rating updates10,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.

Under the Hood

How It Works

Data sources, pipeline, statistics, the in-browser engine, and the frontend, exactly as implemented.

Architecture

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.

OFFLINE · PYTHON (run once, outputs committed)BUILD + BROWSER · TYPESCRIPTDatawrapper version CDN3bTOr/<v>, pS7DN/<v> + Last-ModifiedOur git historyupcoming_matches.json commitsMatch results + Eloresults.csv + results_log.csvfetch_pele_forecasts.py→ data/external/pele/*.csvcompare_vs_pele.pyLayer 1 replay, scoring, testspele_comparison.jsondata/benchmarks/export_pele_page.py+ version histories, ratingspele_page.jsonstatic import at build timeNext.js /pele (static)client components, Rechartsstats.ts in your browserbootstrap, sign-flip, LOO-CV
01

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.

02

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.

03

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):

RPS = ½ · Σₖ₌₁² ( Σᵢ≤ₖ pᵢ − Σᵢ≤ₖ oᵢ )² Brier = Σᵢ (pᵢ − oᵢ)² log loss = −ln max(p_outcome, 10⁻⁶)

All three are strictly proper: a forecaster minimizes its expected score only by reporting its true beliefs.

04

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.

n_required = ⌈ ((z₁₋α/₂ + z_power) · SD(d) / gap)² ⌉

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).

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.

06

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.

07

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.

08

Reproduce everything

python scripts/fetch_pele_forecasts.py  # refresh PELE history (network)
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.