1. Turning recent matches into ratings
The constants on this page are the exact values in our code, not approximations.
Order a club’s last 15 completed matches from newest to oldest and number them i = 0, 1, 2 … Match i gets weight 0.9i: the newest weighs 1, the next 0.9, and the oldest of the fifteen roughly 0.23. The attack and defence ratings are weighted averages over that window:
att = Σ wi·fi / Σ wi · def = Σ wi·ai / Σ wi · wi = 0.9i
f and a stand for goals scored and conceded; we swap in xG for and against once xG data accounts for 60% or more of the window’s summed weight. Measuring coverage by weight rather than by match count matters: a gap in last week’s data counts for more than a gap from months ago.
Each rating is then shrunk toward μ, the competition’s average scoring rate over its two latest seasons as of this kick-off:
shrunk = (n·v + 5·μ) / (n + 5)
where n is the number of matches in the window. The 5 acts as prior strength: after fifteen matches a club keeps three quarters of its own value, after three matches just over a third. That is how a promoted club’s first couple of results are stopped from producing an extreme rating.
2. Expected goals for each side
The shrunk ratings combine into a scoring rate for each team, scaled by the competition’s average home and away goals (mh, ma), with μ the midpoint of the two:
λhome = mh · (atthome / μ) · (defaway / μ)
λaway = ma · (attaway / μ) · (defhome / μ)
There is no separate home-advantage bonus: it is already contained in mh and ma, measured from each competition’s own results. When a league-specific fit is available, λhome also carries a league-fitted home multiplier. Both rates are then clamped between 0.2 and 4.5 goals, so corrupted input data cannot produce an absurd forecast.
3. The Dixon-Coles scoreline grid
We model each side’s goals as a separate Poisson count and then correct the four scorelines with the fewest goals. Plain Poisson consistently misjudges the 0-0, 1-1 and single-goal wins, and correcting that is exactly what Dixon and Coles proposed. For scoreline i-j:
P(i, j) ∝ Pois(i; λhome) · Pois(j; λaway) · τ(i, j)
τ(0,0) = 1 − λhomeλawayρ
τ(0,1) = 1 + λhomeρ
τ(1,0) = 1 + λawayρ
τ(1,1) = 1 − ρ
τ(i,j) = 1 everywhere else
Goals run from 0 to 10 per team, giving 121 cells, normalised to sum to one. ρ is fitted per competition where a fit exists; without one, ρ defaults to −0.10 and the prediction carries the fallback model-version tag, so you can tell the two apart.
4. Deriving each market
One grid, many markets: every number we publish adds up cells of it.
| Market | Definition on the grid |
|---|---|
| Home / Draw / Away | Σ cells with i > j · Σ cells with i = j · Σ cells with i < j |
| Over/under 2.5 | Σ cells with i + j ≥ 3; under is the remainder |
| Both teams to score | Σ cells with i ≥ 1 and j ≥ 1 |
| Correct score | a single cell |
| Double chance, draw, handicap, goals | sums of the same cells, grouped differently |
Because all markets are cut from one distribution, our goal line cannot clash with the exact-score grid, and double chance cannot clash with 1X2. Services that model each market independently cannot promise that. For each market and the fixtures behind it, see prediction types.
5. Stripping the margin, then mixing
For home, draw and away we take one price each: the median across all bookmakers that quote the complete three-way market for the fixture. The implied probabilities of those three prices add up to more than one, and the surplus is the bookmaker margin. We remove it by normalising:
qk = (1 / pricek) / Σj (1 / pricej)
The 1X2 probability we publish is a fixed-weight mix of that market figure and our model:
final = 0.25 · model + 0.75 · market
With no recorded price, final equals the model probability and the prediction is flagged as model-only. Only 1X2 is blended. Goal-line, BTTS and exact-score probabilities are saved with zero market weight — what you read is the grid’s raw output.
The edge we show is the difference between our published probability and the market’s: final − market, in percentage points, for the same outcome. With a 25/75 mix, the edge equals one quarter of the raw gap between model and market — a cautious reading by design. Beside the best available price we also show expected return per unit staked: published probability × price − 1. Each is a statement about the price, never a promise about the result. The largest gaps are collected on value bets.
Does it actually work?
Formulas prove nothing on their own. The evidence is on the accuracy page: an unseen test season, a comparison with a naive baseline, and calibration error for every league — the one that broke our tolerance included.