PSxG Model β€” Post-Shot Expected Goals for Goalkeeper Evaluation

Logistic regression model estimating the probability that an on-target shot becomes a goal, from its projected goal-line crossing + shot geometry (4 features). Trained on 32,698 StatsBomb on-target shots (29.9% goal rate); out-of-sample AUC 0.818, Brier 0.153 (GroupKFold by match). Model version v2-ontarget. Used to compute goals prevented (PSxG βˆ’ actual goals conceded) as the primary shot-stopping pillar of the goalkeeper evaluation framework.

Part of the (Right! Luxury!) Lakehouse soccer analytics platform.

Model Description

PSxG (Post-Shot Expected Goals) conditions on the observed shot destination β€” where the ball was headed when it crossed the goalline plane β€” rather than the shooter's location. This gives a fairer evaluation of the goalkeeper: a shot headed for the top corner is harder to save than one at chest height in the center, regardless of where on the pitch the shot was taken.

Features

The model scores a 4-feature vector (analytics.goalkeeper.PSXG_FEATURE_NAMES), built identically for both modalities through one shared port:

Feature Description
goalmouth_dist_from_centre min(|y_norm βˆ’ 0.5|, 0.5) β€” distance of the goal-line crossing from goal centre. The goal-vs-save signal is a symmetric arch (near-post hardest, dead-centre saveable), which a logistic on a linear y cannot represent.
goalmouth_z Crossing height as a fraction of goal width (z / 7.32 m).
distance_to_goal_m Shot origin β†’ goal centre, metres (StatsBomb yards harmonised Γ—0.9144; tracking already metres).
shot_angle Angle subtended at the posts, radians.

Goal-line crossing (the key fix β€” ADR-060): StatsBomb end_location is the goal-line crossing only for goals (end_location_x=120); for saves it is the save point (~x=118) or deflected end position, so raw end_location_y/z is the wrong coordinate for ~67% of shots (the prior placement-only model was near-random, OOS AUC 0.525). The crossing is recovered by projecting the trajectory location β†’ end_location onto the goal plane (x=120). The tracking modality uses its measured ball crossing (TF-48 shot_crossing_y/z) directly β€” no projection. distance_to_goal/shot_angle are computed identically across modalities (replicating the dbt macros in SPADL coords), so one Champion scores both.

Architecture

Component Detail
Model class sklearn.linear_model.LogisticRegression
Solver lbfgs
Max iterations 1000
Feature scaling StandardScaler (fit on training set, serialized with model)
Serialization JSON (coefficients + intercept + scaler params β€” zero pickle surface)

Logistic regression on four low-dimensional features keeps the model transparent and auditable; the surface is near-linear in log-odds space, so no tree-based or neural model is needed. Richer covariates (goalkeeper position, shot speed) are deliberately deferred (ADR-060 / spec Β§8/E).

Model Output

Field Type Description
psxg float64 Probability that the shot becomes a goal (0 = certain save, 1 = certain goal)

Goals prevented = sum(PSxG over shots faced) βˆ’ actual goals conceded. Positive = better than average, negative = worse than average.

Training Data

Source On-Target Shots License
StatsBomb Open Data ~15K CC-BY 4.0

Training data is published as luxury-lakehouse/statsbomb-shots-on-target on HF Hub.

Only true on-target shots are included: shot_outcome IN ('Goal', 'Saved', 'Post', 'Saved to Post') (Off T, Blocked, Wayward, and Saved Off Target are excluded as off-target). Post / Saved to Post are kept because the tracking shot_on_target_derived geometry counts post/bar strikes as on-target (ball-radius tolerance), so both modalities share one definition. A prior revision filtered on end_location_z IS NOT NULL, which silently included ~46% off-target Off T shots and depressed the goal rate to 15.9%; the corrected population is 32,698 shots at a 29.9% goal rate. On this population the placement-only 2-feature model was near-random (OOS AUC 0.525 β€” raw end_location is the save-point for non-goals); the projected-crossing 4-feature model (v2-ontarget, ADR-060) scores OOS AUC 0.818, Brier 0.153 (GroupKFold by match_key, n=32,698 β€” no same-match leakage).

How to Use

Load and Run Inference

import json
import numpy as np
from huggingface_hub import hf_hub_download

# Download model coefficients
config_path = hf_hub_download("luxury-lakehouse/psxg-model", "psxg_model.json")

with open(config_path) as f:
    model = json.load(f)

# Extract logistic regression parameters (feature order in model["feature_names"])
coef = np.array(model["coefficients"])  # shape (4,)
intercept = np.array(model["intercept"])  # shape (1,)
scaler_mean = np.array(model["scaler_mean"])
scaler_scale = np.array(model["scaler_scale"])

def predict_psxg(features: np.ndarray) -> float:
    """features = [goalmouth_dist_from_centre, goalmouth_z, distance_to_goal_m, shot_angle].
    Build them with analytics.goalkeeper.build_psxg_features_{statsbomb,tracking}
    (StatsBomb projects to the goal line; tracking uses the measured crossing)."""
    x_scaled = (np.asarray(features, dtype=float) - scaler_mean) / scaler_scale
    log_odds = x_scaled @ coef.T + intercept
    return float(1.0 / (1.0 + np.exp(-log_odds)))

# Example: near-post crossing (0.45 from centre), mid-height (0.30), 12 m out, angle 0.40 rad
psxg = predict_psxg(np.array([0.45, 0.30, 12.0, 0.40]))
print(f"PSxG: {psxg:.3f}")

Load Pre-Computed Predictions

from datasets import load_dataset

ds = load_dataset("luxury-lakehouse/psxg-predictions")
df = ds["train"].to_pandas()

# Goals prevented per keeper (positive = better than average)
gp = df.groupby("player_id").agg(
    psxg_sum=("psxg", "sum"),
    goals_conceded=("is_goal", "sum"),
).assign(goals_prevented=lambda d: d["psxg_sum"] - d["goals_conceded"])
print(gp.sort_values("goals_prevented", ascending=False).head())

Intended Use

  • Goalkeeper evaluation: Primary shot-stopping metric for fct_goalkeeper_stats
  • Benchmarking: Compare GK shot-stopping performance against PSxG expectations across seasons
  • Research: Transparent four-feature PSxG baseline for goalkeeper analytics research
  • Downstream: Input to the composite goalkeeper score (Lamberts 2025 framework)

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. In particular, "goals prevented" (PSxG βˆ’ actual goals conceded) is not a fit-for-purpose metric for goalkeeper contract decisions without full Article 14 human oversight by the deploying organisation.

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

  • Four features: projected goalmouth crossing (distance-from-centre + height) + shot distance + angle. Shot speed, goalkeeper position, deflections, and defensive pressure are not captured (deferred, ADR-060 / spec Β§8/E).
  • Open data only: Trained on StatsBomb open data (~15K shots). Commercial datasets with larger coverage may yield different calibration.
  • StatsBomb coordinate system: end_location_z is available only in StatsBomb 360 data. Models trained on providers without z-coordinate data will require a 2D fallback.
  • No freeze-frame context: PSxG does not model the goalkeeper's starting position or movement. See the xG v2 model for freeze-frame-conditioned expected goals.

Model Files

psxg_model.json      -- LogisticRegression coefficients, intercept, and scaler params (JSON)

Model parameters use JSON serialization β€” a text-only format with zero pickle surface and no code execution capability. Predictions are delivered as Parquet (non-executable).

Citation

@article{butcher2025xgot,
  title={An Expected Goals On Target (xGOT) Model},
  author={Butcher, J. and others},
  journal={Big Data and Cognitive Computing},
  volume={9},
  number={3},
  pages={64},
  year={2025},
  publisher={MDPI},
  url={https://www.mdpi.com/2504-2289/9/3/64}
}
@software{nielsen2026psxg,
  title={PSxG Model: Post-Shot Expected Goals for Goalkeeper Evaluation},
  author={Nielsen, Karsten Skyt},
  year={2026},
  url={https://github.com/karsten-s-nielsen/luxury-lakehouse}
}

Companion Resources

Resource Description
On-Target Shot Data Training data: ~15K StatsBomb on-target shots with goalmouth coordinates
PSxG Predictions Per-shot PSxG predictions with player and match identifiers
xG Shot Data Full shot dataset (pre-shot xG features, StatsBomb + Wyscout)
xG Model v2 Deep Sets xG with freeze-frame context and MC Dropout uncertainty

Demo

Try the interactive Soccer Analytics App β€” explore goalkeeper shot-stopping metrics on the Defensive Impact page.

Explore interactively: Soccer Analytics App

More Information

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Dataset used to train luxury-lakehouse/psxg-model