1. From match history to a scoring rate
Every constant below is a constant in the code, not a rounded description of one.
Take a club’s last 15 completed matches, newest first, indexed i = 0, 1, 2 … Each match carries weight 0.9i, so the most recent counts 1, the one before it 0.9, and the fifteenth about 0.23. An attacking figure and a defensive figure come out of that weighted window:
att = Σ wi·fi / Σ wi · def = Σ wi·ai / Σ wi · wi = 0.9i
where f and a are goals for and against — or expected goals for and against, when at least 60% of the window’s weight is covered by an expected-goals record. Share of weight rather than share of matches is the point: a missing record in last weekend’s match matters more than a missing one from December.
Both figures are then shrunk toward the competition’s own scoring mean μ, taken from its two most recent seasons as they stood before this kick-off:
shrunk = (n·v + 5·μ) / (n + 5)
with n the number of matches actually in the window. Five is the strength of the prior: a club with fifteen matches keeps three quarters of its own figure, a club with three keeps a bit over a third. This is what stops a promoted side’s two-match start from producing a confident rating.
2. The two expected-goal rates
The shrunk figures combine into one rate per side, relative to the competition’s home and away means (mh, ma), with μ their midpoint:
λhome = mh · (atthome / μ) · (defaway / μ)
λaway = ma · (attaway / μ) · (defhome / μ)
Home advantage is not a bonus we add — it is already inside mh and ma, measured from that competition’s own results. Where a per-league fit exists, λhome also carries a fitted home scaling factor. Both rates are finally clamped to the range 0.2 to 4.5 goals, so a broken input cannot produce a fixture the model thinks will finish 9-0.
3. Dixon-Coles: the scoreline grid
Goals for each side are modelled as independent Poisson counts, and then the four lowest-scoring cells are corrected — plain Poisson gets 0-0, 1-0, 0-1 and 1-1 measurably wrong, which is the observation the Dixon-Coles adjustment exists to fix. For a 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
The grid runs from 0 to 10 goals per side — 121 cells — and is normalised so it sums to one. ρ is fitted per competition where we have a fit for it; where we do not, it falls back to −0.10, and the prediction is stamped with the fallback model version rather than the fitted one.
4. Reading the markets off the grid
Every market on the site is a sum over the same 121 cells.
| 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 they are all the same object viewed from different angles, our over/under number can never disagree with our correct-score grid, and our double chance can never disagree with our 1X2. Sites that price each market separately have no such guarantee. Market by market, with the fixtures behind each: prediction types.
5. De-vigging the price, and the blend
First the three selections are reduced to one price each: the median across every bookmaker quoting the full three-way market on that fixture. Those three prices imply probabilities that sum to more than one — the excess is the margin — and removing it is a normalisation:
qk = (1 / pricek) / Σj (1 / pricej)
and the published 1X2 probability is a fixed-weight mixture of that and ours:
final = 0.25 · model + 0.75 · market
Where no price is recorded, final is the model probability unchanged, and the prediction is marked as model-only. This step touches 1X2 and nothing else — over/under, both teams to score and correct score are stored with no market component at all, so the number you read on those is the raw output of the grid above.
The gap between the two inputs is what the site calls an edge: model − market, in percentage points, on the same selection. It is a claim about the price being wrong, not about the result being certain. The fixtures where that gap is largest are listed on value bets.
How well any of this works
Equations are cheap. The measurement is on the accuracy page: the hold-out season, the baseline comparison, the calibration error per league — including the league that missed our own tolerance.