Predictions: Ensemble Workflow

One Poisson model per team, round by round

Author

Miguel R.

Published

July 5, 2026

The ensemble workflow starts from the exact same per-team Poisson models as the naive workflow, then adds a player layer on top.

For each team we compute the average Elo of its four best attackers and compare it against the average Elo of the opponent’s defensive block (four best defenders, best midfielder and best goalkeeper). The ratio of the two, raised to a damping exponent ((atk/def) ** 2.5, tuned on the completed rounds - see model vs reality), multiplies the predicted goals: a team whose attack outclasses the opposing defense gets a boost, and a team facing a stronger defensive block gets discounted.

The point of running both workflows on identical fixtures is to answer one question: does player-level information actually improve the prediction, or is the team-level history enough?

1. Setup

All the heavy lifting lives in the project modules:

  • fixtures.py holds the tournament calendar (this is the only file that changes between rounds),
  • model_utils.py trains the per-team Poisson models,
  • report_utils.py generates, stores and scores the predictions.
Code
from model_utils import Workflow
from report_utils import predict_all_rounds, round_detail, scoreboard

2. Generating the predictions

One call predicts every round in the calendar. For each round, the model trains only on matches played before the round started, so these predictions are fully reproducible: re-running this notebook after the tournament produces the same numbers that were published in real time.

Each round is written to predictions/ensemble_<round>.csv, separate from the other workflow’s files, so the two approaches can always be compared side by side.

Code
predictions = predict_all_rounds(Workflow.ATK_DEF, "ensemble")

3. Round by round

For every completed round we show the predicted score next to the real one. correct marks whether the predicted winner matched the actual result (a real draw counts as a miss, since the model virtually never predicts one).

3.1 Group stage - Matchday 1

Twenty-four matches, and the model’s first contact with reality. Group-stage matches allow draws, but a Poisson model almost never predicts identical expected goals for both sides, so every real draw counts against us here.

Code
round_detail("ensemble", "first_round")
team_1 team_2 predicted_score real_score winner_pred winner_real correct
0 Mexico South Africa 3.3 - 0.23 2 - 0 Mexico Mexico True
1 South Korea Czechia 1.8 - 1.31 2 - 1 South Korea South Korea True
2 Canada Bosnia and Herzegovina 1.53 - 1.06 1 - 1 Canada Draw False
3 Qatar Switzerland 0.21 - 7.41 1 - 1 Switzerland Draw False
4 Brazil Morocco 1.99 - 0.98 1 - 1 Brazil Draw False
5 Haiti Scotland 1.47 - 6.45 0 - 1 Scotland Scotland True
6 USA Paraguay 4.09 - 0.75 4 - 1 USA USA True
7 Australia Türkiye 0.54 - 2.66 2 - 0 Türkiye Australia False
8 Germany Curaçao 19.67 - 0.2 7 - 1 Germany Germany True
9 Côte d'Ivoire Ecuador 2.42 - 1.66 1 - 0 Côte d'Ivoire Côte d'Ivoire True
10 Netherlands Japan 3.39 - 0.1 2 - 2 Netherlands Draw False
11 Sweden Tunisia 1.18 - 0.4 5 - 1 Sweden Sweden True
12 Belgium Egypt 1.37 - 0.26 1 - 1 Belgium Draw False
13 IR Iran New Zealand 1.98 - 0.55 2 - 2 IR Iran Draw False
14 Spain Cabo Verde 8.19 - 0.58 0 - 0 Spain Draw False
15 Saudi Arabia Uruguay 0.22 - 9.01 1 - 1 Uruguay Draw False
16 France Senegal 5.52 - 0.0 3 - 1 France France True
17 Iraq Norway 0.0 - 0.89 1 - 4 Norway Norway True
18 Argentina Algeria 2.27 - 4.07 3 - 0 Algeria Argentina False
19 Austria Jordan 1.03 - 0.0 3 - 1 Austria Austria True
20 Portugal Congo DR 31.55 - 0.0 1 - 1 Portugal Draw False
21 Uzbekistan Colombia 0.44 - 1.9 1 - 3 Colombia Colombia True
22 Ghana Panama 1.74 - 0.63 1 - 0 Ghana Ghana True
23 England Croatia 11.39 - 1.77 4 - 2 England England True

3.2 Group stage - Matchday 2

By matchday 2 the favorites usually start separating from the rest of the group, which tends to make matches easier to call.

Code
round_detail("ensemble", "second_round")
team_1 team_2 predicted_score real_score winner_pred winner_real correct
0 Czechia South Africa 1.52 - 0.01 1 - 1 Czechia Draw False
1 Mexico South Korea 1.34 - 2.0 1 - 0 South Korea Mexico False
2 Switzerland Bosnia and Herzegovina 1.56 - 1.49 4 - 1 Switzerland Switzerland True
3 Canada Qatar 1.25 - 0.92 6 - 0 Canada Canada True
4 Brazil Haiti 3.32 - 0.0 3 - 0 Brazil Brazil True
5 Scotland Morocco 0.38 - 2.43 0 - 1 Morocco Morocco True
6 Türkiye Paraguay 2.35 - 1.24 0 - 1 Türkiye Paraguay False
7 USA Australia 2.75 - 0.31 2 - 0 USA USA True
8 Germany Côte d'Ivoire 2.15 - 0.0 2 - 1 Germany Germany True
9 Ecuador Curaçao 3.73 - 0.97 0 - 0 Ecuador Draw False
10 Netherlands Sweden 4.81 - 0.85 5 - 1 Netherlands Netherlands True
11 Tunisia Japan 0.15 - 8.75 0 - 4 Japan Japan True
12 Belgium IR Iran 1.85 - 0.74 0 - 0 Belgium Draw False
13 New Zealand Egypt 3.26 - 1.15 1 - 3 New Zealand Egypt False
14 Spain Saudi Arabia 0.06 - 0.06 4 - 0 Spain Spain True
15 Uruguay Cabo Verde 2.5 - 0.0 2 - 2 Uruguay Draw False
16 France Iraq 7.17 - 0.49 3 - 0 France France True
17 Norway Senegal 1.94 - 0.75 3 - 2 Norway Norway True
18 Argentina Austria 2.92 - 2.38 2 - 0 Argentina Argentina True
19 Jordan Algeria 0.39 - 0.21 1 - 2 Jordan Algeria False
20 Portugal Uzbekistan 3.89 - 0.89 5 - 0 Portugal Portugal True
21 Colombia Congo DR 1.95 - 0.0 1 - 0 Colombia Colombia True
22 England Ghana 22.28 - 0.06 0 - 0 England Draw False
23 Panama Croatia 0.0 - 4.37 0 - 1 Croatia Croatia True

3.3 Group stage - Matchday 3

The hardest matchday to predict: teams that already qualified rotate their squads, and teams that are already out play with nothing to lose. None of that appears in the training data.

Code
round_detail("ensemble", "third_round")
team_1 team_2 predicted_score real_score winner_pred winner_real correct
0 Czechia Mexico 0.6 - 2.62 0 - 3 Mexico Mexico True
1 South Africa South Korea 0.02 - 4.11 1 - 0 South Korea South Africa False
2 Switzerland Canada 0.0 - 1.83 2 - 1 Canada Switzerland False
3 Bosnia and Herzegovina Qatar 1.13 - 0.3 3 - 1 Bosnia and Herzegovina Bosnia and Herzegovina True
4 Scotland Brazil 0.63 - 1.83 0 - 3 Brazil Brazil True
5 Morocco Haiti 2.54 - 0.0 4 - 2 Morocco Morocco True
6 Türkiye USA 0.01 - 2.18 3 - 2 USA Türkiye False
7 Paraguay Australia 1.28 - 0.16 0 - 0 Paraguay Draw False
8 Curaçao Côte d'Ivoire 0.28 - 1.26 0 - 2 Côte d'Ivoire Côte d'Ivoire True
9 Ecuador Germany 0.17 - 7.33 2 - 1 Germany Ecuador False
10 Japan Sweden 0.95 - 1.46 1 - 1 Sweden Draw False
11 Tunisia Netherlands 0.17 - 11.95 1 - 3 Netherlands Netherlands True
12 Egypt IR Iran 0.72 - 1.58 1 - 1 IR Iran Draw False
13 New Zealand Belgium 1.43 - 1.61 1 - 5 Belgium Belgium True
14 Uruguay Spain 0.17 - 5.75 0 - 1 Spain Spain True
15 Cabo Verde Saudi Arabia 0.45 - 0.77 0 - 0 Saudi Arabia Draw False
16 Norway France 5.36 - 2.95 1 - 4 Norway France False
17 Senegal Iraq 2.3 - 1.24 5 - 0 Senegal Senegal True
18 Argentina Jordan 4.34 - 0.26 3 - 1 Argentina Argentina True
19 Algeria Austria 0.52 - 1.48 3 - 3 Austria Draw False
20 Colombia Portugal 3.06 - 3.16 0 - 0 Portugal Draw False
21 Congo DR Uzbekistan 1.19 - 0.0 3 - 1 Congo DR Congo DR True
22 Panama England 0.69 - 12.11 0 - 2 England England True
23 Croatia Ghana 3.49 - 0.04 2 - 1 Croatia Croatia True

3.4 Round of 32

First knockout round. A caveat on scoring: the match history records the score after 90 or 120 minutes, so a match decided on penalties is recorded as a draw and counts against the model even when its pick advanced.

Code
round_detail("ensemble", "round_of_32")
team_1 team_2 predicted_score real_score winner_pred winner_real correct
0 South Africa Canada 0.19 - 2.79 0 - 1 Canada Canada True
1 Brazil Japan 2.8 - 0.95 2 - 1 Brazil Brazil True
2 Germany Paraguay 3.76 - 0.64 1 - 1 Germany Draw False
3 Netherlands Morocco 4.88 - 2.7 1 - 1 Netherlands Draw False
4 Côte d'Ivoire Norway 0.68 - 3.07 1 - 2 Norway Norway True
5 France Sweden 3.19 - 0.38 3 - 0 France France True
6 Mexico Ecuador 0.85 - 0.42 2 - 0 Mexico Mexico True
7 England Congo DR 12.74 - 1.8 2 - 1 England England True
8 Belgium Senegal 0.76 - 1.75 3 - 2 Senegal Belgium False
9 USA Bosnia and Herzegovina 10.15 - 0.92 2 - 0 USA USA True
10 Spain Austria 1.97 - 1.46 3 - 0 Spain Spain True
11 Portugal Croatia 7.02 - 0.37 2 - 1 Portugal Portugal True
12 Switzerland Algeria 0.0 - 0.84 2 - 0 Algeria Switzerland False
13 Australia Egypt 0.2 - 0.6 1 - 1 Egypt Draw False
14 Argentina Cabo Verde 4.18 - 0.39 3 - 2 Argentina Argentina True
15 Colombia Ghana 0.53 - 0.08 1 - 0 Colombia Colombia True

3.5 Round of 16

Predictions for the current round. The evaluation below fills in automatically as matches are played and the ratings are re-scraped.

Code
round_detail("ensemble", "round_of_16")
team_1 team_2 predicted_score predicted_winner
0 Morocco Canada 3.53 - 0.44 Morocco
1 France Paraguay 3.7 - 1.5 France
2 Brazil Norway 2.54 - 2.33 Brazil
3 Mexico England 1.23 - 2.19 England
4 Spain Portugal 1.71 - 0.63 Spain
5 USA Belgium 1.36 - 1.6 Belgium
6 Argentina Egypt 3.39 - 0.18 Argentina
7 Colombia Switzerland 0.44 - 1.92 Switzerland

4. Scoreboard

Winner accuracy and mean absolute goal error per round, over the matches played so far.

Code
scoreboard(prefixes=("ensemble",))
round matches ensemble_accuracy ensemble_goal_mae
0 Group stage - Matchday 1 24 0.542 2.59
1 Group stage - Matchday 2 24 0.625 1.75
2 Group stage - Matchday 3 24 0.542 1.86
3 Round of 32 16 0.688 1.68

5. Discussion

Two patterns are worth calling out.

First, the deeper the tournament goes, the closer the expected goals of the two sides get. That is not a bug: the knockout bracket filters out weak teams, so the survivors are increasingly similar in strength, and the model’s honest answer is “this one is close to a coin flip”. Many of these matches will realistically be decided in extra time or on penalties, which no goals-based model can call.

Second, winner accuracy and goal error move in opposite directions as the stakes rise. Calling the winner gets harder (closer matchups), while the scores the model misses by grow because knockout football produces more extreme results than the friendlies and qualifiers it trained on.