Blocked shots, part 2: putting them back where they came from
The NHL records a blocked shot where it died, not where it was born. How we recover the true origin from Edge tracking — and the two ways the fix fooled us before it worked.
Part 1 ended at the problem: blocked shots are worth scoring, but the league records them where they died — at the blocker — not where they were born. To score a blocked shot you need its origin, and the origin is simply not in the data. This post is how we get it back: where the ground truth comes from, the model fitted on it, and — because both stories are instructive — the two ways the fix fooled us before it worked.
The ground truth hiding in goal clips
The NHL's Edge tracking system watches every game — puck and skaters, frame by frame — but the raw feed is not public. What does surface is a window around each goal: positions for the puck and all skaters at roughly 10 frames a second, in the same coordinate frame the play-by-play uses, reaching back about 14 seconds before the goal. And when a blocked shot happens to fall inside that window, it comes with tracking attached — which means that for a slice of blocked shots, the true origin is sitting there, waiting to be read off.
Three seasons of goal clips (2023-24 through 2025-26) contain 3,190 such pre-goal blocks. For each one we extract an origin: find the frame where the puck reaches the recorded block location, find the release — the moment puck speed jumps to shot speed — and take the last frame the puck is still on the shooter's stick. That final step-back matters: at 10 frames a second, the first frame of flight is already about 8 feet downrange of the stick. Each extraction then has to pass quality gates — the puck genuinely approached the recorded block point, it was released toward the net, the origin is inside plausible shooting range — and 1,347 clean ground-truth origins survive: 771 forward shots, 576 defenseman shots.
The caveat, stated up front: pre-goal blocks are a biased sample — a goal follows within seconds, so they skew slightly closer to the net than blocks at large (median 21 vs 24 feet recorded). We fit the conditional relationship — origin given block location and shooter role — so that marginal skew does not bias the curve; and where far blocks are sparse (beyond ~50 feet) the model clamps rather than extrapolates.
The interpolation
Two empirical facts make the model almost embarrassingly simple. First, the true origin lies essentially on the ray from the net through the block point — the blocker stands between the net and the shooter, and the median angular deviation in the ground truth is about 4°. So the two-dimensional question collapses to one dimension: how far back along the ray? Second, that distance is monotone — the farther out the block was recorded, the farther out the shot originated. So the whole model is one monotone curve per shooter role, mapping block distance to origin distance, fit on the ground truth; the origin is placed at the predicted distance along the net-to-block ray.
Validated held out against origins the fit never saw, the correction roughly halves the error: defensemen 32 → 17 feet RMSE against the no-correction baseline, forwards 27 → 19. The residual ~17–19 feet is irreducible per-shot scatter — block distance alone cannot pin an origin exactly — and the curve targets the conditional mean, which is what a location feature needs.
What it does at corpus scale
The defensemen land almost exactly on the unblocked distribution. The forwards look like a bug: imputed median 42 feet, when unblocked forward shots have a median of 26. We stared at that number for a while. It is real, and the resolution is the single most useful fact in this series for reading blocked-shot data:
A long shot has a long path through traffic, so the blocked population is enriched in far origins — comparing blocked forwards to all unblocked forwards is comparing two different populations. (Defensemen show only a ~1.6× rise: they shoot from the point regardless, so there is little dynamic range for selection to act on.) This is also part 1's selection-bias argument, now with its coefficient attached: blocks are about as far from a random sample of attempts as it is possible to be.
Twice fooled, twice fixed
Between fitting this model and trusting its output, the xG model downstream caught us twice. Both incidents are worth telling on ourselves, because they share a moral.
The fingerprint. The mean curve maps a whole band of block distances onto a narrow band of origins — those tall green spikes above. That collapsed thousands of blocked shots onto a thin manifold of coordinates that no real shot ever occupies. And since a blocked shot is never a goal, the xG model learned to recognize the manifold — from coordinates alone it could identify blocked shots at 0.87 AUC — and pushed their xg below the true danger at release, which is precisely the number we built all this to estimate. The fix: draw each origin from the conditional distribution instead of the mean — per-shot noise sized by the held-out error, seeded per shot so the pipeline still rebuilds bit-identically.
The center-line trough. Subtler and stranger: the model scored dead-center shots as almost never blocked — at 40 feet, straight down the middle, P(block) came out 0.07 while five feet to either side sat at ~0.56. Defenders do not part like a curtain on the center line; this was an artifact. The play-by-play logs coordinates on an integer-foot grid, and dead center is the modal lane, so real shots pile up at exactly y = 0 — while our continuous imputed origins landed there 140× less often. Imputed blocks were the corpus's only continuous coordinates, so the razor-thin bin at y = 0 was almost entirely unblocked shots, and the model concluded "dead center ⇒ never blocked."
The shared moral: the imputation must not give blocked shots a signature. A gradient-boosted model will find any bookkeeping difference between blocked and unblocked coordinates — a too-regular manifold, a missing grid artifact — and read it as hockey. Both fixes are the same fix: make imputed coordinates statistically indistinguishable from recorded ones in every respect except the one thing being corrected.
Where this leaves us
Every blocked attempt in our tables is now scored from its imputed origin: a point shot blocked in the slot is priced as a point shot, drawn from the distribution the ground truth supports, on the grid the league records. That closes part 1's data problem — and it opens a door. Once the block stage of the game has honest geometry, you can start asking questions that were previously unanswerable: whether getting a shot through is its own skill, whether stopping one is, and what either does to the way we score a shot in the first place. How to incorporate that into the model is the work we are on now, and we will write it up when it holds.
Deeper reading
- The blocked-shot imputation report — every figure here plus the agreement and selection-bias reads, with the commands that regenerate them
- The model-baseline report — where the fingerprint was caught