Knockout bracket
Both halves of the draw converge on the Final in the centre. The group
tables on each outer edge feed teams inward: dashed lines connect every group to the
Round-of-32 match its qualifiers go to. Slots show their placeholder (1E =
winner of Group E, 3A/B/C/D/F = a best 3rd-placed team) until decided.
👆 Interactive what-if bracket. The italic teams are the model's projected winners; teams already qualified into an unplayed match (e.g. the Round of 32 once the groups are decided) show upright but are just as clickable. Click a match box to make that team advance — click the upper team's half to pick it, the lower half to pick the other. The picked team turns solid green ✓ and the rest of the bracket re-projects around it; a banner at the bottom of the bracket confirms each pick. Click again to undo, or use Reset picks to clear all. Tip: picking an underdog (a team that wasn't already projected to win) is what visibly shifts the later rounds — picking the favourite just locks in what the model already expected. Zoom in for bigger boxes. (Teams already decided on the pitch can't be changed.) Roll back time: the roll back to picker rewinds the bracket to the end of an earlier matchday — every later real result is unwound (the group tables and the form the model has learned roll back with it) and the tournament is re-forecast from that point. Rolled-back matches get a dashed amber outline and become pickable again.
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #49 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #49 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #51 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #51 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #50 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #50 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #52 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #52 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #53 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #53 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #55 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #55 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #54 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #54 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Runner-up → match #56 | ||
| 1 | 0 | |
| 2 | 0 | |
| 3 | 0 | |
| 4 | 0 | |
| Winner → match #56 | ||
Drag to pan · scroll or use the slider to zoom.
How the projections work
The bracket's Projected mode and the Predictions page are driven by a Monte-Carlo simulation that plays the rest of the tournament many thousands of times and tallies how often each outcome occurs. It is deliberately simple and transparent so the assumptions are easy to see — and to argue with. Finished results are held fixed; only matches not yet played are simulated, so the numbers sharpen as the tournament unfolds.
1 · Team strength (dynamic Elo)
Every team starts from a prior rating on the World-Football-Elo scale (≈2100+ elite, ≈1900 strong, ≈1750 mid, ≈1600 weaker) — a roughly early-2026 snapshot. Co-hosts (USA, Canada, Mexico) get a +60 home-advantage edge; unknown teams fall back to 1700.
Those priors then update from in-tournament results: after every finished match both teams' ratings shift by the standard Elo update, so an over- or under-performing side carries that form into the simulation of its remaining matches.
W is the actual result (1 / 0.5 / 0), We the Elo win-expectancy,
G a goal-margin multiplier (bigger wins move more), and K = 60 — the World-Cup tier,
deliberately responsive. Early on the ratings sit at their priors and diverge as results land;
the live movers are shown on the Predictions page's Form tracker. (Set
PREDICT_ELO_K=0 to freeze the priors.)
2 · One match at a time
Goals (group stage). A rating gap is turned into an expected-goals supremacy, then each side's goals are drawn from independent Poisson distributions:
λA = max( 0.18, (μ + s) / 2 ) λB = max( 0.18, (μ − s) / 2 ) with μ = 2.7
GA ∼ Poisson(λA) GB ∼ Poisson(λB)
So ~200 Elo points ≈ one goal of expected supremacy; evenly-matched teams average 1.35 goals each (2.7 combined, near the historical World-Cup norm). Scorelines — not just win/draw/loss — fall out of this, which is what the group tiebreakers need.
Draw correction (Dixon–Coles). Plain independent Poisson under-produces draws, so the four lowest scorelines are reweighted by the Dixon–Coles factor τ (with ρ = −0.12), nudging 0–0 and 1–1 up and 1–0 / 0–1 down. This lifts the average group draw rate from ~22% to a more realistic ~24% without otherwise disturbing the goal model:
τ(1,0) = 1 + λBρ τ(0,1) = 1 + λAρ (τ = 1 otherwise)
P(x,y) ∝ τ(x,y) · Poisson(x; λA) · Poisson(y; λB)
Knockouts. The same goal model decides the match; if it ends level, the tie goes to a single Elo-weighted coin flip standing in for a penalty shootout, using the standard Elo win-expectancy:
3 · Simulating the whole tournament
Each of N iterations (default 4,000) plays out end to end:
- Simulate every unplayed group match; keep finished ones as-is.
- Build the 12 group tables with FIFA's tiebreakers in order — points → goal difference → goals scored → head-to-head (points/GD/GF among the tied teams) → team name as a stable final fallback (in lieu of fair-play points / drawing of lots).
- Rank the twelve 3rd-placed teams and take the best 8; assign them to the Round-of-32 "3X/Y" slots with a backtracking matcher that respects each slot's eligible groups.
- Resolve the Round of 32 and play each knockout round through to the Final, carrying winners forward (and the two semi-final losers into the third-place match).
Across all iterations the model tallies how often each team finishes 1st/2nd/3rd in its group, reaches each round, and wins the title. Dividing by N gives the probabilities. Results are cached and only recomputed when a new match result lands.
4 · From frequencies to the bracket
The odds tables are these tallies directly: e.g. champion % is the share of simulations a team won the Final, advance % the share it reached the Round of 32.
The Projected bracket needs more care. Picking each slot's single most-likely team independently does not yield a valid bracket — a dominant team can be the favorite in two places that can't both happen (famously, the same team in both the Final and the third-place match, or in two different Round-of-32 slots). Instead the model assembles one internally consistent bracket: it derives a coherent Round-of-32 field from the simulated standings (each group's expected finishing order, plus the best-8 thirds matched to their slots), then advances the favored team of each projected matchup forward, so every later slot is fed by a real earlier result. The confidence shown on a slot is still the honest marginal — the share of simulations in which that exact team reached that exact slot.
Make it your own. In Projected mode you can click any projected team to force it to advance from that match. The bracket is then re-walked around your pick: that team is carried forward and every later slot it feeds is re-resolved, so you can explore "what if" paths. Your picks only steer the single drawn bracket — the simulation itself is unchanged, so the confidence beside each slot stays the model's own marginal (a forced upset can sit next to a low %, which is exactly the point). Reset picks returns to the model's chalk bracket.
5 · Uncertainty, assumptions & how to critique it
This is a teaching-grade model, not a betting market. Known limitations, roughly in order of impact:
- Sampling noise. A probability p from N simulations has a standard error of
about √(p(1−p)/N) — ≈±0.8 percentage points at p=0.5,
N=4,000. It shrinks only as 1/√N, so small gaps between teams may be noise. Raise
PREDICT_SIMSto tighten. - Ratings: subjective start, responsive update. The pre-tournament snapshot is
hand-set — challenge it freely. Ratings now adapt to results via a World-Cup-tier Elo update
(K=60), which is deliberately responsive: over a handful of games it can overreact to a fluke
result as readily as it captures real form. Lower
PREDICT_ELO_Kto damp it, or set 0 to freeze the priors. - Simplified goal model. Goals use Poisson with a Dixon–Coles low-score correction (ρ = −0.12) — better than independent Poisson on draws, but the correction is modest and still doesn't capture full game-state dynamics (red cards, game-management when ahead, late pushes). The total-goals baseline (2.7) and the linear, clamped Elo→supremacy mapping are convenient choices, not estimated fits. ρ is calibrated to the historical ~24% draw norm, so an unusually drawy run (real tournaments swing roughly 18–30% on small samples) is treated as variance, not chased.
- Thin knockout model. A level match is decided by a single Elo coin — no separate extra-time phase, no penalty-specific skill.
- Home advantage is coarse. A flat +60 for all three hosts; no travel, altitude, climate, rest-day, injury, or squad-news effects at all.
- Third-place allocation is a valid matching, not FIFA's official fixed table. The set of qualifying thirds is correct, but which slot each lands in can differ from FIFA's published lookup.
- The projected bracket is the favored-path ("chalk") reconciliation. Because the favorite advances each projected matchup, the single drawn bracket understates upsets — read the per-slot confidence to see how shaky a pick is, and the odds tables for the true spread.
- Not yet back-tested. The model hasn't been calibrated against historical tournaments, so treat the absolute numbers as indicative rather than validated.
6 · Reproducibility
Pure-Python, no external numerical libraries. Runs are deterministic given a fixed random seed.
The full implementation lives in predict.py (simulation), ratings.py
(priors), and compute.py (standings & tiebreakers) — and is documented in the
project README for line-by-line review.