Machine-learning starter project for predicting football match outcomes: home win, draw, or away win.
The model is designed for modern football features such as xG, xA, PPDA, deep completions, progressive passes, shots, possession, market odds, and rolling team form. It is leakage-safe: for each match it only uses team statistics from matches before that date.
- StatsBomb Open Data: event-level JSON with xG, lineups, freeze frames, and some 360 data. Great for learning and feature engineering, but limited competition coverage and not ideal for current top-five-league weekly predictions.
- soccerdata docs: Python package that wraps sources such as ClubElo, FBref, Understat, Football-Data.co.uk, Sofascore, and WhoScored. Useful as a research layer, but source availability can change.
- Football-Data.co.uk: free historical results, basic match stats, and betting odds CSVs. Good baseline data, but it does not provide xG/xA/PPDA.
- Understat Python package: async access to Understat-style xG/xA data. Good for top European leagues, but scraping-dependent and should be used respectfully.
- StatsBomb shot dataset on Hugging Face: shot-level xG training set extracted from StatsBomb Open Data. Useful if you want to build your own xG model.
- TheStatsAPI: historical xG, npxG, xA and other underlying metrics. A practical option if you want reliable current-season predictions.
- Opta, StatsBomb paid API, Wyscout, SkillCorner, Sportmonks, API-Football, FootyStats: worth comparing if you need live fixtures, injuries, lineups, odds, and consistent PPDA/pressure data.
- A predictive analytics framework for forecasting soccer match outcomes using machine learning models: recent open-access EPL outcome-prediction paper using several ML models, including LightGBM and ensembles.
- Combining Machine Learning and Human Experts to Predict Match Outcomes in Football: useful benchmark framing for match prediction.
- Predicting Football Match Outcomes with Explainable Machine Learning and the Kelly Index: useful if you later evaluate predictions against betting markets.
The training code accepts a CSV with at least:
date,home_team,away_team,home_goals,away_goalsOptional modern features are automatically used when present:
home_xg,away_xg
home_xa,away_xa
home_ppda,away_ppda
home_deep_completions,away_deep_completions
home_progressive_passes,away_progressive_passes
home_passes_into_box,away_passes_into_box
home_shots,away_shots
home_shots_on_target,away_shots_on_target
home_possession,away_possession
odds_home,odds_draw,odds_awayLower PPDA usually means stronger pressing, so inspect PPDA feature importance carefully. Tree models can learn the direction, but linear models may benefit from explicit transforms later.
Create an environment:
python -m venv .venv
source .venv/bin/activate
pip install -e .Fetch baseline EPL results, shots, shots on target, and odds from Football-Data.co.uk:
python scripts/fetch_football_data.py --league EPL --start-season 2017 --end-season 2025Train:
python scripts/train.py --data data/raw/football_data.csvPredict upcoming fixtures:
python scripts/predict.py \
--history data/raw/football_data.csv \
--fixtures examples/upcoming_fixtures.csv \
--model-path models/outcome_model.joblib- Start with Football-Data.co.uk plus odds to verify the pipeline works.
- Add Understat match xG/xA by date/team for the top-five leagues.
- Add ClubElo ratings, rest days, travel distance, manager changes, injuries, suspensions, and lineup strength.
- Add PPDA/deep/progressive metrics from a reliable provider. If you cannot get PPDA, use proxy features: opponent passes allowed, defensive third actions, high turnovers, field tilt, or possession-adjusted defensive actions.
- Compare models by log loss and calibration, not only accuracy. Football has many draws and noisy low-scoring outcomes, so calibrated probabilities matter.
- Keep a strict chronological validation split. Random train/test splits leak future team strength into the past.
The starter model uses:
- rolling team form features over the previous 8 matches
- home-vs-away differences for each rolling metric
- attack-vs-defense matchup edges, for example home rolling xG for minus away rolling xG against
- an ensemble of logistic regression, random forest, and gradient boosting
- probability calibration
- time-series cross-validation
The most important comparisons are matchup features:
home_xg_edge = home rolling xG for - away rolling xG against
away_xg_edge = away rolling xG for - home rolling xG against
net_xg_edge = home_xg_edge - away_xg_edge
home_xa_edge = home rolling xA for - away rolling xA against
away_xa_edge = away rolling xA for - home rolling xA against
net_xa_edge = home_xa_edge - away_xa_edge
For PPDA, lower is better, so the comparison is inverted:
home_pressing_edge = away rolling PPDA - home rolling PPDA
For a stronger next version, add LightGBM or XGBoost, tune with Optuna, and evaluate against bookmaker implied probabilities.
See advanced_feature_schema.md for the full professional-style metric list supported by the feature builder.
On the real StatsBomb Premier League 2015/16 xG/xA match dataset:
Model: selected_logistic
Rolling window: 4 matches
Selected features: 35
Training rows: 350
Available features: 82
Time-series CV accuracy: 48.6%
Time-series CV log loss: 1.067
Reproduce with:
PYTHONPATH=src /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 scripts/train_model.py \
--data data/raw/statsbomb_xg_xa_matches.csv \
--model-out models/pro_feature_selected_logistic_model.joblib \
--rolling-window 4 \
--min-periods 3 \
--model-type selected_logisticBenchmark files:
data/processed/pre_match_benchmark.csv
data/processed/feature_selection_benchmark_tuned.csv
Use this when you want the system to keep taking recent completed league matches, train on everything before the fixture, and predict the next match.
Example: train from recent Premier League results and predict Arsenal vs Crystal Palace:
PYTHONPATH=src /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 scripts/predict_next_match.py \
--league EPL \
--seasons-back 6 \
--home Arsenal \
--away "Crystal Palace" \
--rolling-window 4 \
--min-periods 3 \
--model-type selected_logisticIf --date is omitted, the script predicts for the day after the latest completed match in the fetched history. To predict a specific fixture date:
PYTHONPATH=src /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 scripts/predict_next_match.py \
--home Arsenal \
--away "Crystal Palace" \
--date 2026-08-15For multiple upcoming matches, pass a CSV:
date,home_team,away_team
2026-08-15,Arsenal,Crystal Palace
2026-08-16,Liverpool,ChelseaPYTHONPATH=src /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 scripts/predict_next_match.py \
--fixtures fixtures.csv