New to these reports? Start here
- Dotted-underlined words have a plain-English definition — hover or tap them. Every term is also on the glossary page.
- "Null" means we found nothing, not that something broke. Most reports here are negative results, on purpose — knowing an idea doesn't work is the point.
- Two questions get asked separately. First, is the effect real? Second, is it already priced into the betting odds? An effect can be completely real and still useless to bet on.
- A "calibration" row is a self-check. It runs the same method on something already known to be true. If that fails, the whole report is unreliable — so it's reported alongside the findings.
- If a confidence interval includes zero, the real effect might be nothing at all, so no claim gets made.
College hockey player value: shot xG, zones, goalies, skaters (2026-10-10)
Scripts: scripts/college_hockey/build_shot_xg.py (fit + write), scripts/college_hockey/goalie_value.py,
geometry in [redacted]. Results JSON next to this file.
The old xG was worse than a constant
Every stored xG came from ExpectedGoalsModel (xg_elo_prediction_model.py) or the routes' calculate_xg,
both of which put the net at x=89, y=42.5. CHN's chart has the goal lines at x~10 and x~90 and the centre at
y=50, and a team attacks a different end each period. The stored xG barely varied (goals averaged 0.056,
saved shots 0.054) and its log losslog lossA score for probability forecasts that punishes confident wrong answers harshly. Lower is better. on shots on goal was 0.3522, worse than predicting
the league rate for every shot (0.3122).
Geometry
- x is 0-100 along the rink; shots start at ~10 (goal line) and stop at ~37 (blue line), so 64/27 ft per unit. y is the 85-ft width, 0.85 ft per unit. Goals then sit a median 16.6 ft out with a 12.8-ft lateral spread.
- Attacking end per (game, team, period) = the half holding most of the team's attempts; fewer than 1% of attempts are in the other half, and the end flips between periods as it should (2,065 of 2,074).
- Empty-net goals: the shot chart has no clock and the goal log's team field is unreliable (5,401 goals say "Team"; some games credit every goal to one abbreviation), so each period's empty-net goals are credited to the game winner and that many of the winner's goal-shots are flagged, farthest first: 307 of 320.
Model
Logistic regression on distance, angle and zone, 5-fold cross-validation grouped by game, 2025-26 + 2026-27 charts.
| Target | n | goals | base rate | log loss (CV) | base | AUC |
|---|---|---|---|---|---|---|
| goal given unblocked attempt | 86,847 | 5,757 | 0.066 | 0.2269 | 0.2439 | 0.708 |
| goal given shot on goal | 61,114 | 5,757 | 0.094 | 0.2934 | 0.3122 | 0.688 |
Calibrated by decile (predicted vs actual on-goal rate, 2.4% to 20%). Location only: no rebounds, rush, screens or shot type, so AUC ~0.69 is the ceiling for this data, typical for location-only hockey xG.
Zones
| Zone | Attempts | Share | Goals per shot on goal |
|---|---|---|---|
| s1_prime | 11,938 | 10.6% | 19.1% |
| s2_scoring | 19,744 | 17.5% | 12.9% |
| s3_outer_slot | 21,605 | 19.1% | 8.1% |
| low_wing_l | 5,564 | 4.9% | 9.0% |
| low_wing_r | 5,511 | 4.9% | 8.4% |
| upper_wing_l | 6,114 | 5.4% | 3.7% |
| upper_wing_r | 6,236 | 5.5% | 4.4% |
| point_l | 12,811 | 11.3% | 3.4% |
| point_c | 7,501 | 6.6% | 6.5% |
| point_r | 12,943 | 11.5% | 3.6% |
| behind_net | 436 | 0.4% | 7.3% |
| outside_zone | 2,527 | 2.2% | 3.4% |
Goalies
- Saves above average per 100 shots, season to season: r = 0.31 (95% CI95% confidence intervalThe range the true value is plausibly in. If this range includes zero, we cannot rule out that the real effect is nothing at all. 0.14 to 0.49, n = 137 goalies with 400+ shots in both). Skill is real but most of a season is noise.
- Within 2025-26, odd vs even games: GSAx r = 0.33, GSAA r = 0.32 (n = 32). The location adjustment has not yet shown it is more repeatable; one charted season is too little to tell.
- Teams allow 0.085-0.105 xG per shot on goal (SD 0.0041): a few goals a season for a starter.
Not done / caveats
- Shots carry no shooter name, so there is no individual skater xG.
xg_elo_prediction_modelstill computes its own xG with the wrong geometry. It is not used by the live predictions (prediction_generator uses only its EloElo ratingA rating system, originally from chess, that moves a team up or down based on results and the strength of the opponent.), so it was left alone; fix and re-test before using it.- Model fitted once on 2025-26; refit by running build_shot_xg.py by hand. The nightly shot cron applies it.
Skaters (scripts/college_hockey/skater_value.py, /college_hockey/skaters)
Offense and penalties only. CHN has no ice time and no on-ice data, so skater defence is not measured; +/- is shown but not counted (it repeats at r = 0.32, the least of anything).
- Goals created (Hockey-Reference): (G + A/2) scaled to team goals, empty-net goals removed.
- Replacement: goals created per game of each team's 13th+ forwards / 7th+ defencemen by games played.
- Penalties: power plays given (minor 1, major 2.5) vs the position average x league PP conversion.
- 6.98 goals per win (team-season win% on goal differential per game).
- Positions from CHN rosters (94% matched; the rest treated as forwards).
Season to season, skaters with 15+ GP in both (3,522 pairs):
| Per game | r | 95% CI |
|---|---|---|
| gar_per_game | 0.609 | 0.587-0.631 |
| points_per_game | 0.654 | 0.633-0.676 |
| goals_per_game | 0.598 | 0.572-0.622 |
| shots_per_game | 0.721 | 0.701-0.738 |
| penalty_gar_per_game | 0.508 | 0.481-0.538 |
| plus_minus_per_game | 0.322 | 0.290-0.353 |
GAR is mostly scoring, so it repeats about as well as points; shot volume repeats best.
Next season's team goal differential per game from last season's, leave-one-season-out (252 team-seasons, 2021-22 to 2025-26). Prior GD alone: RMSE 0.7735. Adding the roster change in each metric (returners + incoming transfers by name and DOB, minus last season's roster):
| Roster metric | RMSE | Seasons better |
|---|---|---|
| points | 0.7536 | 4/5 |
| gar | 0.7473 | 4/5 |
| gar_shot_weighted | 0.7419 | 4/5 |
Real but modest (~4%). The shot-weighted version (goals half as scored, half as shots x league shooting %) is stored as pred_gar/pred_war. Only three variants were tried and the 0.5 weight was not tuned. Roster pages can include mid-season additions, a small leak toward these numbers. Kept separate from the pre-registered roster-turnover test (PREREG_ROSTER_TURNOVER_2026.md), whose frozen files were not touched.