
xG or "expected goals" grades a shot by how likely it was to go in, before the result is decided. A tap-in with an empty net is likely to have a very high xG like 0.90 : This tells us that the player is almost certainly going to score or if you took that shot 100 times we’d expect you to score 90 of them. Vice versa, a speculative shot from the back wall with three defenders covering the goal is maybe 0.02 : essentially, take that shot a hundred times and score 2 on average.
A single shot result tells you almost nothing on its own. Did the player read the play and execute, or get lucky? Did the defense pull off something special, mess up, or was the shot expected to be saved? Goals and shots can't tell those apart. xG can.
If a player takes shots worth 5.20 xG across a series and scores 7 goals, they're finishing above their chances - they're clinical, or on a hot streak. Similarly with 5.20 xG worth of shots and only scoring 2 goals from those shots? They left goals on the table. Same shot xG, but completely different stories.
For every shot taken, we want one number: out of all the shots in our dataset that was similar to this one - same relative position, same kind of trajectory, same kind of defensive setup - what fraction of those shots went in?
That's the number xG returns. A shot rated 0.60 means: shots that looked roughly like this one ended up as goals about 60% of the time across our training data. It's an expected probability, not a verdict.
We trained our model on hundreds of thousands of competitive Rocket League shots from real replays, and for every single one of those shots, we know whether it ended up being a goal. The model's job is to find the patterns: which kinds of situations consistently produce goals, and which ones don't.
No one actually told it "shots close to goal score more often" - it discovered that from evaluating the data, along with thousands of more subtle patterns it would take a person watching tens of thousands of replays to notice. The result is a model that can take a new shot it's never seen before, look at the situation, and give you the probability the shot will be a goal, based on every shot it learned from before.
At the precise moment a player makes contact with the ball we take a snapshot and evaluate. We use the contact moment because that's when the shot's quality is locked in and is the cleanest repeatable instance to look at. The model then asks: Where is every car in the arena? How fast is each one moving? Where is the ball heading? Where is the goal relative to the shot?
From this snapshot it works out things about the shot's quality: how much of the goal a defender is covering, how long the defense actually has to react, where each opponent is, where your teammates are, whether they're rotating in or away from the play as well as a bunch more. Some of these are things you'd immediately point out while watching a clip back - "the defender is miles out of position," "that angle is tough to score from." Others are much more subtle.
We deliberately don't feed it: the score, the clock, who's taking the shot, whether it's a grand final or a group stage game, LAN or Online. Those things of course affect whether a shot gets taken and the pressure around it but not how dangerous it was the instant it left the car. We keep the model focused purely on the shot itself.
Testing, testing and even more testing. (But we know its not perfect)
If the model says a group of shots are worth 0.40 xG each, then about 40% of them should actually go in. If it says 0.80, 80% should go in. Across our test data this lines up extremely well across the full range from 0 to 1. When the model says "this is a 60% chance," it really is a 60% chance.
As you can see from the image below the calibration of our model is generally good (not perfect but there will always be some level of variance). We evaluated the model over hundreds of thousands of pro Rocket League shots from our database to train this.

Data isn’t able to catch everything. So we ran the model over real RLCS replays we knew well, shot by shot, and manually checked them: do the xG values we spit out in the game match what you’re watching? Tap-ins should land in 0.70–0.99. Hopeful long-range shots should be much lower in the 0.05–0.15 range. Contested mid-range shots somewhere in the middle. When something didn't match - like an open goal coming out at 30%, we dug into it. This iteration loop is how we caught the largest issues and how we built confidence that the model agrees with what you see as a viewer, not just with the data alone.
xG is at its most useful over many series and a large amount of shots. Variance is real - a 0.80 xG shot still misses 20% of the time. Over a few hundred shots, though, the totals start to mean a lot.
Volume vs Quality example:
It is also important to note that xG can be inflated with lots of smaller xG opportunities in a single series i.e.
1.00 xG could come from:
or
These will both show as the same series xG but are completely different in shot quality and quantity. Again, over time and hundreds of shots the totals start to mean a lot.
A few things to keep in mind:
Our xG sits underneath a large part of the player rating calculation. Anything that touches "did this player generate quality scoring chances," "did the defense fail to limit those chances," or "did the team's positioning collapse" reads off the xG model for valuable context. Making the xG as accurate as possible means everything built on top of it in the rating gets sharper too.

xG is used in a wide variety of more traditional sports, here are some examples:
BLAST ApS., Hauser Plads 1, 3., 1127 Copenhagen