What xG actually measures

Every shot happens in a context: distance to goal, angle, body part, pressure, whether it came from a set piece, whether the keeper was set. An expected goals model turns those features into a probability — the chance that an average player scores from that exact situation.

It is not a prediction of the match. It is a measure of chance quality: if a team accumulates 2.3 xG from twenty shots, the model is saying "that pile of chances, repeated forever, is worth about 2.3 goals".

What it is good for

  • Separating process from results. Over five matches, goals are noisy; xG stabilises much faster. A team that keeps creating 1.8 xG and scoring 0.6 is either unlucky or has a finishing problem — both worth investigating.
  • Comparing shot profiles. Two teams can both take 14 shots: one from inside the box with a central angle, the other from 25 metres. xG makes that difference visible.
  • Talking about defence without the ball. Low xG conceded is usually a better signal of a solid structure than "few shots conceded".

Where it breaks

  • Different providers, different numbers. StatsBomb, Opta and Understat all publish xG, and they disagree — sometimes by 15–20% on the same shot. Different data collection, different features, different training sets.
  • It ignores the player. A model gives the same value to the same shot regardless of who is shooting. Post-shot models (which add where in the goal the ball ended up) correct part of this, but pre-shot xG cannot.
  • Single matches prove nothing. A 0.4 xG difference in one game is well inside the noise. xG is a tool for samples, not for headlines.

How to use it responsibly

  1. Treat it as a baseline, not a verdict. The interesting question is not "who deserved to win" but "what does the gap tell us".
  2. Always pair it with volume. 0.9 xG from 5 shots and 0.9 xG from 20 shots are different stories.
  3. Look at the trend, not the value. Rolling five-match averages beat single-match numbers every time.

Build your own in 20 lines

The core of a simple xG model is a logistic regression over a handful of features: distance, angle, and whether the shot was a header or a penalty.

import pandas as pd
from sklearn.linear_model import LogisticRegression

def distance(x, y):
    return ((120 - x) ** 2 + (40 - y) ** 2) ** 0.5

def angle(x, y):
    left = abs(y - 36)
    right = abs(y - 44)
    import math
    return math.atan2(7.32 * (120 - x), (120 - x) ** 2 + (36 - y) * (44 - y))

shots["distance"] = [distance(x, y) for x, y in zip(shots.x, shots.y)]
shots["angle"] = [angle(x, y) for x, y in zip(shots.x, shots.y)]

model = LogisticRegression()
model.fit(shots[["distance", "angle", "is_header"]], shots["goal"])
shots["xG"] = model.predict_proba(shots[["distance", "angle", "is_header"]])[:, 1]

That model will not beat a provider's, but it will teach you more about xG than any dashboard: every feature you add, every bias you introduce, becomes visible.

TL;DR

xG is a quality-of-chance metric that stabilises fast, compares like with like, and lies confidently in small samples. Use it as a lens, not a judge.