Calibrated XGBoost — all 13 features with isotonic calibration (production model)
Part of the (Right! Luxury!) Lakehouse soccer analytics platform.
Model Description
Logistic Baseline
A logistic regression fitted on two geometric features (distance to goal center, shot angle) with isotonic calibration. Serves as an interpretable lower bound — any production model must beat this baseline.
Calibrated XGBoost
A gradient-boosted tree classifier (XGBClassifier) fitted on all 13 features, wrapped in scikit-learn's CalibratedClassifierCV with isotonic regression. The calibration step ensures that predicted probabilities are well-calibrated (a 0.15 xG prediction means ~15% of such shots are goals).
Serialization
Both models are serialized as JSON envelopes — no pickle is used (banned by project security policy):
XGBoost: Booster saved via save_raw("json"), base64-encoded in a JSON envelope alongside isotonic calibrator thresholds
Logistic: Coefficients, intercept, and classes stored as JSON arrays alongside isotonic calibrator thresholds
This makes model weights fully inspectable, version-controllable, and safe to load without arbitrary code execution.
Coverage includes the Premier League, La Liga, Serie A, Bundesliga, Ligue 1, Champions League, World Cup, and more. Both sources are unified to the StatsBomb 120×80 yard coordinate system at the dbt staging layer.
Features
All 13 features used by the XGBoost model:
Feature
Type
Description
distance_to_goal
Numeric
Euclidean distance from shot location to goal center (yards)
shot_angle
Numeric
Angle subtended by the goal from the shot location (radians)
Shot type (Open Play, Free Kick, Corner, Penalty, etc.)
play_pattern
Categorical
Build-up pattern (Regular Play, From Counter, From Corner, etc.)
Categorical features are one-hot encoded. The logistic baseline uses only distance_to_goal and shot_angle.
Coordinate System
All coordinates are in the StatsBomb system: 120 × 80 yards, with (0, 0) at the bottom-left corner of the pitch and the attacking goal at x = 120. Wyscout coordinates (0–100% scale) are converted at the dbt staging layer.
Hyperparameters
Parameter
Value
XGBoost n_estimators
100
XGBoost max_depth
3
XGBoost learning_rate
0.1
XGBoost eval_metric
logloss
Calibration method
Isotonic regression
Test split
20% (stratified by competition_id)
Random state
42
Evaluation Metrics
Both models are evaluated on a held-out test set using:
Metric
Description
Brier score
Mean squared error of probability estimates (lower is better)
Log loss
Logarithmic loss (lower is better)
ROC-AUC
Area under the ROC curve (higher is better)
Calibration error (ECE)
Expected calibration error across 10 uniform bins (lower is better)
Results (held-out test set, ~26K shots)
Model
ROC-AUC
Brier Score
Custom XGBoost (calibrated)
0.979
0.059
Custom Logistic (baseline)
0.761
0.082
StatsBomb xG Benchmark
The custom XGBoost model is benchmarked against StatsBomb's proprietary xG on the StatsBomb subset of the test set. Acceptance criterion: custom xG Brier score must be within 10% of StatsBomb xG Brier score.
How to Use
Quick Start
pip install huggingface_hub xgboost scikit-learn
python
1import json
2import base64
34from huggingface_hub import snapshot_download
5from xgboost import XGBClassifier
6import numpy as np
78# Download model9model_dir = snapshot_download("luxury-lakehouse/xg-model-statsbomb-wyscout")1011# Load XGBoost model from JSON envelope12withopen(f"{model_dir}/xgboost_model.json")as f:13 envelope = json.load(f)1415booster_raw = base64.b64decode(envelope["booster_b64"])16xgb = XGBClassifier()17xgb.load_model(bytearray(booster_raw))1819# Predict xG for a shot (requires one-hot encoded feature vector)20# See training notebook for full feature engineering pipeline
Note: The raw XGBoost booster above produces uncalibrated probabilities. For
production use, load the model with deserialize_xgboost_model (shown below) which
wraps the booster in scikit-learn's CalibratedClassifierCV with isotonic regression.
Without this calibration step, predicted xG values may be systematically over- or
under-confident.
Full Pipeline (with calibration)
For production use with isotonic calibration, use the deserialize_xgboost_model and deserialize_logistic_model functions from the analytics.xg_model module:
Shot valuation: Assign expected goal probabilities to shots for match analysis
Player evaluation: Aggregate xG for player performance assessment (goals vs. xG)
Tactical analysis: Identify high-quality shooting opportunities by location and context
Research: Reproducible xG baseline for sports analytics on open data
EU AI Act — Intended Use and Non-Use
This model is published for research and reproducibility purposes on public, open-licensed match data. It is not intended for, not validated for, and not supplied to any use that would fall within Annex III §4 (Employment, workers management and access to self-employment) of Regulation (EU) 2024/1689 — including recruitment or selection of natural persons, decisions affecting work-related contractual relationships, promotion, termination, task allocation based on individual traits, or the monitoring and evaluation of performance and behaviour of workers for employment decisions.
Any deployer who wishes to use this model for such a purpose is responsible for performing their own conformity assessment under Article 43, for drawing up the technical documentation required by Article 11 and Annex IV, for implementing the human oversight measures required by Article 14, for declaring accuracy metrics under Article 15, and for ensuring the data governance obligations of Article 10 are met. Note specifically that the training data contains no protected attributes and therefore cannot support the group-fairness audits required by Article 10(2)(g) without ingesting additional personal data.
See the AI_GOVERNANCE.md gap analysis in the source repository for the project's full risk classification, re-classification triggers, and governance posture.
Limitations
Open data only: Trained on publicly available StatsBomb and Wyscout data. Commercial datasets with richer features (freeze-frame defenders, goalkeeper position) would yield better models.
No defensive context: The model does not include freeze-frame features (number of defenders, goalkeeper position, blocking angle). These are available in StatsBomb 360 data but not universally across all matches.
Cross-source alignment: StatsBomb and Wyscout use different event taxonomies and coordinate systems. The dbt staging layer normalizes them, but subtle differences in shot classification may remain.
Calibration on open data: Isotonic calibration is fitted on the same data distribution. Applying to a substantially different league or era may require recalibration.
Citation
If you use this model, please cite the XGBoost method and this repository:
bibtex
1@inproceedings{chen2016xgboost,
2 title={XGBoost: A Scalable Tree Boosting System},
3 author={Chen, Tianqi and Guestrin, Carlos},
4 booktitle={Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining},
5 year={2016}
6}
bibtex
1@software{nielsen2026xgmodel,
2 title={Custom xG Model: Logistic Baseline + Calibrated XGBoost on StatsBomb and Wyscout Open Data},
3 author={Nielsen, Karsten Skytt},
4 year={2026},
5 url={https://github.com/karsten-s-nielsen/luxury-lakehouse}
6}
Model Files
xgboost_model.json -- calibrated XGBoost (JSON envelope, no pickle)
logistic_model.json -- calibrated logistic baseline (JSON envelope, no pickle)
metrics.json -- evaluation metrics and training configuration