Oddball Hockey Oddball Hockey

Changelog

A record of how the Devils Performance Index formula has changed over time. Each entry explains what was added, removed, or modified — and why.

October 2026 — Expected Goals Corrected, and a Share Added

A correction first. The expected goals figure this site has been publishing was wrong, and it was dragging player ratings down with it. Every DPI score on the site has been recalculated.

What was wrong

Expected goals were produced by a fixed curve that fell off with distance from the net. The curve was never fitted to anything — it was a plausible-looking shape, not a measurement — and it ran far too hot. Across the Devils' 2025-26 season it credited 1,771 expected goals in games that produced 479 actual goals, an inflation of about 3.7 times. Two things caused it:

  • Blocked shots were counted. They were 26.9% of all attempts and absorbed 30.1% of the expected goals credited, despite being attempts that by definition cannot score. Worse, the NHL records a blocked shot's coordinates at the point where it was blocked rather than where it was taken, so the curve read them as much closer to the net than they really were.
  • The curve itself was uncalibrated. Tested on games it had not seen, attempts it rated at a 30% chance of scoring actually went in 10.9% of the time.

Because the formula rewards scoring above expectation, an inflated expectation turned into a penalty that grew with shot volume — the index was quietly punishing players for shooting. In the 4-1 win over Vancouver on October 10, sixteen of nineteen skaters carried a negative finishing component. Timo Meier took seven attempts, was credited 2.26 expected goals for them, and lost 4.51 points as a result, which cut his published rating from roughly 6.3 to 1.81. His seven attempts are worth 0.37 expected goals under the corrected model, and that night now rates 2.78.

The new model

Expected goals now come from a model fitted on 27,931 shots from 328 games sampled across the league in 2025-26, using shot distance, angle and shot type. It is scored on whole games held out of the fit, so the figures below are honest out-of-sample performance rather than the model marking its own homework.

  • Calibrated. It predicts a 6.80% scoring rate on held-out shots that actually scored at 6.97%. The old curve predicted 17.45%.
  • Better at ranking chances too — log loss improved from 0.290 to 0.231, and AUC from 0.690 to 0.731.
  • Only unblocked attempts carry expected goals, which is what every public model does and for the same reason.
  • Fitted on 2025-26 alone, deliberately. The NHL appears to have changed how it records shot locations from 2023-24, probably by taking them from the puck-tracking system behind NHL EDGE. It was never announced, and it pushed several well-known public models out of calibration. Expected goals should not be compared across that boundary.
  • The whole model, its training sample and its known weaknesses are documented in scripts/xg-model.js, and scripts/fit-xg-model.js reproduces the fit from scratch.

Expected goals share, at 5-on-5

The site can now say who controlled the quality of the chances, not just who took them. Under the shot chart, each game reports the share of 5-on-5 expected goals New Jersey generated, and the season leaderboard carries a per-player version.

Working this out needs to know who was on the ice for every shot, which the NHL's play-by-play does not say — it names only the shooter and the goalie. The shifts are published separately, so the two are joined on the clock. The join resolves 98.1% of 5-on-5 attempts to exactly five skaters a side, and agrees with the NHL's own strength-state code on 1,118 of 1,120 events in a twelve-game audit. Anything that does not resolve cleanly is dropped rather than guessed at, and a game whose shift data is missing or corrupt reports no on-ice figures rather than zeroes.

Why it is not in the per-game rating

It is tempting to fold a player's one-game share straight into his rating for the night. It would be a mistake. A player's expected goals share in one game correlates about −0.03 with his share in the next one — measured over 1,352 consecutive pairs of games. That is indistinguishable from zero. A typical player-game rests on about 3 expected goals in total, and single-game values run the whole range from 0% to 100%. Put in the nightly score, it would add noise and nothing else.

Over a season it holds up perfectly well, which is why it appears on the leaderboard and not on the game page, and why the leaderboard waits for 150 minutes of 5-on-5 ice time before showing a player a figure at all.

Plus/minus is out of the formula

Its place in the index has been taken by on-ice expected goal differential at 5-on-5 — the same idea, a player's effect at both ends while he is out there, but measured on the quality of the chances rather than on whether the puck happened to go in.

The reason is blunt: over 2025-26, splitting the season into odd and even games, plus/minus correlated −0.387 with itself. A player's plus/minus in one half of the season pointed the wrong way for the other half. On-ice expected goal differential came in at +0.514 on exactly the same split. The weight was set so the component carries the same influence it did before, and plus/minus remains on every player's stat line as a boxscore number — it simply no longer moves the rating.

What this means for old scores

Ratings published before today were computed on the inflated model and are not comparable with the ones on the site now. The whole season has been recalculated. Scores rose slightly on average — the mean per-game skater rating moved from 3.68 to 4.00 — with the biggest gains going to high-volume shooters who had been carrying the largest phantom penalties.

October 2026 — Shareable DPI Card

Every game now produces a compact card built for X and Bluesky, linked from a “Share This Game” section on the page.

  • Built for the size it is actually seen at — a timeline shows an image at roughly 500 pixels wide. The card carries the score and the top five by DPI with their bars and stat lines, and nothing else. The detailed card, with the shot chart and the full performer list, is still generated for anyone who wants it.
  • Posting the site link works too — the link preview now unfurls the current game's card rather than the site logo.
  • Previews refresh between games — both platforms cache a preview per image URL, and the card is rewritten at the same path every game. The game ID is stamped into the image URL each run so each game gets a fresh preview instead of the last one.

October 2026 — Simpler Colors, Player Stat Lines

A presentation change only. No DPI weights moved, so every score on the site means exactly what it did before.

One color scale instead of fifteen

The charts had accumulated about fifteen colors with no system behind them — ten hues in the DPI bars alone, four more on the rink. Everything now runs on a single diverging scale: purple means the player helped, red means it cost the team, grey is neutral.

  • Ten bar categories became three groups — Offense, Two-way and Discipline, each a shade of the same hue, mirrored on both sides of the axis. The darkest shade always sits nearest the center line, so a shade means the same thing whichever direction the bar runs. Hovering a segment still shows the individual components, so no detail was lost.
  • The shot chart reuses the same purples — stepped by how close the attempt came to a goal, with blocks in neutral grey. Goals also get a size bump and a white ring, so they stay findable without relying on color.
  • Red and purple — red and green, the obvious good/bad pairing, collapses under the two most common forms of colorblindness. Red and blue is the textbook replacement, but together they read as Rangers colors, so purple was measured against the alternatives and came out strongest.
  • Win and loss badges sit outside the scale — they keep a reserved green and red, and always carry the word, so color never carries the result on its own. The rink's own red and blue lines stay as a real rink looks.

Boxscore line for every player

Each player now shows their game stats next to their rating, so the number and the performance behind it can be read together: goals, assists, points, shots on goal, hits, blocks, takeaways, giveaways, penalty minutes and plus/minus. Goalies show saves, shots against, goals against, save percentage and the decision. Stats that are zero stay visible but recede.

This also fixed a bug where goalies always displayed "0 SV" — the save count was being read from a field the data never filled in.

October 2026 — Season Rollover and Playoffs

Housekeeping for the start of the 2026-27 season. The DPI formula itself did not change, so season-over-season scores remain comparable.

What changed

  • Season leaderboard no longer starts blank — The leaderboard required five games played, so for the first two weeks of a season it showed an empty table. The minimum now scales to the games actually played, and the note above the table states the threshold in force.
  • The October gap is handled — The season flips over on Oct 1, but the opener is usually a few days later. In that window the data job failed outright because the new season's schedule had no finished games. It now falls back to the previous season and labels the page as showing that season's final game.
  • Playoff games appear — Postseason games were filtered out everywhere. The scoreboard, player tables and shot chart now cover them, with a banner giving the round, game number and series standing. Multiple overtimes are labelled correctly ("2OT" rather than "OT").
  • Season stats stay regular season — Record, points, goals for and against, and the DPI leaderboard are all regular-season only. A seven-game playoff run averaged against an 82-game sample would rank players on sample size as much as on performance.
  • Cache pruning — The game cache carried every past season forever, reaching 1.8 MB. It now keeps only the current season.

February 2026 — Shot Chart

Added an interactive shot chart visualization between the scoreboard and the player tables. It shows every shot attempt from the most recent game — goals, shots on goal, missed shots, and blocked shots — for both the Devils and their opponent, plotted on an SVG rink diagram.

What it shows

  • Shot dots — Each dot is colored by type: red for goals, blue for shots on goal, grey for misses, and amber for blocked shots. Dot size scales with expected goals (xG), so high-danger chances from the slot are visually larger than point shots.
  • Zone shading — A subtle density overlay highlights where each team's shots clustered. Red shading for the Devils, grey for the opponent.
  • Split rink — Devils shots appear on the right half, opponent shots on the left, mirrored so both teams shoot toward the outside edges.
  • Tooltips — Hover any dot to see the shooter name, shot type, xG value, and period.

Summary stats

Below the chart, a summary row shows shots on goal, total shot attempts (Corsi), and team xG for each side.

Technical details

  • Shot coordinates come from the NHL play-by-play API, which was already being fetched for xG and Corsi calculations. The raw x/y data is now preserved and normalized (goal always at x=89) before being written to latest-game.json.
  • Adds roughly 5–7 KB to the data file. The game cache is unaffected.
  • The rink is a responsive SVG that scales to any screen width.

January 2026 — DPI v2

This was a big overhaul. The original formula relied mostly on box-score stats — goals, assists, shots on goal, hits, blocks, plus/minus. It worked, but it was blunt. A player who fired 10 wide shots looked the same as someone with 2 dangerous chances.

The NHL's play-by-play feed opened up a lot more. I rebuilt the formula around that data.

New data sources

The biggest change is that DPI now pulls from play-by-play event data, not just the box score. That means individual Corsi (every shot attempt, not just shots on goal), expected goals based on shot location, penalties drawn, and the ability to filter everything for 5-on-5 play. Before this, there was no way to separate even-strength performance from power-play noise.

What was dropped

  • Shots on goal — Replaced by individual Corsi (iCF), which counts all shot attempts including blocks and misses. iCF is a better measure of shot volume because it captures intent, not just what got through.
  • Goals vs. league average — Replaced by goals vs. expected goals (xG). Comparing a player's goals to a league-wide average was too coarse. Comparing to xG tells you whether they're finishing above or below what their shot quality suggests.
  • Ice time — Was a standalone category. Now baked into per/60 normalization for hits, blocks, and takeaways, so ice time still matters but doesn't get double-counted.
  • Shifts — Removed entirely. It didn't add meaningful signal beyond what ice time already captured.

What was added

  • Individual Corsi (iCF) — +0.08 per attempt (x1.2 for defensemen). Measures total shot generation, rewarding players who create chances even if shots get blocked.
  • Expected goals (xG) — +1.0 per xG (sum of shot-location probabilities). Credits players for getting shots from dangerous areas.
  • Goals vs. xG — +2.0 per goal above expected. A finishing bonus for players who beat their expected output.
  • Penalties drawn — +0.7 per penalty drawn. Drawing penalties creates power plays, and the old formula didn't account for that at all.
  • 5-on-5 bonus — A composite of even-strength iCF, xG, takeaways, and giveaways. This rewards players whose underlying numbers hold up at 5v5, not just on the power play.

What was modified

  • Hits, blocks, takeaways — Now normalized to per-60-minute rates instead of raw counts. A fourth-liner with 8 minutes of ice time shouldn't get the same credit for 3 hits as a top-liner with 3 hits in 20 minutes.
  • Penalty weights — Reduced from -1.0 per PIM to -0.5. The old penalty was too harsh — a single roughing minor was wiping out the value of a full game's physical play.

Technical improvement

Added a persistent game cache so the fetch script doesn't re-download games it already has. This speeds up daily updates and reduces API calls.

Bar chart changes

The per-player bar charts were updated to reflect the new formula categories:

  • Added: Shot Quality (iCF + xG), Discipline (penalties drawn + faceoffs), Finishing (goals vs. xG, shown as negative when below expected), Turnovers (giveaways)
  • Removed: Ice Time, Goals vs. Avg, Shots