Premier League predictor
I'm building a model that predicts the outcome of every Premier League game each week — win, draw, or loss, with a probability for each. I'm building it by hand, from scratch, as a way to learn: no ML libraries doing the modeling for me, just NumPy, SciPy, and the math.
The code is public.
What this is
Every week, the model outputs three probabilities for each of the ten Premier League fixtures: home win, draw, away win. The goal is simple to state and hard to do: beat the bookmakers' implied probabilities, week after week, measured honestly on games the model has never seen.
Bookmaker odds are the benchmark, never an input — the model doesn't get to peek at the answer key. Promoted teams stay in; every game gets a prediction, no exceptions.
How the model works
The core is a Dixon–Coles model, the classic statistical approach to football scores. Each team gets an attack rating and a defense rating, there's a league-wide home advantage, and a small correction for low-scoring games. From those ratings the model computes the probability of every scoreline from 0–0 to 10–10, then adds them up into home/draw/away probabilities.
The first version worked but had a flaw: with only a few games of data, newly promoted teams got extreme ratings. After four losses, Coventry's attack rating was so negative the model gave them a 0.04% chance at Nottingham Forest. They won. That one game blew up the week's score.
Version 2 fixes this two ways. First, shrinkage: a gentle prior that pulls ratings toward average until a team has played enough games to earn an extreme one — the same idea as not judging a batsman by his first over. Second, time decay: recent games count more than old ones, with a 180-day half-life. Both settings were tuned on two full past seasons the model was never evaluated on.
Everything is validated walk-forward: to predict a game, the model may only use games played before it. No leakage, no peeking. An Elo ratings system runs alongside as a standing challenger.
How I'll use it
Each week follows the same loop:
- Before the matchweek — publish the model's win/draw/loss probabilities for all ten games.
- After the matchweek — compare predictions against results, write up what the model got right and wrong, and add it as a new section below.
- Iterate — feed what I learn back into the model. Current ideas: better draw calibration, combining the model with Elo, and expected-goals features.
Matchweek 6 October 10–12, 2026
preview — not yet played
Preview, published before a ball is kicked. The model was refit on all 1,950 games played through September 20 (σ=0.5 shrinkage, 180-day time decay). The Elo challenger runs alongside and agrees with 8 of the 10 picks.
| Game | Predicted | Actual | Comments |
|---|---|---|---|
| Arsenal v Leeds | Home win · 60% | The week's second-strongest home lean. | |
| Aston Villa v Brentford | Home win · 39% | Tightest call of the week: 39/28/33. | |
| Chelsea v Bournemouth | Home win · 45% | Elo disagrees — it likes Bournemouth. | |
| Ipswich v Fulham | Away win · 39% | Fulham, one of two away picks this week. | |
| Sunderland v Brighton | Away win · 43% | Brighton clear favorites despite being on the road. | |
| Man United v Tottenham | Home win · 63% | The week's most confident pick. | |
| Crystal Palace v Nott'm Forest | Home win · 35% | A genuine coin flip: 35/31/34. Elo takes Forest. | |
| Hull v Everton | Home win · 43% | Hull favored, but the 36% draw chance is the week's highest. | |
| Liverpool v Man City | Away win · 40% | The big one — City slight favorites at Anfield. | |
| Coventry v Newcastle | Away win · 50% | Newcastle the week's second away banker. |
Matchweek 5 September 18–20, 2026
4/10 correct
The first reviewed week. Ten games, and the model's first outing in its current form.
model got it right model got it wrong Draw the game was a draw
| Game | Predicted | Actual | Comments | |
|---|---|---|---|---|
| Brentford 3–0 Chelsea | Home win · 47% | Home win | Strongest home lean of the week; the market slightly favored Chelsea. | ✓ |
| Tottenham 2–3 Aston Villa | Away win · 35% | Away win | The model's only away pick of the week — and it disagreed with the market, which had Spurs at 48%. | ✓ |
| Brighton 3–0 Arsenal | Away win · 45% | Home win | Model and market both liked Arsenal. | ✗ |
| Everton 1–0 Ipswich | Home win · 60% | Home win | The comfortable one — everyone agreed on Everton. | ✓ |
| Newcastle 2–1 Hull | Draw · 34% | Home win | Model read it as a coin flip (34/34/32). | ✗ |
| Nott'm Forest 0–1 Coventry | Home win · 70% | Away win | Wrong pick, better calibration: v1 gave Coventry 0.04%, v2 gave 7%. | ✗ |
| Bournemouth 0–1 Liverpool | Home win · 40% | Away win | Model overrated Bournemouth's strong start; the market favored Liverpool. | ✗ |
| Leeds 0–0 Crystal Palace | Home win · 55% | Draw | Nobody saw the 0–0 coming; draws remain the blind spot. | ✗ |
| Man City 5–3 Sunderland | Home win · 68% | Home win | The bankers of the week — City at home. | ✓ |
| Fulham 1–1 Man United | Away win · 38% | Draw | Model leaned United (38%) in an open game; draw was its second choice (29%). | ✗ |
What I take from this week
Four out of ten — level with the bookmakers on accuracy, a touch behind on log loss (1.144 vs 1.080), and behind the Elo challenger, which had the best week of the three at 5 of 10. An honest, average week.
The encouraging part: the two games where the model disagreed with the market, it was right both times — Brentford over Chelsea and Villa at Spurs. The worrying part is familiar: draws. The model made a draw its top pick exactly once all week, and two draws happened. Fixing draw calibration is the top modeling priority.
And the Forest–Coventry game did its job as a stress test. The old model's 0.04% was a failure of humility; 7% is a number a reasonable person could hold. Shrinkage works as advertised.
Code
Everything — model, validation, and tuning — is on GitHub.