I Built a Robot Instead of Having Opinions
changed: monte carlo simulation visualisation added
The Pitch
The World Cup has rolled around once again, and as a workplace tradition, there’s a prediction league1 that most of us got involved in for a bit of fun, not to mention the honourable bragging rights of topping our little leaderboard. This was the first one for me, and although football offers little entertainment value personally, the possibility of predicting the outcome based purely on historical data seemed rather enticing.
The only issue? I know nothing about football. Or rather, I know nothing of the state of play when it comes to teams’ performances. So, I set out to create a robot to figure it all out for me.
But we have to rewind a little as to how we got there.
As a somewhat cathartic exercise in humouring my current interests, I was looking into physics and radioactive decay. One rabbit hole later and I stumbled across the Poisson distribution2 and wondered whether or not this could have some application to our little prediction league. Sure enough, my excitement at discovering a new application for this formula was swiftly curtailed after a little research showed this was a well-trodden field. After some research into what others more well versed had produced, I set out to implement what I hoped to be a winning formula.
Before moving onto the good stuff, a little aside on how the prediction league works:
| Correct Prediction | Points Scored |
|---|---|
| Outcome (win/draw/loss) | 1 |
| Score | 3 |
In addition to the above, for each group/knockout stage there’s the use of a Joker. The application of which can only be used once at each stage and has the effect of doubling your points for the specific fixture you use it on (2 points for correct outcome, 6 for correct results). Your chosen team to lift the World Cup trophy needs to be locked in by the knockout stages but alterations to the scoreline can be made until right before kickoff.
The first stage was acquiring the data, of which there’s a surprisingly large amount publicly available. The problem was making sure I had the right kind of data. After some research into how this prediction engine might work, all I really needed was the outcomes of as many football matches between international teams. After a little digging I found 49,499 (at the time of initial collection) international football results since 1872, courtesy of martj42’s public dataset3, perfect. Alongside the results file, I used the shootouts file to provide the outcomes of any matches that went to penalties.
And that’s it.
Every prediction the robot makes starts from the same two numbers, more on that below.
I Care About Nothing But Net
It seems a little counterintuitive at first, but the predictive model cares nothing for how many games you won, lost or drew. The only factor it considers is the goal breakdown, how many were scored, how many were conceded, and importantly, against whom.
The model assumes that each side’s goal rate, , is multiplicative (Maher 19824):
(i vs j) = (baseline) × (i’s attack multiplier) × (j’s defence multiplier)
The adjustments of these variables are the whole trick: scoring 3 past Brazil says more than scoring 3 past a minnow. However, this calculation is circular: to rate the att of a team you must know the def of the opposition to calculate its impact.
To fix this chicken and egg problem, every att and def starts at 0. From here we freeze the defence scores for all teams and fit the attack score given the current defence value of 0. We then freeze the attack score and refit the defence score. In addition to these two, both mu (the average) and home_adv are refit in the same way. Each pass round these freeze and refit cycles looks to improve the fit, so it can never oscillate forever.
A fictitious fitting (click to expand)
To help visualise this fitting loop I’ve generated a small sample which uses the same maths behind the ~200 team results data:
Four fake football teams, Astoria, Borland, Cresta and Dovia, and their Sunday league.
| Matches |
|---|
| Astoria 2-0 Dovia |
| Astoria 2-1 Borland |
| Astoria 0-0 Cresta |
| Borland 3-2 Dovia |
| Borland 1-1 Cresta |
| Cresta 1-0 Dovia |
League totals:
| Team | Scored | Conceded |
|---|---|---|
| Astoria | 4 | 1 |
| Borland | 5 | 5 |
| Cresta | 2 | 1 |
| Dovia | 2 | 6 |
Every team starts with att and def at 0. mu starts at the log of the average goals per side, 13 goals / 12 matches = 1.083, mu .
With all defences frozen at 0, each side’s “expected if average” is the same for every match. .
team Σk (scored) Σr ratio att = log(ratio)
Astoria 4 3.25 1.231 +0.208
Borland 5 3.25 1.538 +0.431
Cresta 2 3.25 0.615 −0.486
Dovia 2 3.25 0.615 −0.486
Note Cresta and Dovia get identical attacks: same goal count, and the model doesn’t yet know their opponents apart. Remember that, it’s about to change.
Defence update (using the fresh attacks):
team att def mu = −0.409 (baseline 0.664)
Astoria +0.029 (×1.03) +0.928 (÷2.53)
Borland +0.936 (×2.55) −1.191 (÷0.30)
Cresta −0.672 (×0.51) +1.056 (÷2.87)
Dovia −0.293 (×0.75) −0.792 (÷0.45)
Cresta and Dovia’s attacks split apart (−0.67 vs −0.29) despite identical goal totals - the loop worked out who the goals came against and priced the schedules differently. This is the opponent adjustment indicated with Brazil above, visible in the wild.
Astoria’s attack is… average (+0.03), even though it out-scored everyone but Borland. The model looked at its 4 goals, noticed they came against the two leakiest defences in the universe (Borland ÷0.30, Dovia ÷0.45), weighed the 0-0 against Cresta, and concluded: goals explained by opposition, not by attack. Astoria’s dominance lives almost entirely in its defence (+0.93). With three matches of evidence, one blank is loud.
Borland shows attack ×2.55, defence ÷0.30 (concedes more than three times the average).
Model Parameters (“the tuning knobs”)
The elephant in the room however, was that every historical match had an equal impact on the fitting of these values. It seems a little unfair that a football match that took place before any of the current team players were born should have the same weighting as their own personal contribution.
As part of the model simulation there were 4 distinct constants. Each of these had a particular impact on the teams’ ratings:
- Recency decay - 2 years: a match 2 years ago counts half as much as one today, if it were 4 years ago then a quarter of what it counts today, etc. This meant that anything pre-2015 had less than a 2% impact on any ratings.
- Friendlies - 0.5: any friendly matches held only 50% of their potential impact.
- World Cup boost - 3.0: a weight multiplier for 2026 tournament matches. Any matches played in the tournament had a 3 times weighting for the rating.
- Host bonus - 0.2: a log-scale boost to hosts’ expected goals (USA/Canada/Mexico). This was calibrated against previous World Cups.
Each of these tuning knobs was tested in isolation and was tested against a fixed seed and run against 4 historical World Cups.
All tuning was locked in by 13th June to avoid any overfitting/look-ahead bias.
Tuning for Improved Fit (click to expand)
Two of the tuning knobs, half-life and World Cup boost, were a little more nuanced to determine. The best course of action was to backtest these against historical tournaments to see what value helped to fit results that we already know the outcome of.
Each cell replays the knockout rounds of four past World Cups (2010, 2014, 2018, 2022) with ratings refitted as of each tournament’s group-stage end under that setting, then scores the predictions two ways:
- Pooled log-loss (lower = better; punishes confident wrong answers hardest)
- Correct advance picks.
Darker cell = better log-loss, the outlined cells are the live settings used for the simulation.
The knob with genuine signal is the memory (half-life) one. Although the log-loss was lower at 1 year, at year 2, an additional correct knockout outcome was predicted for minor log-loss increase.
So, two grades per team (`att` and `def`). But how do two become one (outcome/score)?
The Poisson Bit
In 1898, Russian economist and statistician Ladislaus Bortkiewicz published a book titled The Law of Small Numbers, in which he observed that rare events in a large population follow a Poisson distribution even when the underlying probabilities vary. To show that the distribution was more prominent than originally thought, he turned to Prussian army records: the number of soldiers killed by horse kicks each year, in each of the 14 cavalry corps, over a 20-year period. Those counts followed a Poisson distribution.
Goals follow this same species of event, namely rare, roughly independent and take place within a fixed amount of time.
Before we get into the Poisson distribution formula, it’s important we understand how the goal rate, , is calculated based on the teams’ attack and defence multipliers we discussed in the previous section:
(i vs j) = (baseline) × (i’s attack multiplier) × (j’s defence multiplier)
These attack and defence multipliers are exponential () values, so if we take the of these, we can turn the multiplication into addition:
mu- log of the average team’s goals against an average teamatt_i- teams i’s attack on a scale. att = +0.10 → → ×1.105, i.e. +10.5% goals in every fixture, where average = 0.def_j- team j’s defence, same scale as the attack but the sign is flipped so that higher is better (concedes fewer goals). A team facing a defence of +0.50 divides the team’s attack by .home_adv- this is a shared bump in stats for the home side. The fitted value for this came out at 0.226 which is a 1.25 multiplier, not bad given the well known (apparently) ~30% home effect. At the World Cup, almost everything is neutral ground, so this mostly mattered for fitting against the historical data. The three hosts in this year’s World Cup instead got a fixedHOST_BONUSof 20% at prediction time.
The use of exponentials is important. Firstly, rates () must be positive, regardless of what each team’s ratings are, guarantees this. Secondly, the effect should compound, a good attack helps proportionally against a weak defence.
Some real numbers from the current fit (as at 2026-07-02):
| Team | att | def |
|---|---|---|
| Spain | +1.573 | +1.558 |
| France | +1.549 | +1.416 |
| Argentina | +1.485 | +1.773 |
| England | +1.420 | +1.402 |
| Panama | +0.608 | +0.587 |
mu = -0.007 (baseline 0.99 goals)
home_adv = 0.226
Plugging in these values for Argentina’s rate against France:
And France’s rate against Argentina:
Argentina’s edge in this fixture is entirely down to its superior defence. However, an average of 1.065 is not a prediction of 1.065 goals, it’s a scoring menu.
This is where the Poisson distribution comes in. If something happens at an average rate of per window, and each occurrence is roughly independent of the last, then the probability of seeing exactly k occurrences is:
Where the formula comes from without calculus
If you were to chop the football match into 90 one-minute slices, a team averaging = 1.4 goals per match scores in any given minute with probability ~1.4/90 1.5%. The chance of no goals in 90 straight minutes is - and . That’s not a coincidence, the Poisson formula is exactly the limit of this minute-by-minute counting, and the slices get infinitely thin.
The current fit gives Argentina = 1.065 against France. The goal menu:
P(0) = exp(−1.065) = 0.345 (34%)
P(1) = exp(−1.065) · 1.065 = 0.367 (37%)
P(2) = exp(−1.065) · 1.065²/2 = 0.195 (20%)
P(3) = exp(−1.065) · 1.065³/6 = 0.069 ( 7%)
Those are literally the bars in the goal-menu figure below. Note the headline lesson: an average of ~1 goal means scoring zero is nearly as likely as scoring one.
The visualisation below shows the probabilities of xG (expected goals) for each team, i.e. Argentina to score 0 goals against France 35%, 37% to score 1. It’s these menus that will be used in the simulations, which we’ll look into in the next section.
The Poisson distribution has a single parameter, its mean . This is why the entire modelling problem can be reduced down to a single good per team per fixture.
That’s just one side of the coin though, there’s also a goal menu for the opposing side and by multiplying the two menus for each xG5, every scoreline has a price. This is what will be used to determine the exact score used in the prediction.
The scoreline grid offers a humble realisation. The most likely score is only 16-17% with the current team ratings, not exactly a clear answer. To emphasise this point a little more, have a look at the interactive scoreline grid below. Drag both sliders to 0.80 for both teams and watch how 0-0 and 1-0 dominate the predicted outcomes; this is why knockout football can be such a coin flip. Drag one of those lines to 3.0 and the favourite still only wins ~82%.
A known flaw (click to expand)
The scoreline grid assumes the two scores are entirely independent. Dixon & Coles (1997)6 showed low-scoring draws are slightly more common than independence implies. I have deliberately not corrected the model given its simplicity offsets the minor log-loss improvement.
The honest output of all this is that a ‘clear favourite’ prices out a W/D/L near 50/28/22. The better team draws or loses all the time, and the model refuses to pretend otherwise.
Now that we’ve a price for every scoreline of every fixture, it’s time to do something with it. After all, a World Cup is not 104 independent fixtures.
Monte Carlo Simulations
There are two distinct parts to this model: questions to be answered.
- What score do I predict for this fixture?
- Who wins the group / makes the final / wins it all?
The second of these is the macro question and requires the chaining of many possibilities to come to a conclusion. The Poisson distribution answers the first question given it sums the 81 cells (8 goals as the maximum scoreline, 9×9 grid) and can calculate the optimal prediction.
The Monte Carlo7 rolls from those same Poisson menus only this time the same distributions are executed with dice instead of summed with algebra.
The Monte Carlo rolls the dice 100,000 times for match outcomes based on a sampling of the Poisson menus, rolling up and counting the scoreline for each match. The most common of which becomes the predicted outcome (note this is not the predicted score used for point scoring). This builds a picture for what the model believes will be group winners, knockout results and the overall winner.
The visualisation below shows the reason for running so many simulations. The x-axis is logarithmic, so at 10 simulations the “estimate” swings by tens of points, at 100 it is still wobbling by several, by a few thousand the lines are flat. The mini simulation below runs to 4,000 simulations; however, a similar story is shown at 100,000 simulations.
The only place that the model refuses an opinion is for penalty shootouts. For the knockout stages and beyond, any draws that were predicted had extra time added at a third of the slices for Poisson distribution and penalties were a 50/50 coin flip.
Now that our simulations have provided probabilities, we need scorelines.
The Decision-Theory Layer
Determining the probability of a match is just the first part of the problem though. To win the league I’d need to optimise my scorelines to ensure that I give myself the best chance of winning. So far we’ve produced the probability of scorelines coming to fruition, and the initial reaction would be to choose this as our scoreline. After all, it’s the most likely one.
However, choosing the scoreline is a separate, non-statistical problem. What we want to do is maximise our expected payoff under the league’s scoring rule as indicated in The Pitch section. Three points for exact score, one for outcome and none for an incorrect outcome.
For this we need another equation to calculate the expected points (EP) for a scoreline:
A scoreline pick’s value is twice its chance of being exactly right, plus once its chance of being directionally right. Every one of the 81 grid cells for a fixture gets this score, and the model takes the maximum value.
Importantly, the most likely scoreline does not automatically make it the best pick as the modal score only maximises the first term, whereas this calculation lets the outcome term vote too.
If we again cast our minds back to the scoreline grid, geometrically, a draw pick banks only on the diagonal; a win pick banks the whole triangle to one side of it.
To explain this a little clearer, consider the scoreline grid below for Mexico v United States:
We can extract two important probabilities from this table (although 1-0 shows 12%, this was rounding precision and is in fact 11.9%):
| pick | P(exact) | P(outcome) |
|---|---|---|
| 1-1 (most likely score) | 12.0% | 25.1% |
| 1-0 (EV-optimal) | 11.9% | 50.8% |
Taking these probabilities, we can then plug each cell into the equation to get the following:
Giving up one-tenth of a percentage point of exact-hit chance and I can double the outcome mass of the pick - that’s +52% expected points, for free. The optimal pick is a scoreline of the favoured outcome except in genuine coin flips; this is because the draw diagonal is nearly always the smallest of the three regions. As a result, the robot almost never predicted draws, not because they are rare - in fact the model predicted 25-32% on average - but because the scoring rule punishes draw picks.
The use of the joker was fairly simple: whichever fixture in the group/knockout stage had the highest EP, that’s the one it went on. The idea being, spend it on the most boring mismatch available and hope there’s not an upset, which of course, there will be.
The knockout correction (click to expand)
In the knockout stages, there must be a winner, and so over optimising the 90-minute grid is subtly wrong. A 1-1 pick can’t cash if the tie ends 2-1 in the 105th minute. For this, an additional corrected grid was created, and the logic was just to follow each route to the final score. Each predicted draw in the knockout stages spawns a fan of possibilities given the additional 30 minutes of extra time. This is its own Poisson grid using for each side.
Perhaps Mexico v USA ends 1-1 at 90-minutes; however, using the knockout correction full-time becomes 2-1 if the ET goes 1-0, which the additional grid prices at 22%:
P(full-time 2-1) = P(2-1 at 90') + P(1-1 at 90') × 22%
└ route 1: no ET ┘ └ route 2: via extra time ┘
For the two non-overlapping routes, probabilities are summed.
So the robot knows what to predict and how much each prediction is worth. Time to see what it actually said.
The Cliffhanger
As a preface to the bracket graphic below and all of the visualisations above, they were all generated using simulation snapshots up to July 2nd 2026. At the time of writing this, there have been a number of upsets, some exact scorelines and quite a few correctly identified outcomes.
The above is the single most likely remaining tournament based on the simulated model. Every tie shows the best-expected-points score and the winner’s chance of advancing. However, this exact script is wildly unlikely; it is just more likely than any other outcome.
The model's two finals (click to expand)
As an interesting aside, the model simulation was run on 7th July (visualisations are frozen at July 2) and it produced two different finals, and both are right. The simulation predicted that the most frequent pairing across 100,000 simulations was in fact Spain v England (15.7%). The bracket, however, was built match by match and instead ended with Spain v Argentina. This divergence is due to how the model builds its pathway to the final. In the 7th July simulation, England needed only a single win against Norway to advance to the semi-finals, whereas Argentina are yet to play and must first get past Egypt and then play the quarter-final match. As a result, summed over every simulated tournament, England are in the final more often than Argentina (~35% vs ~29%) even though they usually lose the head-to-head.
The most common destination and the most likely route are different questions, and they split apart when the head-to-head favourite has the harder road (at that point in time of the simulation). The two finals may re-converge or swap as the knockouts continue to play out, but that’ll become apparent soon enough.
The final is to be played on the 19th July and I intend to follow this up with a part 2, a post-mortem:
What did the tournament do to the model's hidden numbers?
Did a robot that promised itself 60 points in the group stages actually collect them?
A cruel poetic irony has been wrought. I write this article to showcase my creation's knowledge of football, a robot I built to avoid having opinions. And yet, the robot has decided to alter its opinion of the predicted World Cup winner. Alas, I'm locked in and await the verdict.
Footnotes
-
Maher (1982), “Modelling association football scores”, Statistica Neerlandica 36(3), 109–118 ↩
-
Here, the xG (expected goals) means the model’s pre-match average, not the TV shot statistic ↩
-
Dixon & Coles (1997), “Modelling Association Football Scores and Inefficiencies in the Football Betting Market”, JRSS C 46(2) ↩