World Cup combinatorics
June 15, 2026 at 8:54 PM by Dr. Drang
We’re in the middle of the first set of games in the group stage of the 2026 World Cup, and I’ve been thinking about how many ways the points can be distributed among the teams in a given group. I used Python to help with the enumeration.
Here’s a quick summary of how the group stage works: The teams are split into groups of four. Within each group, all the teams play each other once. A team gets three points for a win and one point for a draw in each of the three games it plays. The teams are ranked by their point totals within their group when this stage is over.1
Six games are played in each group. You can calculate that number in several ways, but it’s easy enough to just list them all. Let’s say the teams in our group are Alpha, Beta, Gamma, and Delta. Then the six games are:
Alpha vs. Beta
Alpha vs. Gamma Beta vs. Gamma
Alpha vs. Delta Beta vs. Delta Gamma vs. Delta
Since there are three possible outcomes in each game (W–L, L–W, and D–D), there are
possible results for the six games of the group stage.
We can get the point distribution among the teams for each of these possible results with this bit of Python code:
python:
1: #!/usr/bin/env python3
2:
3: from itertools import product
4: import numpy as np
5:
6: # Possible point distributions from each game.
7: game1 = [[3, 0, 0, 0], [0, 3, 0, 0], [1, 1, 0, 0]]
8: game2 = [[3, 0, 0, 0], [0, 0, 3, 0], [1, 0, 1, 0]]
9: game3 = [[3, 0, 0, 0], [0, 0, 0, 3], [1, 0, 0, 1]]
10: game4 = [[0, 3, 0, 0], [0, 0, 3, 0], [0, 1, 1, 0]]
11: game5 = [[0, 3, 0, 0], [0, 0, 0, 3], [0, 1, 0, 1]]
12: game6 = [[0, 0, 3, 0], [0, 0, 0, 3], [0, 0, 1, 1]]
13:
14: # Enumerate all possible six-game results, add up the points, and sort.
15: games = np.array(list(product(game1, game2, game3, game4, game5, game6)))
16: points = games.sum(axis=1)
17: points = points.tolist()
18: points.sort(reverse=True)
19: print(f'Number of point arrangements: {len(points)}')
The output is
Number of point arrangements: 729
which matches our calculation above.
Lines 7–12 give the three possible point allocations for each of the games, where the inner lists are the points given to Alpha, Beta, Gamma, and Delta—in that order. Line 15 uses the product function from the itertools library to build all 729 outcomes. It turns them into a NumPy array because I wanted to use the sum function (Line 16) to add up the points from each game without any laborious looping. After this step, points is a 729×4 NumPy array.
Line 17 then turns points into a list of lists, and Line 18 sorts the 729 entries. It looks like this:
[[9, 6, 3, 0],
[9, 6, 1, 1],
[9, 6, 0, 3],
[9, 4, 4, 0],
[9, 4, 3, 1],
.
.
.
[0, 4, 5, 7],
[0, 4, 4, 9],
[0, 3, 9, 6],
[0, 3, 7, 7],
[0, 3, 6, 9]]
There are repeats among these results. For example, there are two ways to get the result
[5, 5, 4, 1]
This set of points can come from either
Alpha draws Beta [1, 1, 0, 0]
Alpha beats Gamma [3, 0, 0, 0]
Alpha beats Delta [3, 0, 0, 0]
Beta draws Gamma [0, 1, 1, 0]
Beta beats Delta [0, 3, 0, 0]
Gamma beats Delta [0, 0, 3, 0]
or
Alpha draws Beta [1, 1, 0, 0]
Alpha draws Gamma [1, 0, 1, 0]
Alpha beats Delta [3, 0, 0, 0]
Beta beats Gamma [0, 3, 0, 0]
Beta draws Delta [0, 1, 0, 1]
Gamma beats Delta [0, 0, 3, 0]
Sum down the columns to prove to yourself that this yields point totals of
[5, 5, 4, 1]
OK, so how many unique point total results can there be? To answer this, I added the following code:
python:
21: # Unique points, team-by-team.
22: unique_team_points = [ list(q) for q in set(tuple(p) for p in points) ]
23: unique_team_points.sort(reverse=True)
24: print(f'Number of unique point arrangements: {len(unique_team_points)}')
which output
Number of unique point arrangements: 556
Unlike the 729 results we got earlier, I have no formula for calculating this number, and I have no interest in trying to come up with one. This is one of those situations where brute force programming is the best use of my time.
Line 22 uses the common Python trick of turning a list into a set to eliminate duplicates and then turns the set back into a list. The less-common trick is that it first turns the inner lists into tuples. This is needed because something like
set(points)
returns this error:
TypeError: cannot use 'list' as a set element (unhashable type: 'list')
In these 556 results, we kept the teams separated. In other words, we considered results
[9, 6, 3, 0]
[9, 6, 0, 3]
[9, 3, 6, 0]
[9, 3, 0, 6]
[9, 0, 6, 3]
[9, 0, 3, 6]
etc.
to be different because the team scores were different. But what if we just wanted to know the possible point combinations without regard to which team got which point total? Again, I have no formula for this, but it was easy to work it out in code:
python:
26: # Unique points without regard to team.
27: sorted_points = sorted((sorted(p, reverse=True) for p in points), reverse=True)
28: unique_points = [ list(q) for q in set(tuple(p) for p in sorted_points) ]
29: unique_points.sort(reverse=True)
30: print(f'Number of unique sets of points: {len(unique_points)}')
31: print()
32: point_strings = [ f'{p[0]} {p[1]} {p[2]} {p[3]}' for p in unique_points ]
33: print('\n'.join(point_strings))
The first line of output is
Number of unique sets of points: 40
which is quite a reduction. Reshaping the 40 lines that followed into four columns of ten gives
9 6 3 0 7 6 3 1 7 3 2 2 5 5 3 2
9 6 1 1 7 6 2 1 6 6 6 0 5 5 3 1
9 4 4 0 7 5 4 0 6 6 4 1 5 5 2 2
9 4 3 1 7 5 3 1 6 6 3 3 5 4 4 3
9 4 2 1 7 5 2 1 6 5 4 1 5 4 4 2
9 3 3 3 7 4 4 1 6 5 2 2 5 4 3 2
9 2 2 2 7 4 3 3 6 4 4 3 5 3 3 2
7 7 3 0 7 4 3 2 6 4 4 2 4 4 4 4
7 7 1 1 7 4 3 1 5 5 5 0 4 4 4 3
7 6 4 0 7 4 2 2 5 5 4 1 3 3 3 3
These are the point totals for the 40 different ways a group stage can end. The two levels of sorting in Line 27 are the key to bringing the results down from 729.
You can go to ESPN’s World Cup table for 2022 to show which of these 40 results occurred that year. There’s a popup menu button on the page that lets you check 2018, 2014, 2010, and 2006, too.
I’m sure serious soccer fans have been digging into the math behind this year’s change from eight groups to twelve. I’m not a serious fan, so this is good enough for me.
-
I won’t be going through the rules of how teams advance to the next stage—they start out simple but can get complicated quickly, especially now that the tournament has expanded. ↩