EntroπaLabs
Decision support · Valuation

Valuing a hockey player from a box score

Points tell you who is good. Surplus value tells you who is *tradeable* — and the gap between those two is where a GM actually works.

Ask who the best player on a hockey team is and you get an argument about points. Ask who the most valuable asset is and you get a different answer, because value in a salary-capped league is production you didn't pay full price for. The NHL GM Playbook is built around that second question.

WAR from a box score

Proper WAR models use play-by-play and shot-quality data. Public box scores give you goals, assists, shots, hits, blocks, takeaways, giveaways, plus-minus, penalty minutes and ice time — so the model builds the best estimate those columns support, and is explicit that it is an estimate.

Every countable contribution is converted to goals:

offense  = 0.72·goals + 0.45·assists + 0.02·shots
two_way  = 0.045·takeaways + 0.030·blocks + 0.008·hits
           − 0.045·giveaways + 0.06·plusMinus
contrib  = offense + two_way − 0.02·PIM

Then the crucial step, the one that separates WAR from a counting stat: subtract what a replacement-level player would have produced in the same ice time, and convert to wins.

gar  = contrib − replacement_per_60 · (toi_min · gp / 60)
       replacement: 0.85 for forwards, 0.62 for defencemen
war  = gar / 6.0        (~6 goals per win)

The ice-time term is what stops the model rewarding volume. A forward who plays 22 minutes a night has to clear a much higher bar than one playing 11, because the alternative to him is not nothing — it is a replacement player absorbing those minutes. Two players with identical point totals and very different ice time get very different WAR, which is the correct and frequently unpopular answer.

Market value, and the number that actually matters

WAR says how good a player is. It says nothing about whether you'd want the contract. So a second model estimates what that production commands on the open market:

forwards:   0.11·points + 6.0·max(0, ppg − 0.30) + 1.0
defence:    0.06·points + 0.24·max(0, toi − 15) + 1.2
× age multiplier: 1.00 ≤27 · 0.92 ≤30 · 0.80 ≤33 · 0.65 older

The max(0, ppg − 0.30) term is deliberately non-linear: the market pays a steep premium above roughly a third of a point per game, because that is where genuine top-six scoring starts and teams bid against each other for it. Below it, production is close to fungible.

And then the number the whole application is organised around:

surplus = market_value − cap_hit
Surplus value is why entry-level contracts dominate the board. A 21-year-old producing at a $9M rate on a $0.95M deal carries about $8M a year of surplus. A genuinely better 30-year-old paid $10.5M to produce at $10M carries none. The second player wins you more games; the first is the one the league trades for. If the rankings look like they are overrating kids, they are working correctly — that is the currency.

Contract projections

For pending free agents, next-contract AAV is projected from production, ice time and age, then term from the age band — 7 years for a young player who has earned real money, 2 for someone past 31. Where CapWages publishes its own projection, that is used in preference; the internal model is the fallback for players it doesn't cover.

The age curve applies a discount at both ends, for different reasons. Under-24s take a bridge or RFA discount because they lack leverage. Over-33s are discounted because teams have finally learned what the back half of a long deal costs them.

Deployment: lines, pairs, needs

The Team Lab builds a plausible lineup rather than an optimal one, and the distinction is worth being honest about. Forwards are ranked by a production/role score and filled centre-first down four lines; defence pairs are built to balance handedness, taking the best available left shot and pairing him with the best available right shot.

Roster needs come from ranking each area by relative weakness and always surfacing the top one or two — even for a strong team. A tool that tells a contender "no needs" is useless; a GM's question is never "do I have a hole", it is "where is my next upgrade".

What it doesn't know

No zone starts, no quality of competition, no special-teams split, no injuries, no no-trade clauses, no retained salary, no draft capital. The trade analyzer compares surplus totals on each side and calls the winner, which is a reasonable first-order model of a trade and a poor model of a negotiation.

The coefficients are calibrated by judgement, not fitted to historical trades — there is no labelled dataset of "fair" deals to fit against. Cap hits and contract status come live from CapWages, whose public site JSON is an undocumented endpoint, so the fetcher validates itself against anchor players it knows the answers for (McDavid at $12.5M, Matthews at $13.25M, Makar at $9.0M). If those don't match, the shape of the feed changed, and the pipeline falls back to a checked-in CSV rather than publishing numbers it can no longer vouch for.

That last mechanism is the part I would reuse anywhere. Scraping an undocumented source is a reasonable thing to do; publishing whatever it returns without checking is not. A handful of known values, asserted at runtime, converts a silent data corruption into a loud fallback.

Open the playbook