xJawn NHL expected goals
Today's puzzle Remember Some Guys — name the skater from the shape of his career. Three clues, one guess a day. Play →

← Blog

The talent-blind baseline

The model that scores every shot in the tables — its structure, the reasoning behind it, and its honest scorecard.

Expected goals (xG) is the probability that a given shot attempt becomes a goal, estimated from where and how it was taken — distance, angle, shot type, whether it followed a rebound or a rush, the strength state, and more. The model is deliberately talent-blind: it scores the chance for a league-average shooter against a league-average goalie, so that a player's finishing (goals minus xG) measures their shooting talent against that neutral bar.

That choice is the whole design. A model that already knows Auston Matthews is shooting cannot tell you how good Auston Matthews is at shooting — the talent gets absorbed into the prediction and disappears from the residual. Keeping identity out is what keeps the residual meaningful, and it is still true of the model scoring this site today.

Updated 20 July 2026 — which model scores the tables. Until now it was the v1 model described on this page. The replacement was the identity-blind structural half of Model v2's joint fit: the same talent-blind contract, but with the structure re-estimated net of player talent instead of absorbing it. That matters for exactly the reason this page is about — a baseline fit without identity quietly soaks up whatever talent correlates with its features (skilled shooters over-represented in good locations inflate the location terms, which then under-credit those shooters), and the joint fit measures the two in tension so it does not. The scoreboard is still talent-blind; v2's player-aware composite remains out of the tables, because you cannot measure a shooter against a yardstick that already knows who they are.

Updated 25 July 2026 — the cascade came back. The canonical model is now nested_structure__nested_structure_xg__d2ec2662, the nested structural export of the same joint fit: three chained questions — does the attempt get through?is it on net?does it go in? — with each stage's structure re-estimated net of that stage's own people terms (shooters, goalies, and the blockers in front of them), then composed with every player term left out. Same talent-blind contract, better chassis: each attempt is scored by the stages that actually decide it, and the per-stage breakdown returns to the charts below. The fitted artifact was rebuilt once since (August 2026) on corrected blocked-shot origin imputation — a data fix, not a design change. Model v3 is the full account: why the cascade won, and the decomposition that inverted the reason it was built. Everything below this line — the training discipline, the segregated regimes, the held-out scorecard — describes the model now in use.

How it is trained

  • Same-era, grouped by game. Evaluation uses K-fold cross-validation split by whole game (a shot and its rebound never straddle the split), with train and test drawn from the same era — the game changes over time, so we never train on old seasons to score new ones.
  • Two prediction columns, never conflated. Out-of-fold predictions are the honest source for performance; the in-sample final model produces the canonical xg used for aggregation (uniform scoring across every shot). The tables on this site are the in-sample canonical column — labeled as such, not a held-out score.
  • Segregated regimes. Empty-net and penalty shots are scored by dedicated models, so their potent goal environments never contaminate defended-net predictions.

Honest limitations

  • xG is a model of chance quality, not a verdict — it is calibrated in aggregate, but any single shot's number carries uncertainty.
  • Shooter/goalie identity is not in this model (by design — that is what makes finishing measurable). The talent-aware variant is its own post.
  • Coordinates are arena-adjusted but imperfect; blocked-shot origins are imputed.

This build

  • Model id: nested_structure__nested_structure_xg__d2ec2662 (nested_structure)
  • Season shown: 2025-26
  • Displayed xg: in-sample canonical column (aggregation)

How well does the xG model score shots?

Every number in this section is out-of-fold — each shot scored by a model that never saw its game in training — over 2,344,674 shot attempts (112,235 goals) across the full corpus. This is the honest held-out report, not the in-sample column the tables use.

MetricValueReading
Danger AUC (goal vs save, on-net)0.773 ranks real scoring chances; immune to easy blocked-shot separability
All-shots AUC0.826 includes blocks/misses — flattering, shown for context
Brier score0.04113 mean squared error of the probabilities (goal rate 4.8%)
Log-loss0.15656 the training objective, held-out

Calibration — when we say 10%, does it happen 10% of the time?

Shots grouped into ten equal-count bins by predicted xG; each point compares the bin's mean prediction to its observed goal rate. On the dotted line = perfectly calibrated. Log scales, so every decade from long-range point shots to point-blank chances is visible.

Stability by season

Discrimination (AUC) and calibration error (Brier) per season — one model, sixteen seasons, no season-specific tuning.

What the model looks at

Gain importance per cascade stage — how much each feature contributes to the splits that decide blocked?on net?goal? Location dominates (distance, angle, x/y); sequence context (rebounds, rush) earns its keep in the goal stage.

The game engine — how well does it call games?

The matchup engine (matchup__5184e7b0) predicts game winners from the xG pipeline. How it works, in one breath: every team carries ratings — opponent-adjusted, league-centered expected-goals rates per 60 (5v5 attack & defense, special teams, discipline, finishing & saving residuals), updated game by game with a 40-game half-life and shrunk ⅓ toward average each summer. A Poisson regression turns tonight's two rating vectors + rest/schedule features into expected goals for each side (λ), anchored to an as-of league scoring-environment offset so the level tracks the era rather than lagging it; the finishing-and-saving signal is reinforced within the season by the player-identity model's own monthly-refreshed conversion ratings. A score grid over all possible finals converts the λs into win, tie, and total-goals probabilities — with a fitted Dixon–Coles adjustment correcting the independent-Poisson grid's undercount of ties — and an OT/shootout layer settles the tie mass.

Every number below is from the walk-forward backtest: to score season N, the engine is fit only on seasons before N — it never peeks. Seasons through 2018-19 were the development window (used for tuning); the shaded holdout was never used to make a modeling choice. The Games page tracker is the third, strictest tier: predictions published before puck drop this season, scored as issued.

Season by season vs the challengers

Log-loss per season (lower is better) for the shipped Poisson engine and its three comparators: a logistic classifier on the same ratings, goals-only Elo (no xG — "did the pipeline add anything?"), and the home-rate constant (always predict the league home-win rate). The first seasons train on very little history; the engine finds its feet as the ledger grows.

Engine (holdout, 8,510 games) log-lossBrieraccuracy
poisson0.663 0.23659.4%
logistic0.662 0.23559.3%
elo0.670 0.23958.6%
constant0.691 0.24953.6%

Yes — the plain logistic edges the Poisson on winner log-loss (~0.001). It optimizes exactly this one number; the generative Poisson is kept as the headline because only it produces the score grid behind expected goals, totals, and the matchup maps. Both clearly beat Elo: the xG pipeline earns its keep. Interestingly, when Poisson and Elo pick different winners they split those games nearly evenly — the engine's edge is probability accuracy, not contrarian picks.

Trust the number — the confidence record

Holdout games grouped by how confident the engine said it was about the favorite. A calibrated forecaster's 60–65% games should land ~62% — and they do.

Stated confidencerecord expectedactual
50–55%1592–1439 52.4%52.5%
55–60%1427–1048 57.4%57.7%
60–65%1036–600 62.3%63.3%
65–70%607–263 67.3%69.8%
70%+396–102 73.9%79.5%

Goals, not just winners — the totals check

The engine's λs claim to be expected goals, so hold them to it. The curve rises decile by decile: when the engine predicts more goals, teams score more goals — the ranking signal is clean. The level holds too, now: an earlier generation of this engine ran a couple of tenths of a goal per game cold, because a model trained only on past seasons lags the NHL's rising scoring environment. The as-of environment offset closed it — the holdout gap is down to 0.03 goals per game.

The same story at the game level: predicted vs actual distribution of total goals (6.15 predicted vs 6.18 actual per game). The matchup-specific totals beat a climatology baseline on over/under 5.5 (log-loss 0.681 vs 0.697). The two fixes an earlier version of this page called "queued" — the scoring-environment lag and the independent-Poisson tie undercount — have both shipped: the environment offset above, and the Dixon–Coles adjustment in the score grid.

Seen something wrong? Say so — corrections get folded in and credited: Bluesky X Reddit Email