Methodology
Last updated November 9, 2026
How PuckLab gets its numbers: where the data comes from, how each model works, which hockey rules every stat follows and how the numbers are kept current. Each definition below is the one the site computes, and each term links to its glossary entry.
Data sources
Every game comes from the NHL's own public data: the schedule, play-by-play events, shift charts and boxscores from the NHL APIs (api-web.nhle.com and api.nhle.com), and the NHL's HTML game reports when a game's shift chart is missing. Official season totals come from the NHL's stats API.
Contracts come from CapWages, matched to each player's NHL id; transactions from Wikipedia's NHL transactions pages. Every on-ice metric is computed by PuckLab from the NHL's raw events, with no third-party stats provider.
Expected goals (xG)
Expected goals (xG) give each unblocked shot attempt the probability that it becomes a goal. PuckLab's model (version 3, in use since September 2026) works in two stages: stage A estimates the chance that an unblocked attempt reaches the net, stage B the chance that a shot on goal is a goal, and a shot's xG is A × B.
It reads 49 inputs built from the play-by-play: the shot's distance and angles to the net actually attacked, its location (slot, point, off wing), its type, rebounds and flurries of the shooting team's own attempts, the previous event (its type, team, zone, distance, speed and the time before the shot), the time since the last faceoff and who won it, and the skaters on each side, with empty-net and extra-attacker flags.
It is trained on the three most recent regular seasons, weighted 0.5, 0.75 and 1.0 from the oldest to the latest: older seasons were tested and made predictions on unseen games worse. Blocked shots get no xG (0, as MoneyPuck and Evolving-Hockey count them), penalty shots one flat value (their observed conversion rate), and shootout attempts and misses that fall short of the net are left out. Empty-net shots get an xG and are flagged, and every xG total on the site leaves them out.
The model is neutral: it doesn't know who shot or who was in goal, nor the period, the clock, the score or home ice (tested and dropped, with no value on unseen games). What a shooter or a goalie adds shows up beside it: goals above expected for a shooter, GSAx for a goalie.
Accuracy on games the model never saw: a model trained on 2021-22 to 2023-24, with 2024-25 for early stopping and calibration, predicted the 2025-26 regular season and was scored beside MoneyPuck's public model on the shots both models scored. Area under the ROC curve (1 = every goal ranked above every non-goal, 0.5 = a coin flip): 0.766 for PuckLab and 0.770 for MoneyPuck on the 109,038 shots with a goalie in net; 0.778 and 0.742 on the 85,950 of them at 5v5. The power play isn't compared: MoneyPuck's file records the penalty time left after a goal, which leaks the outcome.
Calibration: each season has its own adjustment (one per stage and shot type) so that its expected goals match its goals. In the regular season, the current season's adjustment is refit every week on its own shots, pulled toward the previous season's while the sample is small, then once more when the regular season is complete; never in the playoffs. A refit re-scores the season's games already played.
Goals saved above expected (GSAx)
Goals saved above expected (GSAx) = the expected goals of the shots on goal a goalie faced − the goals he allowed, empty-net shots and goals excluded. It counts shots on goal only, through stage B (the chance that a shot on goal is a goal): a missed shot isn't a save. A positive GSAx means he allowed fewer goals than those shots were expected to produce.
The first term is his expected goals against (xGA): the xG of the shots on goal he faced. GSAx starts with the 2015-16 regular season and the 2010-11 playoffs; before them it is unknown and shown empty, never 0.
Corsi, Fenwick and the hockey rules
Corsi counts every shot attempt: goals, shots on goal, missed and blocked shots. Fenwick counts the unblocked ones. A player's CF%, FF% and xGF% are his team's share of the attempts, the unblocked attempts and the expected goals while he is on the ice at 5v5: for ÷ (for + against) × 100. A team's are the same shares over its 5v5 play, and its HDCF% the share of high-danger chances, the attempts from the most dangerous locations.
Every stat follows the same hockey rules:
- Situations (5v5, power play, penalty kill) come from the NHL's situation code on each event, not from a timeline rebuilt from penalties.
- A shot attempt blocked by the shooter's own teammate counts for the shooter's team. NHL.com's SAT counts most of them for the other team, so from 2023-24 on a 5v5 CF% here can sit a few tenths of a point from NHL.com's SAT %.
- In the regular season, the shootout is left out of every stat built from game events. In the playoffs every overtime period counts (there is no shootout), and an overtime loss is a loss; the 2020 round robin's seeding games, played under regular-season rules, are the exception.
- Every expected-goals total, goals above expected and GSAx leave empty-net shots out; empty-net goals still count in the score.
- A goalie's game played is a game the NHL lists him in (ice time, a decision, or a shot against); a start is being in net at 0:00 of the first period; a shutout is no goal against as his team's only goalie with ice time.
- The regular season is 84 games from 2026-27.
- Percentile ranks compare skaters of the same position group (forwards or defensemen) with 20 or more games played, and goalies with 10 or more starts, in the regular season. The Compare page and every playoff ranking rank everyone with at least one game played.
Player archetypes
Each skater gets one of ten archetypes from his percentile ranks: hard gates on percentiles, tested in order, and the first one he passes is his, with a tier from his ice time. The ten group into six role families: snipers and playmakers, power and two-way forwards, defensive forwards and grinders, offensive defensemen, two-way defensemen, and shutdown and physical defensemen.
A skater needs 20 games at 5v5 in a season to be classified. During the regular season his label is updated day by day and only changes when another archetype fits clearly better; once the regular season is complete, the season is classified again in one pass. A player's archetype as of a season is the latest one among that season and the two before it.
Contract value
A contract's cap hit is its average annual value: the total salary ÷ the term, the same in every season of the deal.
A skater's contract value (model v3) is the fair average annual value of his contract's remaining seasons: his profile in the season before, priced by a market model of cap share fit on five off-seasons of skater contracts (entry-level contracts excluded) within his role family, then moved along his position's aging curve; restricted-free-agent seasons are priced at the RFA price, and no season is valued under the league minimum. Surplus value = that fair value − his cap hit, and each skater not on an entry-level contract is ranked within his role family.
Every page shows one value: model v3, else the earlier v2 model's, else the value of comparable contracts. A contract none of them values reads "Not valued", never $0.
Daily refresh and NHL corrections
A daily run at 10:30 UTC loads the games played since the last run (events, shifts and boxscores), the standings, rosters and transactions, then rebuilds the stats and model values built from them.
The NHL sometimes revises a game's boxscore days later (shots on goal, time on ice). Every day the run compares the stored games with the NHL's stats and reloads the ones that differ. Official per-game numbers come from the NHL's boxscores and official season totals from the NHL's season stats.
Pages read these stored numbers, cached until the next run writes new data, so a number reads the same everywhere it appears.