← research

How my expected goals model works

A shot is not a shot. This is how I put a number on the difference.

September 20, 2026 · Keith Wilcox

Shots on goal is the stat everyone quotes and it's close to useless on its own. A wrist shot from the top of the crease and a point shot through four bodies both count as one. Everybody watching the game knows they aren't the same thing, and the scoresheet doesn't care.

Expected goals is the fix. Every unblocked shot gets a number between 0 and 1: the share of times a shot like that one goes in, based on every other shot like it in my database. Add them up and you get what a player or a team should have scored, which is a much better description of a night than the final score.

It's the model everything else on this site runs on. The shot maps size their dots by it. The goalie cards use it to work out saves above expected. The finishing card compares a shooter's goals to it.

So here's what goes into it, how well it works, and the parts it gets wrong.

What it's looking at

Every unblocked shot attempt since 2023-24: shots on goal, and shots that missed the net. Blocked shots are left out on purpose, because the NHL records a block where the blocker was standing, not where the shot came from, and feeding the model a location that isn't the shot location is worse than having no row at all.

Shootout attempts and penalty shots are out too. They're not the same game.

Empty net shots get a number, but they don't train the model and they don't count toward a player's totals. An empty net is a different sport.

season game_type attempts empty_net goals total_xg
0 2023-24 regular 114793 780 8070 7498
1 2023-24 playoffs 7689 63 503 476
2 2024-25 regular 112207 933 7894 7321
3 2024-25 playoffs 7717 69 534 491
4 2025-26 regular 112058 992 8080 7802
5 2025-26 playoffs 7261 76 486 486

Each of those rows carries seven things the model is allowed to know:

  • How far the shot was from the net and the angle to it
  • Shot type: wrist, snap, slap, backhand, tip-in, deflection, wrap-around
  • Strength: 5v5, power play, penalty kill, and so on
  • Rebound: a shot on goal from the same team within three seconds of the last one. Only saves make rebounds, which sounds obvious and cost me a couple of hours later in this piece.
  • Rush: the shot came within four seconds of a turnover or of play being in the neutral or defensive zone, so the defense was still catching up
  • Home or away, and which period

That's it. No player names. The model has no idea who shot the puck, which is deliberate, because that's what makes it possible to measure finishing: if Draisaitl's own skill were baked into his expected goals, you could never catch him beating it.

What distance alone does

Start with the simplest thing. Wrist shots at 5v5 that aren't rebounds, grouped by distance, with what the model says next to what actually happened.

Chart: What distance alone does
feet shots model actual
0 5 478 0.296 0.303
1 10 6914 0.136 0.145
2 15 7879 0.115 0.122
3 20 7802 0.096 0.099
4 25 9134 0.083 0.088
5 30 11293 0.069 0.073
6 35 13383 0.053 0.054
7 40 13709 0.037 0.035
8 45 12189 0.024 0.022
9 50 9718 0.018 0.016
10 55 8435 0.015 0.011
11 60 9190 0.015 0.013
12 65 8156 0.012 0.010
13 70 5067 0.010 0.009
14 75 8507 0.006 0.002

From five feet it's about a 30% shot. From 35 feet it's 5%. From the blue line it's 1%, which is why "he's got to get pucks to the net" is only good advice some of the time.

The model line and the actual line sit on top of each other, which is the first sign it isn't making things up.

Where the goals come from

Distance isn't the whole story though. Here's every shot location on the ice, colored by what the model thinks a shot from there is worth.

Chart: Where the goals come from

That's the slot, and it's the whole reason teams talk about it the way they do. Everything in tight and straight on is dark; the further you go toward the boards, the faster it drops off, because you're shooting at less net.

Does it actually work?

The model that writes the numbers onto this site is fit on everything I have, all three seasons. That's the right way to use it and the wrong way to test it. Grade a model on the shots it learned from and it'll look great, and you'll have learned nothing.

So here's the honest version, and it's what the numbers below come from: train on 2023-24 and 2024-25 only, then go predict 2025-26 cold. Same features, same settings, one less season of practice.

Three ways to judge what comes back:

  • Can it rank shots? That's AUC. Pick a goal and a non-goal at random, and AUC is how often the model gives the goal a higher number. 0.5 is a coin flip.
  • Is it confident when it should be? That's log loss, where lower is better. The bar to beat is what you'd get by giving every shot the league average.
  • Do the numbers mean what they say? If the model calls a batch of shots 10%, about 10% of them should go in.
trained on 240,561 shots from 2023-25
tested on  118,251 shots from 2025-26 it had never seen (8,019 goals, 6.8%)

AUC          0.747   (0.5 = guessing)
log loss     0.2226
  vs league-average guess     0.2480
  vs simpler logistic model   0.2251

total expected goals 8,410 vs 8,019 actual (+4.9%)
Chart: Does it actually work
shots model actual
bucket
0 11928 0.0081 0.0058
1 11846 0.0143 0.0113
2 11743 0.0208 0.0213
3 11785 0.0303 0.0299
4 11844 0.0445 0.0410
5 11805 0.0624 0.0605
6 11828 0.0820 0.0823
7 11823 0.1055 0.1077
8 11830 0.1371 0.1325
9 11819 0.2064 0.1861

0.747 AUC. Hand it a goal and a non-goal and it picks out the goal about three times in four. For a single shot that's modest and it should be — one shot of hockey is mostly luck, and a model claiming otherwise would be lying. It's over a season that this turns into something.

Log loss 0.223 against 0.248 for guessing the league average every time. The plain logistic version gets 0.225, so the fancier model wins, but not by much. Most of what there is to know is distance and angle.

The total came in 4.9% high, 8,410 expected against 8,019 actual. That's the part I'd most like to fix, and the calibration chart shows where it comes from. The middle of that chart is excellent — when the model says 8.2%, 8.2% of them go in — but look at the last dot. On the best chances in hockey it says 20.6% and 18.6% go in. It's a little too excited about the shots it likes most, which is also why a few of the season totals below run rich.

Everywhere a normal shot lives, though, the dots sit on the line, and that's the property that lets you add these up in the first place.

Does it pass the eye test?

Statistics are one thing. I also want the model to agree with what I see when I watch a game.

average expected goals:
wrist shot from the slot, 5v5              0.133   (4,840 shots)
point slap shot, 5v5                       0.019   (4,354 shots)

wrist from 20-35 ft, even strength         0.067   (9,670 shots)
same shot on the power play                0.087   (1,695 shots)

rebounds                                   0.120   (7,889 shots)
everything else                            0.066   (110,362 shots)

rebounds actually went in 11.0% of the time, everything else 6.5%

from the same range (within 20 ft, 5v5): rebound 0.127 vs other 0.112  (1.13x)

A slot wrist shot is worth about seven times a point slap shot. The same shot is worth more on the power play than at even strength. Rebounds are worth nearly double, and they really do go in nearly twice as often. None of that is surprising, which is the point — if the model disagreed with all of that, the model would be wrong.

The rebound thing I got wrong

Worth telling, because it's the kind of mistake that doesn't announce itself.

When I first built this, the rebound flag barely moved anything, which doesn't match a single game I've ever watched. I'd defined a rebound as any attempt within three seconds of the previous attempt by the same team. Sounds fine. It isn't. Under that definition a shot three seconds after a missed shot or a blocked shot counted as a rebound too, and neither of those creates one. Nobody is banging home the return off a puck that sailed into the glass.

So the rebound bucket was packed with shots that weren't rebounds, and the effect got watered down by all of them.

The fix was one clause: the previous attempt has to have been on goal, because only a save makes a rebound. There was a second, sneakier version of the same mistake right behind it — I was grabbing the previous row, so a hit or a faceoff logged in between hid the actual last shot. That one needed a window function instead.

Even fixed, the effect is smaller than the eye test says. The 1.8x above is mostly geography: rebounds happen in front of the net, and being in front of the net is already worth a lot. Hold the range constant — inside 20 feet, 5v5 — and a rebound is 0.127 against 0.112, about 13% more. I think that's the one-second clock. The NHL timestamps to whole seconds, so a genuine scramble at the crease and a shot three full seconds after a save look identical to me, and the really dangerous ones get averaged in with the ones where the goalie already got set.

Nothing about the output ever looked broken. It just quietly disagreed with hockey, and I only caught it by going looking for things that should be obviously true. That's a step I run on every model now.

Who it liked last season

player shots goals expected difference
0 Connor McDavid 416 41 42.1 -1.1
1 Jason Robertson 419 44 37.7 6.3
2 Cole Caufield 386 51 37.6 13.4
3 Nathan MacKinnon 460 45 36.1 8.9
4 Alex DeBrincat 438 37 36.0 1.0
5 Wyatt Johnston 300 44 35.6 8.4
6 Tomas Hertl 319 22 33.7 -11.7
7 Kirill Kaprizov 414 39 33.1 5.9
8 Jake Guentzel 309 35 32.7 2.3
9 John Tavares 300 29 32.4 -3.4

This is the table that makes expected goals worth the trouble. Everyone on it generated the most dangerous shots in the league — but the difference column is where it gets interesting. Cole Caufield turned 37.6 expected into 51 goals. Tomas Hertl got the same kind of looks and scored 22. McDavid, for all of it, finished about exactly as expected.

Beat your expected goals and you either finished well or you got lucky, and one season can't tell you which. Separating those is its own model, and it's on the site as the finishing rating.

What it can't see

The list of things this model doesn't know is longer than the list of things it does, and that's the honest part:

  • What happened before the shot. A one-timer off a cross-ice pass and a wrist shot off the rush from the same spot look identical to it. Pass data would fix this and the NHL doesn't publish any.
  • Screens and traffic. A clean look from 40 feet and the same shot through two bodies get the same number.
  • Who's shooting. On purpose, so that finishing can be measured separately.
  • Who's in net. A shot on a backup counts the same as a shot on Hellebuyck. Goalie quality is handled on the goalie pages instead.
  • Where the shot went. The NHL records where shots come from, not where they end up, so glove-side high and blocker-side low are the same shot here.
  • Scorer bias. Every building has people logging these events by hand, and some rinks are more generous than others about what counts as a shot from where. It's small but it's real.
  • One-second clock. Rebounds and rush chances are timed off a clock that only counts whole seconds, so some genuinely quick sequences get missed.
  • It runs about 5% rich, almost all of it on the high-danger end. Treat a player's total as a good estimate, not a decimal-place fact.

Where it shows up on the site

  • Shot maps on player pages: every dot is sized by its expected goals, with the season total next to it as ixG.
  • The game log: attempts, unblocked attempts, shots and ixG, night by night.
  • Goalie cards: the six zones a goalie faces shots from, each one colored by the goals he saved above what those shots were worth.
  • The finishing card: a shooter's goals against his expected goals, by zone, with an honest range around it instead of a single number.
  • The draft value model doesn't use any of this yet, but a version built on shot quality instead of points is the obvious next thing to try.
Built from the notebook 04_xg_model_explained.ipynb. The models behind it live on this site: shot maps and expected goals on player pages, draft value on the draft tab.
i watch the games

© 2026 i watch the games. All rights reserved.

ThreadsInstagramTwitter