NHL Adjusted Plus-Minus (RAPM) — Methodology
What these ratings are
Each player gets two ratings, both measured in shots per 60 minutes of 5-on-5 play, relative to the league average for that season:
- Offense rating — how many more (or fewer) shots the player's team generates while they are on the ice, above or below an average player.
- Defense rating — how many fewer (or more) shots the player's team allows while they are on the ice. It is oriented so that higher is better: a positive defense rating means the player suppresses shots relative to average.
- Total rating — offense rating plus defense rating: the player's net impact on the 5-on-5 shot balance per 60 minutes, relative to average.
A rating of 0 is exactly league average. A +5 offense rating means "about
five more shots generated per 60 minutes than an average player would produce in
the same role."
Why "adjusted", and why relative to average
Raw on-ice shot differentials reward players who happen to skate with strong linemates or against weak opponents. RAPM (Regularized Adjusted Plus-Minus) is a single ridge regression across every 5-on-5 shift in the season that estimates each player's independent contribution while holding their teammates and opponents fixed. Because all players are estimated jointly, the ratings are already teammate- and competition-adjusted.
The ratings are expressed relative to league average by construction. The model fits one league-average shot rate (the intercept) plus a per-player deviation from it, and the deviations are constrained to average out to zero across the league (an exposure-weighted sum-to-zero constraint). This makes "0 = average" an exact, interpretable baseline rather than an artifact of the fit.
Why REML instead of cross-validation for the penalty
Ridge regression needs a penalty strength that controls how much player ratings are shrunk toward average. On this design the usual cross-validation choice is numerically unstable — the design has a near-flat direction that lets the intercept trade off against a uniform shift of all player ratings, so CV can pin the penalty at its search boundary and hand back an unhealthy fit.
We instead select the penalty by REML (restricted maximum likelihood) of the
equivalent mixed model, treating player effects as a random effect. REML has a
well-defined interior optimum with real curvature in all 9 seasons,
which removes the instability. Across the 9 seasons the selected
penalty (variance ratio λ) ranges from about 26 to 126
(see seasons_meta.csv). The sum-to-zero constraint additionally pins the
intercept to the exact weighted league-average shot rate, so the "relative to
average" interpretation above holds exactly.
Data-quality caveat: compare rankings within a season, levels across seasons cautiously
The input is public play-by-play and shift data. Its completeness varies by
season: the shift/event coverage is essentially complete in 2018-2019
(coverage-dispersion statistic rowsum_std = 0.006) and least complete in
2023-2024 (rowsum_std = 0.278); per-season figures are in
seasons_meta.csv. Lower-coverage seasons undercount some events, which can
shift the overall level of a season's ratings up or down.
Within a season, the rankings are robust. The ordering of players is stable
across estimator choices — the standard and constrained estimators agree on the
offense ranking with a Spearman correlation between 0.89 and
0.98 across seasons. Across seasons, compare cautiously. A player
who is +4 in one season and +3 in another is not necessarily better in the
first — part of that gap can be the season's data-completeness level rather than
a real change in play. Use the ratings to rank players within a season and to
identify tiers; be careful reading small cross-season level differences as
signal.
Coverage of the export
9 seasons (2016-17 through 2024-25), 8,215 player-seasons in
total, of which 6,232 clear the 200 weighted-minute eligibility bar
used as the site's default filter (the eligible flag in each
rapm_<season>.csv). Players below that bar are retained but flagged ineligible,
because their ratings are estimated from little ice time and are noisy. Player
positions (C/L/R/D) are not in the play-by-play source and are looked up
separately from the public NHL player records; a small number of players whose
record could not be resolved are left blank.
Generated by scripts/build_website_export.py. Ratings computed with the
sum-to-zero + REML constrained estimator (apm/fit_constrained.py), the
configuration validated as arm C in scripts/estimator_comparison.py.