Machine learning · 25,979 matches

Can a model beat the betting market?

Bookmaker odds are a forecast made with injury reports, team news, and money at stake. This model uses only public match data and never sees a price.

Held-out fixtures · model above, market below HomeDrawAway
Real Madrid 2-0 Atlético Madrid
Model
Market
Manchester United 3-0 Liverpool
Model
Market
Arsenal 0-0 Chelsea
Model
Market
Chelsea 3-5 Arsenal
Model
Market

Four fixtures from the held-out seasons, including one both forecasts got wrong. Across all 16,888, the two differ by a median of 5 percentage points, and the model captures 92% of the bookmaker's edge over base rates.

The result

Accuracy against the baseline for each task

A football accuracy figure means nothing on its own, so each task is shown with the naive baseline for that same task and the bookmaker as the upper reference.

Out-of-sample across six held-out seasons. Each season is predicted by a model trained only on the seasons before it.
TaskBaselineThis modelBookmaker
Three-way, home, draw or away45.5%52.3%53.0%
Home win versus anything else54.5%64.2%65.3%
Decided matches, draws removed61.0%70.0%70.9%
Matches the model calls at over 80%, 4% of fixtures-85.6%-

Accuracy is the wrong headline. It keeps only the top guess, so 34/33/33 and 90/5/5 score the same when the favourite wins. Predict a home win every time and you match the base-rate forecast on accuracy, 45.9% against 45.6%. You also score a log loss of 18.7 against 1.07. The metric here is the ranked probability score. It uses the whole distribution and respects the ordering of outcomes.

Reliability diagram: the model's curve tracks the bookmaker's along the diagonal
When the model says 60%, it happens 60% of the time. Calibration tracks the bookmaker across the whole range. That is what makes these probabilities usable, not just ranked.

Feature contributions

Squad ratings carry the most signal

Features are grouped into blocks that switch off independently, so each block's contribution is measured rather than assumed.

Feature setRPSAccuracy
Head-to-head and promotion context0.212648.9%
Rolling form0.206550.3%
Dixon-Coles goal model0.203251.5%
Elo ratings0.202151.7%
FIFA ratings of the starting eleven0.201152.3%
Gradient boosting on all features0.199652.4%
Neural net with learned team embeddings0.199452.2%
The two averaged0.199152.3%
Bookmaker odds, run through a model0.198152.7%
Bookmaker odds, used directly0.196753.0%

Notes

Ranked probability score by feature set, against the bookmaker's line
Every configuration against the bookmaker's line. Nothing built from public data crosses it.

Two model classes

Boosting ranks better, embeddings calibrate better

Elo compresses a club to one number and Dixon-Coles to two. Neither can express a matchup, meaning the idea that one side is awkward for another beyond what their ratings predict. A network with a learned vector per club can. It sees both vectors, their difference, and their elementwise product.

ModelRPSAccuracyCalibration error
Gradient boosting0.199952.3%0.0100
Team-embedding network0.199552.2%0.0047
The two averaged0.199152.3%0.0063
Mean over five random seeds. The gradient boosting and embedding rows differ by more than four times their seed-to-seed spread.

The network is worse at picking winners and twice as well calibrated. Averaging the two at equal weight keeps both properties, and that is the best model here. The weight is fixed rather than tuned on purpose. Sweeping it found an optimum between 0.4 and 0.75, but picking from that range would be fitting a hyperparameter to the held-out seasons.

The embeddings do not encode club strength. Their magnitude correlates with home win rate at r = 0.01, and the largest belong to mid-table sides like Cagliari and Middlesbrough rather than Barcelona. That is expected. Elo, squad ratings and Dixon-Coles already supply strength as features, so what is left for an embedding to learn is club-specific residual structure.

The ceiling

Why the model stops improving

Before the embedding network, four separate attempts to close the gap produced no improvement.

ChangeRPSAccuracy
Elo, form, squad and Dixon-Coles0.199752.2%
Head-to-head, promotion, squad churn added0.199652.4%
Forty-configuration hyperparameter search0.199852.2%
Three models stacked through a meta-learner0.200052.3%
Probability calibration0.201351.7%

Four failed attempts do not prove a ceiling, so the remaining gap was broken down by context.

PartitionGap to the market, in RPS
Early, middle, late season+0.0036 · +0.0026 · +0.0044
Full starting eleven known, or not+0.0030 · +0.0031
Promoted team involved, or not+0.0031 · +0.0030
Market unsure, moderate, confident+0.0036 · +0.0027 · +0.0028
Across nine leagues+0.0005 to +0.0041

The gap is uniform across every partition. A modelling weakness would concentrate somewhere. A model blind to team news should lose most ground late in the season and on fixtures with unknown lineups. This one does not. A constant penalty of roughly three thousandths is what a fixed information disadvantage looks like. The missing information is injuries, suspensions, motivation and money flow, and none of it is in this database. In the Eredivisie the gap is +0.0005.

The ceiling is set by the sport rather than the model. Bookmakers reach about 53% on three-way football, and 55.8% in the most predictable league here. A substantially higher figure usually means draws were dropped, future information leaked in, or the baseline was wrong.

Out-of-sample RPS by held-out season, model against bookmaker
Six held-out seasons, so the result does not depend on a single fold.

Betting returns

The model does not make money

Fractional Kelly staking on the best price across six bookmakers, on out-of-sample forecasts only.

Minimum edge requiredBetsReturn95% interval
None21,871-1.47%-5.21 to +2.56
5%15,833-1.42%-5.34 to +2.85
10%11,326-1.59%-5.92 to +2.75
20%5,818-3.85%-11.22 to +4.06

The no-skill return is -2.83%. That is what a bettor loses to the margin when shopping the best of six books. Losing 1.4% means the model converts about half the margin into edge, but does not clear it. The full sweep is shown because picking one favourable threshold would make almost any strategy look profitable.

Return on investment by minimum required edge, with bootstrap confidence band
Above the no-skill line at every threshold, but below zero at all of them.

Data quality

Three problems in the source data

40% of the event feeds are empty

The shots-on-target column is non-null for 54.7% of matches. But 40.5% of those hold the stub <shoton />, which parses without error and returns zero. That records roughly 5,800 ordinary matches, averaging 1.54 home goals, as having no shots taken. Usable coverage is 32.6%.

The feeds carry little signal where they do exist

Measured against a control (the goal feed, which reconciles with the scoreline at r = 0.96), shot difference explains goal difference at only r = 0.13, corners at 0.07. Possession, at 0.24, is the only informative feed. This is why the event block is switchable rather than assumed useful.

A bug in the odds conversion

An earlier version of this analysis converted odds to probabilities by overwriting the columns in place:

df[home] = (1/df[home]) / (1/df[home] + 1/df[draw] + 1/df[away])
df[draw] = (1/df[draw]) / (1/df[home] + ...)   # home is a probability now

By the second line the home column holds about 0.45, not odds of 2.20, and its reciprocal re-enters the denominator. The triples sum to 0.573 instead of 1.0. The away probability drops to 0.021 against a correct 0.281. That affected twenty of the thirty odds features. A regression test now covers it.

Method

How it is evaluated

Python, LightGBM, scikit-learn and SciPy, with 38 tests, linting and CI. Every number above is reproducible from the command line.

uv run football-forecast fetch      # 313 MB, checksum-verified
uv run football-forecast build     # parse feeds, tune Elo, build features
uv run football-forecast backtest  # walk-forward evaluation
uv run football-forecast audit     # reproduce and re-score the earlier version