Elo ratings are the simplest serious football model: one number per team, updated after every match. In about thirty lines of Python you can rate every Premier League team since 2015-16 and turn the ratings into win, draw and loss probabilities. This tutorial uses the clean matches table from Load and Clean Football Data With pandas.
How Elo works
Each team has a rating, starting at 1500. Before a match, the difference between the two ratings gives the home team's expected score:
E = 1 ÷ (1 + 10^(−(R_home + HFA − R_away) ÷ 400))
where HFA is a home-advantage bonus in rating points. After the match, each rating moves by K × (actual − expected), where the actual score is 1 for a win, 0.5 for a draw and 0 for a loss. K controls how fast ratings react.
The code
K, HFA, DRAW = 20, 60, 0.25
ratings = {}
probs = []
for row in matches.itertuples():
rh = ratings.get(row.HomeTeam, 1500.0)
ra = ratings.get(row.AwayTeam, 1500.0)
e_home = 1 / (1 + 10 ** (-(rh + HFA - ra) / 400))
# turn the expected score into three probabilities
p_home = max(e_home - DRAW / 2, 0.02)
p_away = max(1 - e_home - DRAW / 2, 0.02)
s = p_home + DRAW + p_away
probs.append((p_home / s, DRAW / s, p_away / s))
score = {"H": 1.0, "D": 0.5, "A": 0.0}[row.FTR]
ratings[row.HomeTeam] = rh + K * (score - e_home)
ratings[row.AwayTeam] = ra - K * (score - e_home)
matches[["elo_h", "elo_d", "elo_a"]] = pd.DataFrame(probs)
Two design choices to notice:
- Probabilities are recorded before the update. Each forecast uses only matches played earlier, so there is no leakage from the future.
- Draws are handled crudely. Elo has no concept of a draw, so we give every match a fixed draw probability of 25% and split the rest using the expected score. It is simple and it works, but it is the first thing to improve.
The ratings at the end of 2025-26
top = sorted(ratings.items(), key=lambda kv: -kv[1])[:5]
print([(team, round(r)) for team, r in top])
Our output:
| Team | Elo rating |
|---|---|
| Arsenal | 1774 |
| Man City | 1753 |
| Liverpool | 1664 |
| Man United | 1657 |
| Aston Villa | 1646 |
That matches the shape of the 2025-26 table, in which Arsenal won the title ahead of Manchester City.
Choosing the parameters
We tested a few settings by scoring forecasts from 2019-20 to 2025-26 (2,660 matches) with the Brier score, where lower is better:
| Setting | Brier score |
|---|---|
| K = 10 | 0.5923 |
| K = 20 | 0.5890 |
| K = 30 | 0.5894 |
| No home advantage (HFA = 0) | 0.5938 |
| HFA = 60, new teams start at 1450 | 0.5887 |
| HFA = 100, new teams start at 1450 | 0.5972 |
Watch out: We tuned these settings on the same seasons we then report, which flatters the best numbers slightly. For an honest result, choose parameters on earlier seasons and test on later ones, as explained in Score Your Model With the Brier Score.
Key point: Home advantage matters: removing it makes the forecasts clearly worse. Starting promoted teams a little below 1500 helps slightly, because new teams in the data are mostly promoted sides.
Ideas to improve it
- Model draws properly, for example with an ordered logit on the rating difference.
- Scale K with the goal difference, so a 4–0 moves ratings more than a 1–0.
- Regress ratings towards the mean between seasons.
To compare Elo with goal-based forecasts, try the Poisson match predictor.
- Elo needs only results and gives every team one number, updated after each match.
- Record forecasts before updating, so every prediction uses past data only.
- In our test, K = 20 and a home advantage of about 60 points worked best; draws are the weak point.
Sources
- Elo rating system — Wikipedia
- English football results and data — Football-Data.co.uk
18+ only. Models do not guarantee results. See our Responsible Gambling page.