Empirical replacement for the ad-hoc pseudo-observation constants in project_rosters_2026_27.py (PSEUDO_GP=25, PSEUDO_STARTS=80) and the Chunk-D shooter-GAx shrink. Two independent methods; the walk-forward predictive sweep is the decider. All rates are shrunk as sum / (n + k) -- k is "pseudo-observations of league-average (zero)".
Data: relwpa_v3_by_game*.csv (16 seasons, skaters only -- goalies removed via player_bio.csv pos=G plus a TOI/GP>2700s fallback), shooters_gax_by_season_v2.csv, goalies_gsax_by_season_v2.csv. relwpa is in win-probability percentage points (/100 = wins).
Player-seasons with >=40 GP, games split odd/even by game_id, per-game mean each half, correlated across 8,805 player-seasons:
| quantity | value |
|---|---|
| half-vs-half Pearson r | 0.577 |
| (Spearman) | 0.537 |
| median GP (full season) | 67 |
| Spearman-Brown full-season reliability | 0.732 |
| implied k = m(1-r)/r pseudo-GP | 24.5 |
Cross-check for the shot metrics (adjacent-season correlation is an upper bound on k because it also contains true talent drift): GSAx raw per-shot YoY weighted r = 0.112 at median 820 shots -> implied k ~ 6491 shots (231 starts); GAx raw per-SOG YoY weighted r = 0.149 at median 83 SOG -> implied k ~ 474 SOG.
For each season s >= 2012: predict next-season rate as sum_s/(n_s + k), score vs realized s+1 rate, weighted by s+1 sample size, pooled across all seasons.
| k | weighted corr | weighted MAE |
|---|---|---|
| 5 | 0.5475 | 1.6858 |
| 10 | 0.5520 | 1.6299 |
| 15 | 0.5537 | 1.5957 |
| 20 | 0.5545 | 1.5739 |
| 25 | 0.5549 | 1.5602 |
| 30 | 0.5550 | 1.5518 |
| 35 | 0.5551 (max) | 1.5469 |
| 40 | 0.5550 | 1.5447 |
| 45 | 0.5550 | 1.5442 (min) |
| 50 | 0.5549 | 1.5449 |
| 60 | 0.5546 | 1.5494 |
| 75 | 0.5542 | 1.5593 |
argmax corr k=35, argmin MAE k=45.
This variant matches how the projection actually feeds the shrink (recency-decayed multi-season sums), so this k is the one that replaces PSEUDO_GP.
| k | weighted corr | weighted MAE |
|---|---|---|
| 5 | 0.5639 | 1.5852 |
| 10 | 0.5684 | 1.5590 |
| 15 | 0.5701 | 1.5432 |
| 20 | 0.5709 | 1.5327 |
| 25 | 0.5713 | 1.5258 |
| 30 | 0.5714 | 1.5214 |
| 35 | 0.5714 (max) | 1.5190 |
| 40 | 0.5714 | 1.5180 (min) |
| 45 | 0.5713 | 1.5180 |
| 50 | 0.5712 | 1.5188 |
| 60 | 0.5709 | 1.5220 |
| 75 | 0.5703 | 1.5293 |
argmax corr k=35, argmin MAE k=40.
| k | weighted corr | weighted MAE |
|---|---|---|
| 50 | 0.2190 | 2.516 |
| 100 | 0.2239 | 2.390 |
| 200 | 0.2275 | 2.311 |
| 300 | 0.2289 | 2.292 |
| 400 | 0.2297 | 2.289 (min) |
| 500 | 0.2301 | 2.291 |
| 600 | 0.2303 | 2.294 |
| 800 | 0.2306 (max) | 2.301 |
argmax corr k=800, argmin MAE k=400.
League shots per start (Faced/Games): 28.1.
| k | weighted corr | weighted MAE |
|---|---|---|
| 500 | 0.1493 | 0.948 |
| 1000 | 0.1516 | 0.896 |
| 2000 | 0.1534 | 0.864 |
| 3000 | 0.1542 | 0.855 |
| 5000 | 0.1550 | 0.853 (min) |
| 8000 | 0.1554 | 0.855 |
| 12000 | 0.1557 (max) | 0.857 |
argmax corr k=12000 shots (= 427 starts), argmin MAE k=5000 shots (= 178 starts).
| k | weighted corr | weighted MAE |
|---|---|---|
| 100 | 0.2697 | 0.918 |
| 300 | 0.2740 | 0.848 |
| 500 | 0.2746 (max) | 0.815 |
| 1000 | 0.2742 | 0.778 |
| 2000 | 0.2725 | 0.765 (min) |
| 3000 | 0.2712 | 0.767 |
argmax corr k=500, argmin MAE k=2000.
| constant | current | recommended | basis |
|---|---|---|---|
| PSEUDO_GP (skater relWPA) | 25 | 35 | decayed-predictor sweep: corr max at k=35, MAE min at k=40; plateau ~25-50. Split-half implies 25. Current 25 sits on the plateau edge -- cost of staying is ~0.0002 corr / +0.007 pct-pt MAE; 35 is the center of the plateau on both criteria. |
| GAx pseudo-SOG (Chunk D) | -- | 500 | MAE min k=400, corr max k=800 but nearly flat 400-800; the independent YoY-implied k = 474 SOG lands on the same value. Plateau 400-600. |
| PSEUDO_STARTS (goalie GSAx) | 80 | 180 (= 5000 shots / 28.1 shots-per-start) | MAE min at k=5000 shots; corr still creeping up at 12000 but +0.0007 from 5000; YoY-implied ~6491 shots agrees. Current 80 starts = ~2250 shots under-shrinks: MAE 0.00864 vs 0.00853 per shot, corr 0.1534 vs 0.1550. |
| RebPer100 pseudo-SOG | -- | 500 | corr max at k=500 (plateau 300-1000). MAE keeps falling toward heavier shrink (deviation target is near zero-mean, so MAE alone rewards over-shrinking); corr is the discriminating criterion here. Note rebound control needs ~10x less shrink than GSAx -- per-shot rebound skill is far more reliable than per-shot save skill, consistent with the xg_v2 finding that goalie persistence concentrates in the rebound game. |
Shape of the optima: flat. Every sweep is a broad plateau (relWPA 25-50, GAx 400-600, GSAx 3000-8000 shots, rebounds 300-1000), so the exact integer matters little; what matters is order of magnitude, and the ad-hoc PSEUDO_GP=25 was essentially right while PSEUDO_STARTS=80 was ~2x too trusting of goalie samples.
Cross-method agreement (split-half / YoY-implied vs predictive sweep): skater 25 vs 35, GAx 474 vs 500, GSAx 6491 vs 5000 shots -- two independent derivations land within a factor of ~1.4 everywhere, which is strong evidence the constants are real properties of the metrics, not artifacts of the tuning procedure.
Multi-season note: the 3-season decayed predictor wants the same k as the single-season one (both plateau at 35-50); the decayed effective sample (den = sum of decayed GP, ~1.9x a single season for a full-time player) is what the shrink divides into, so no separate constant is needed -- just keep applying num/(den+k) to the decayed sums as the projection already does.