How to Calculate Expected Goals (xG)

To calculate expected goals, collect historical shot data with locations and context, compute features such as distance and angle to goal, body part, and assist type, then fit a model, often logistic regression, that predicts whether each shot was scored. The model's predicted probability for a new shot is its xG. Validate with log loss and calibration on held-out shots.

Step 1: get shot data

You need event data with every shot's location, outcome, and context. Several providers publish free event data for selected competitions and seasons, often intended for research and education. Paid providers offer broader coverage. Check licence terms before using data commercially.

Step 2: define the target

Each shot is a row. The target is 1 if the shot was a goal and 0 otherwise. Decide early how to treat own goals, which are not shots, and penalties, which are usually modelled separately with a fixed historical conversion rate.

Step 3: compute distance and angle

Convert pitch coordinates to metres using the provider's pitch dimensions. Distance is the straight line from the shot location to the centre of the goal. Angle is the angle the goalmouth subtends from the shot location, computed from the positions of both posts. Wider angles and shorter distances mean higher scoring probabilities.

Step 4: add context features

Useful additions include body part, such as foot or head; assist type, such as through ball, cross, or cutback; play pattern, such as open play, set piece, or counterattack; whether the shot followed a dribble; and, if available, defender count between the shooter and goal and the goalkeeper's position.

Step 5: fit a model

Logistic regression is a common baseline: it outputs probabilities directly and its coefficients are interpretable. Gradient-boosted trees can capture non-linear effects and interactions, such as headers being much weaker at distance. Split data by match or season, not randomly by shot, to avoid leakage from the same game appearing in training and testing.

A minimal example in Python

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss, brier_score_loss

# shots: columns x, y (metres, goal centre at x=105, y=34), header, is_goal, season
def features(df):
    dx, dy = 105 - df.x, df.y - 34
    dist = np.hypot(dx, dy)
    a = np.arctan2(7.32 * dx, dx**2 + dy**2 - (7.32 / 2) ** 2)
    angle = np.where(a < 0, a + np.pi, a)
    return pd.DataFrame({"dist": dist, "angle": angle, "header": df.header})

train, test = shots[shots.season < 2025], shots[shots.season == 2025]
model = LogisticRegression(max_iter=1000).fit(features(train), train.is_goal)
p = model.predict_proba(features(test))[:, 1]
print(log_loss(test.is_goal, p), brier_score_loss(test.is_goal, p))

Step 6: validate

Goals are rare, so accuracy is misleading: predicting no goal for every shot is highly accurate and useless. Use log loss and the Brier score to measure probability quality, and plot calibration: group shots by predicted xG and compare with actual conversion rates. A well-calibrated model's 0.2 shots are scored about 20 percent of the time.

Step 7: aggregate

Sum shot xG by team and match to get team xG, and by player and season for player xG. Report non-penalty xG separately. Check totals against published figures for the same competition as a sanity check, expecting some differences because models vary.

Where the xG calculation goes wrong

Mixing coordinate systems from different providers, leaking information by including post-shot features such as where the shot ended up, splitting train and test randomly by shot, ignoring blocked shots or including them inconsistently, and over-fitting with many sparse features on small datasets.

Improving the model

Freeze-frame data showing player positions at the moment of the shot allows features such as defenders in the shooting lane and goalkeeper distance from goal. These improve accuracy considerably. Separate models for headers and footed shots, or interaction terms, also help.

Calculating xG from odds instead

Without shot data, you can estimate expected goals for upcoming matches from betting odds, by inferring goal expectations from match result and total goals prices. That approach answers a different question, forecasting goals before a match, rather than measuring chance quality after it.

Interpreting coefficients

In a logistic regression, negative coefficients on distance and positive coefficients on angle confirm that closer, more central shots are more likely to score. A negative header coefficient shows headers convert less often than footed shots from the same location. Checking that coefficients make football sense is a quick sanity test.

Next steps

Once the baseline works, add features one at a time and keep each only if it improves log loss and calibration on held-out seasons. Document the model version used for every published number.

This page describes how the metric is built and is not betting advice.

Frequently asked questions

What is the formula for expected goals?
There is no single formula. xG is the predicted probability from a model trained on historical shots. A simple version is a logistic regression using distance and angle to goal, where the probability equals the logistic function of a weighted sum of those features.
Which features matter most for xG?
Distance and angle to goal matter most in basic models. Body part, assist type, and play pattern add useful information. With tracking or freeze-frame data, defender positions and goalkeeper location improve accuracy significantly. Separate handling of headers and set pieces also improves most simple models.
How do I check if my xG model is good?
Use log loss or the Brier score on shots the model never saw, and check calibration by comparing predicted xG with actual conversion rates in bins. Compare season totals with published xG for the same matches as a sanity check.