methodology
how the model works · what it misses · citations
the pipeline
Everything on this site is built from the HockeyTech API in four steps. Each one writes plain CSVs the next one reads, so any stage can be re-run on its own.
scraper pulls raw play-by-play and summary JSON per game. converter flattens them into event CSVs (shots, goals, penalties, faceoffs, hits, blocks) and normalizes rink coordinates so both teams attack the same direction. metrics computes everything derived. Known scorer errors — usually coordinates entered at the wrong end of the rink — are corrected from a corrections.csv file rather than by patching code.
location xG
A logistic regression fitted on three seasons of PWHL data (2023–2025): ~12,595 shots, 1,039 goals, log-loss 0.2680. It takes three inputs — distance to net, shot angle, and shot type — and returns the probability that a given shot becomes a goal. The net is at x=89. Wrist shots are the reference level, so every other shot type's coefficient reads as a shift relative to a wrist shot.
| term | coefficient | |
|---|---|---|
| intercept | −1.129124 | |
| distance | −0.042386 | per foot |
| angle | −0.007982 | per degree |
| Wrist | 0.000000 | reference level |
| Default | 0.202909 | HockeyTech unclassified bucket |
| Snap | 0.247062 | |
| Backhand | −0.226502 | |
| Slap | 0.525135 | n=652 |
| Tip | 0.074694 |
These coefficients are inlined in three places — pwhl_metrics.py, pwhl_goalie_metrics.py, and game.html. A refit has to update all three. pwhl_xg_refit.py regenerates them and reports the delta against the deployed values.
what it misses
This is a location model. It is deliberately narrow, and these are the known gaps:
- No rebound flags. Rebounds convert at roughly 2–3× the base rate. The model treats them as ordinary shots from that location.
- No rush flags. Rush chances convert at roughly 1.5–2×. Same treatment.
- Not strength-state adjusted. A power-play shot and an even-strength shot from the same spot get the same xG.
- All-situations only. There are no 5v5 / PP / PK splits anywhere on the site, because the HockeyTech API doesn't expose shift data. This isn't a modelling choice — the data isn't there.
bayesian shooting %
Raw shooting percentage is unusable in small samples — score on your only two shots and you're a 100% shooter. xS% pulls each player toward a league-wide prior, hard when their sample is thin and barely at all when it's large.
The prior is built by adding two fitted Beta distributions together: an empirical PWHL fit and Kierans' (2021) NHL fit. Because both are Betas, combining them is just adding their α and β. Kierans dominates — his sample is much larger (concentration ~162 against our ~48). Forwards and defenders are fitted separately.
| position | PWHL fit | Kierans NHL fit | combined |
|---|---|---|---|
| forwards | α=5.00, β=42.84 | α=12.96, β=149.22 | α=17.96, β=192.06 → 8.5% |
| defenders | α=2.91, β=45.28 | α=5.69, β=168.85 | α=8.60, β=214.13 → 3.9% |
Two caveats the pipeline flags itself. The defender prior rests on 25 qualifying player-seasons pooled across three seasons — genuinely thin, and weaker than the forward prior as a result. And the forward mean has climbed every season (8.8% → 9.4% → 10.5%), so this prior is not fixed; it should be refit each offseason rather than treated as settled.
game score
A weighted composite of box-score events, adapted from Dom Luszczyszyn's framework. It answers one question: how much value is this player generating per unit of ice time?
The shot weight comes from the league's own conversion rate — roughly 8% in the PWHL implies about 0.075 expected goals per shot. Primary assists are weighted well above secondary ones because they correlate ~80% with goal value, while secondary assists are more distal.
Blocked shots are deliberately excluded. They track team shot-blocking systems more than individual defensive skill, and at a 0.03 weight the contribution worked out to ~0.004 GS/60 — indistinguishable from noise.
TODO: the deeper game score post referenced from "what is this?" — weight derivation, and why A2 was moved off Luszczyszyn's original value.sample thresholds
Nothing is hidden, but thin samples are marked so they aren't read as settled. Flagged players are also excluded from percentile rankings.
- Skaters — flagged below 5 GP or 50 total minutes of ice time.
- Goalies — flagged below 5 GP or 120 minutes (~2 full games), below which GAA and SV% carry no information.
- xS% — needs at least 20 shots on goal to display at all.
- Goalie xG stats (GSAx, xSV%, SV−xSV) — need at least 200 shots against, below which xGA accumulation is too thin to mean anything.
citations
- Kierans, J. (2021). Isolation of Latent Player Shooting Ability in the NHL. Concordia University, MSc Thesis. Sections 4.2.1 and 4.2.2. — source of the NHL shooting-talent priors for forwards (2016–19, ≥200 sog) and defenders (≥50 sog).
- Csiernik et al. (2025). Frontiers, PMC12580244. — used to validate that PWHL-specific zone conversion rates are reflected in the location xG model.
- Luszczyszyn, D. — Game Score framework, adapted here with a reweighted secondary assist.