matchday / 2026
2026 · Machine learning · Product
Matchday
Football predictions built without using bookmaker odds.
Overview
Matchday turns ten seasons of match data into honest P(H/D/A) calls — no odds inputs, bookmaker as benchmark, and a live app with fixtures, manual predictor, title simulator, and disclosed metrics.

An honest comparison to the bookmaker
Most football models either leak odds into the features or hide behind accuracy. I wanted a model that could survive an honest comparison to the bookmaker — trained only on what is known before kickoff, evaluated on log-loss, and clear about where it still loses.
Designing the pre-match feature pipeline
A 23-feature fair-play pipeline: Elo plus margin Elo, 5/10-match rolling form, head-to-head, rest days, and goals/shots/corners averages — with leakage checks and pinned train/val/test windows so no future information ever contaminates a pre-match prediction.
Why XGBoost, and what it beat

A tuned XGBoost multiclass model beat both a logistic-regression baseline and the training-prior naive. The choice was empirical rather than fashionable: gradient boosting captured the non-linear interactions between form, strength, and rest that linear models flattened — verified on a locked 2024/25 test of 1,752 matches, not on validation peeking.
Evaluating against bookmaker probabilities
Locked 2024/25 test: naive 1.077 vs XGB 0.985 vs B365 bookmaker 0.965. Live 2026/27-partial test (146 matches): 0.989 log-loss at 50.7% accuracy. The model still loses to the bookmaker — published, not hidden. Taking no odds as input is a deliberate handicap: it keeps the comparison honest but concedes the single strongest pre-match signal that exists.
Serving predictions with FastAPI and Next.js
Shipped behind a FastAPI backend (predict, fixtures, evaluate, simulation) with a Next.js frontend: weekly fixtures, name-your-tie predictor, a 2,000-season Monte Carlo title simulator, Dixon-Coles scorelines stamped conditional on XGB, and a model page that publishes log-loss instead of hiding it. Train/serve parity is enforced by a point-in-time test, 51 pytest gates guard regressions, and the full loop — refresh, retrain, reload, simulate — runs from a Makefile in production.



Structure
- 01
Ten seasons of matches
- 02
23 pre-match features
- 03
XGBoost
- 04
FastAPI
- 05
Next.js product
The result
Locked 2024/25 test (1,752 matches): naive 1.077 → XGB 0.985 vs B365 bookmaker 0.965. Live 2026/27-partial test (146 matches): 0.989 log-loss at 50.7% accuracy. Train/serve parity is enforced by a point-in-time test, 51 pytest gates guard regressions, and the full loop — refresh, retrain, reload, simulate — runs from a Makefile in production on Pxxl + Vercel.
- 0.985 log-loss
- 1,752-match locked test
- 5 leagues live
If I built it again
The honest benchmark changed the project from a model into a product: losing to the bookmaker publicly is what makes the rest of the numbers believable. Next iteration is calibration by league — the residual errors are not evenly distributed, and the model knows it.
Further reading + source