How we build expected goals
Every unblocked shot in the NHL isn’t worth the same — a tip from the slot and a point shot from the boards are both “shots,” but only one is a real chance. Expected goals (xG) puts a number on that difference, and it’s the layer under most of what RinkIQ shows. Here’s exactly how ours is built, and how it relates to the Corsi and Fenwick counts you’ll see elsewhere on the site. It’s all homegrown from the NHL play-by-play — no third-party stats.
First, counting shots: Corsi and Fenwick
Both are simple shot-attempt counts pulled straight from the play-by-play — they measure quantity, not quality:
- Corsi — every shot attempt: goals, shots on goal, missed shots and blocked shots. The broadest measure of who’s driving play.
- Fenwick — the same, but unblocked (drop blocked shots). A blocked shot never reaches the net, so Fenwick is a cleaner read on dangerous volume.
We report each as a share — a team’s attempts as a percentage of all attempts in its games (Corsi% / Fenwick%) — so 50% is even play and above 50% means a team out-attempts its opponents.
Then, quality: expected goals
xG takes those same unblocked (Fenwick) attempts and asks a better question — not “how many?” but “how likely was each to score?” Every shot gets a probability between 0 and 1 from where and how it was taken:
- Where: shot location, distance to the net, and angle.
- How: shot type (wrist, slap, tip, …) and the ice zone.
A shot from the slot might score 20% of the time; one from the point, 2%. Sum a team’s shot probabilities and you get xGF (expected goals for); the opponents’ shots in the same games give xGA. So Fenwick is the quantity of unblocked chances, and xG is the same chances weighted by quality.
The model
The probabilities come from a gradient-boosted decision-tree model (XGBoost) trained on hundreds of thousands of shots, then isotonic-calibrated so the numbers mean what they say — of all the shots we rate a 0.10, about one in ten really does go in. It’s fit expanding-window per season (each season is scored by a model trained only on earlier seasons), so it never sees the future and stays honest as an input to the win model.
The model trains on regular-season shots only — the convention public xG sources use, and playoff hockey is a different base rate. Playoff shots are still scored, so the team and player totals you see are regular season, with any playoff production reported separately rather than folded in.
AUC measures discrimination — how well the model separates goals from non-goals (0.5 is a coin flip, 1.0 is perfect). The calibration gate checks honesty: per season, |Σ xG − Σ goals| / Σ goals must stay within 3% on the seasons that feed live predictions.
| Predicted xG | Shots | Predicted | Actual |
|---|---|---|---|
| -0.00-0.02 | 80,311 | 1.6% | 1.4% |
| 0.02-0.04 | 96,592 | 2.9% | 2.9% |
| 0.04-0.08 | 123,463 | 5.8% | 5.8% |
| 0.08-0.15 | 93,332 | 11.0% | 11.2% |
| 0.15-0.25 | 58,160 | 18.7% | 18.0% |
| 0.25-0.40 | 3,069 | 30.0% | 25.9% |
| 0.40-1.00 | 763 | 53.4% | 46.1% |
Reliability by bucket: in each predicted-xG band, how often shots actually scored. Close columns mean well-calibrated shot quality.
From shots to teams and players
The same per-shot xG rolls up two ways. Summed per team per game it gives the xGF / xGA on the team pages and the Shot Quality tab. Summed per shooter it gives individual xG (ixG) — how many goals a player’s shot selection was worth — and comparing that to their actual goals gives goals above expected, the finishing signal behind our player value and injury-impact scoring.
What it doesn’t know yet
Our xG reads location and shot type, but not the pre-shot context that makes the very best chances — rebounds, cross-ice passes, odd-man rushes, or whether it was a power play. So the highest-danger shots are still rated a touch low, and we say so rather than hide it. Adding that context is the next step; the model is honest about its current range in the meantime.
