About the Numbers
How this site calculates Expected Goals (xG) and goals above expected, Corsi/Fenwick shot attempts, era-adjusted GF+/GA+, PDO and its sustainable rate, and Pythagorean expected points for the Sabres, plus a glossary of the hockey analytics terms fans run into most.
Buffalo Goal is an independent fan project, built for fun and the love of Buffalo hockey. It isn't affiliated with, endorsed by, or connected to the NHL, the Buffalo Sabres, or any team. The numbers here are an unofficial, best-effort estimate for fun, not a statistic from the league or the team.
The short version
Not every shot is equally likely to score. A point-shot through traffic and a wide-open one-timer from the slot are very different scoring chances, even though both count as one shot on goal. Expected Goals (xG) assigns each shot a probability, somewhere between 0 and 1, of becoming a goal, based on where it came from and the situation it was taken in. Add up every shot's xG over a game and you get a measure of shot quality, not just shot quantity.
How this site calculates it
Every unblocked shot attempt (a shot on goal, a missed shot, or a goal) is scored by a gradient-boosted model (LightGBM) built from roughly 360,000 shots of NHL play-by-play data across three seasons, 2023-24 through 2025-26: regular season and playoffs, league-wide, not just Sabres games. Three recent seasons ranked shots better in testing than a ten-season history did, so the older seasons were left out. A league-wide dataset is necessary for the model to learn what actually separates a good scoring chance from a low-danger one. Separate curves per situation (even strength, power play, shorthanded, and empty net, the last split further by distance) map the model's raw output onto the actual rate those shots go in, with an extra curve for redirects and tip-ins, which the model over-rates by about a tenth. Those curves are fit by cross-validation across the three seasons, so each shot's curve comes from a fit that never saw it, and they are re-fit each month on recent games so a game's xG stays anchored to how often shots are really going in this season.
Accuracy is measured honestly: every shot used to check the model, and to fit its probability curves, is scored by a fit that never saw that shot, so the check isn't graded on its own homework. Distance and angle are also constrained so that, all else equal, a shot's xG can never go up as it moves farther from the net or to a worse angle.
The model does not know who took the shot or who was in net. That's deliberate: the goal is to measure the danger of the chance itself, for an average shooter against an average goalie, so the number can be used to judge shooters and goalies rather than bake their skill in.
What goes into a shot's xG
- Distance and angle to the goal, and the shot's raw location on the ice.
- Shot type: wrist, snap, slap, backhand, tip-in, deflection, wrap-around.
- Strength: even strength, power play, or shorthanded (as a skater-count difference), plus how many seconds have passed since the power play began. A penalty kill is at its most vulnerable right after the whistle, before it sets up.
- Rebounds: whether the shot came within three seconds of a prior shot by the same team, and how sharply the shooting angle swung from that shot. A loose puck the goalie has to slide across for is far more dangerous than a second look from the same spot after the defense resets.
- Pace: how many seconds since the previous event, and how far the puck traveled in that time. A quick shot off a bouncing puck near the crease plays very differently from the next clean look twenty seconds later, and a “shot” logged a moment after an event forty feet away is usually a recording artifact that never really threatened.
- Cross-slot movement: whether the puck crossed the middle of the ice in the seconds before the shot. A pass that pulls the goalie from one post to the other is about the closest the public feed comes to seeing a set-up pass.
- Off wing: whether the shooter was on the side of the ice away from their handedness, set up to one-time a pass rather than having to settle the puck first.
- Preceding event: what kind of play came right before the shot (a faceoff, a hit, a giveaway, a takeaway, or another shot), and where on the ice it happened.
- Fatigue: how far into their shift the shooter was, and how long the skaters defending had been on the ice, taken from the NHL's shift charts. A defender late in a long shift gives up a better look than a fresh one.
- Score state: whether the shooting team was leading or trailing, and by how much. Trailing teams take slightly lower-quality shots on average.
- A few smaller signals: home ice, and how far into the game the shot came.
What's left out
- Blocked shots are excluded. The NHL records the location where the shot was blocked, not where the shooter released it, so their distance and angle can't be trusted.
- Empty-net attempts are scored by the same model, which knows the net is empty and scales the danger down with distance: a tap-in into an empty cage is near-certain, a clear from the far end of the ice much less so. That value counts toward the total.
- Shootouts are a separate skills competition, not the game situation the model was trained on, so shootout attempts are excluded entirely.
- Games before the 2016-17 season don't have xG available. That's where reliable shot coordinates in the NHL's public feed start.
Known limitations
The NHL's public play-by-play feed doesn't include player-tracking data, so the model has no way to see a screen in front of the goalie, defensive pressure, traffic, or a cross-ice pass right before the shot. A wide-open snap shot and a heavily screened one from the same spot on the ice look identical to it. It can see how long the shooter and the defenders had been on the ice, but not what they were doing out there. It also has no read on lineups or which goalie is in net, by design.
Deflections and tip-ins are a specific blind spot: the feed logs them as a single event, with no record of the pass or point shot that was redirected, so the model has to lean on the shot type and location alone. It over-rates them by about a tenth, so redirects get their own calibration curve to pull the totals back in line, but a single redirect's xG is still among the shakier numbers here.
The model is checked by cross-validation across the three seasons it was built on, with every shot graded by a fit that never saw it. Sorted by predicted value, each group scores close to the rate the model assigned it, from the low single digits up through the 30 percent range. Because the situation curves are re-fit each month against the current season, a team's season xG stays within a couple of percent of the goals actually scored; on a fresh season, before that re-fit, it can run a few percent high. It is a better guide to season-long play than to a single game. The rougher spots: power-play shot ranking (with no pre-shot passing in the feed, the model can't tell a wide-open one-timer from a point shot) and shots from beyond about 60 feet, which still read a little high. Treat xG as a useful estimate of shot quality, not a precise or official one.
The numbers here will not match MoneyPuck, Natural Stat Trick, Evolving-Hockey, or the other established public xG models, and often not by a small amount. Every model makes its own choices about which shots to count, what goes into the estimate, and how it is calibrated, so no two public xG figures line up exactly. This one is an independent, best-effort attempt at building a model from the same public play-by-play data, worked out from scratch rather than copied from any of them. It is not meant to replace the bigger public models, which have had far more scrutiny. Use whichever you trust, but do not expect the totals to agree.
Data source
Play-by-play data comes from the NHL's public API, api-web.nhle.com, the same feed that powers live scores elsewhere on this site.
Sabres xG in context
xG is most useful as a comparison, not a single number in isolation. Buffalo's season-by-season xG table shows team-level shot quality for and against back to 2016-17, so you can see whether a given Sabres season's results lined up with the underlying shot quality, or diverged from it (a team can out-chance opponents and still lose, or the reverse). Every individual game page from that point on breaks the same thing down shot by shot, with a running xG chart alongside the actual score.
Because the model only covers 2016-17 onward, it can't say anything about how, say, the 1990s Sabres would grade out by today's shot-quality standards. That's simply outside what the NHL's public play-by-play feed provides for older seasons. Everything before 2016-17 on this site (records, standings, player stats, head-to-head history) is still fully tracked; xG specifically just isn't one of those things for the older eras.
Goals above and below expected
Once every shot has an xG, two follow-on numbers fall out. For a skater, GAx (goals above expected) is their actual goals minus the total xG of their shots. A player with 20 goals on shots worth 14 xG has a GAx of +6: they finished six more than an average shooter would have from those same chances, whether through skill, a hot stretch, or plain puck luck. For a goalie, GSAx (goals saved above expected) flips it, the total xG of every shot faced minus the goals actually allowed, so a positive number means stopping more than the shots on average should have gone in.
Both appear on the skater and goalie tables on Team Stats, on player pages, and behind the finishing-luck band on the Goals vs xG scatter. The same caveat as xG itself applies, only more so: one season of GAx is still a smallish sample, and a good share of any player's number is noise that won't repeat.
The short version
Before xG existed, analysts needed a cheap stand-in for puck possession: count every shot attempt (a shot on goal, a miss, or a blocked shot) for each team while a specific group of skaters is on the ice. That count is Corsi. Drop the blocked shots and count only shots on goal plus misses, and it's called Fenwick instead. Neither knows anything about shot quality, the way xG does. What they're good at is volume: who actually had the puck and was doing something with it.
How this site calculates it
Most sites only report Corsi/Fenwick at the team or individual-player level. This site goes one step further and attributes every 5-on-5 shot attempt to the specific forward line or defense pair that was actually on the ice for it, reconstructed from the NHL's public shift-chart data (who was on the ice, and when) intersected with the play-by-play (what happened, and under what game state). That's what powers the Line Combinations table on every game page and on Team Stats, sortable by CF%, CF% Rel (a combo's CF% compared to the team's own CF% when that combo was off the ice), xG%, and more, with an adjustable minimum-ice-time filter so a real, trusted sample can be told apart from a couple of noisy shifts.
Independently checked against another publicly available source of Corsi/xG numbers for the same players and lines: on-ice time matched within a minute or two in every case tested, and CF% matched within a couple of tenths of a point.
Which shot count is which
Three different “shots” numbers turn up around the site, and they aren't supposed to agree with each other:
| Count | What it includes | Where you see it |
|---|---|---|
| Shots on goal | Shots that hit the net or scored. The official NHL count, all situations, whole game. | The box-score line on a game page; season shot totals. |
| Shot attempts (Corsi) | Shots on goal plus missed shots plus blocked shots, counted at 5-on-5. | A game page's “5v5 Game Control” and Line Combinations CF%; the Game Flow chart. |
| Unblocked attempts (Fenwick) | Shots on goal plus missed shots, at 5-on-5. Blocks removed. | The Game Flow chart's “Unblocked” toggle; FF% columns. |
So a game where Buffalo is out-shot on goal but wins the shot-attempt share just means a lot of its tries were blocked or missed. xG is scored per shot on goal and summed, a narrower set still than the attempts CF% counts.
Known limitations
Shift-chart data isn't available for every season this site otherwise covers, so line-level Corsi/ Fenwick is a modern-era feature, not something tracked back through Sabres history the way box-score totals are. A line/pair's xG% specifically can still swing more than its CF% does on a small sample: this site's own xG model won't agree shot-for-shot with any other site's, so the two can genuinely rank a line differently even when their ice-time totals match closely.
Score and venue adjustment
Raw attempts carry the score. A trailing team shoots more and a leading team sits back, so over one game the raw share flatters whoever was losing: across 12,282 regular-season games league-wide, a team down a goal in the third period takes about 57% of the 5-on-5 attempts and a team up two takes about 42%. The site's own Win DNA study found the consequence: out-attempting the opponent over a single game came with a lower Sabres win rate, and only the share while tied ran the other way. Expected goals are already protected (the xG model reads the score state per shot), so the attempt counts are the exposed number.
The fix weights each attempt by its situation. For every regular-season game from 2016-17 to 2025-26 (shootout excluded), each attempt is filed by the shooting team's goal differential at that moment (a goal's own attempt in the state before it), whether the shooter was at home or away, and the period. The share of attempts the teams in that cell took against their opponents in the mirror cell is measured, and an attempt in the cell counts for 0.5 divided by that share. A team leading by one at home in the third takes about 44% of the attempts, so each of those counts for a little over one; a team trailing by one on the road in the third takes about 58%, so each of those counts for a little under 0.9. The pooled 5-on-5 third-period table:
| Shooting team is | Share at home | Share away | Weight, home | Weight, away |
|---|---|---|---|---|
| Down 3 or more | 58.6% | 56.0% | 0.853 | 0.893 |
| Down 2 | 58.8% | 56.7% | 0.851 | 0.881 |
| Down 1 | 58.3% | 55.7% | 0.858 | 0.897 |
| Tied | 51.2% | 48.8% | 0.976 | 1.026 |
| Up 1 | 44.3% | 41.7% | 1.129 | 1.198 |
| Up 2 | 43.3% | 41.2% | 1.156 | 1.213 |
| Up 3 or more | 44.0% | 41.4% | 1.136 | 1.209 |
Shot attempts (Corsi) at 5-on-5, third period, 2016-17 to 2025-26 regular seasons, 12,282 games. The first and second periods carry the same shape at about half the size, and the effect is stable: season by season, the share for a team down one at 5-on-5 (all periods, both venues) stayed between 51.8% and 53.9%, with the low end in 2020-21 (empty arenas, and the home-ice term shrank to nothing that season too). The site uses the pooled table for every game, including games older than the measured seasons.
The adjusted share is a counterfactual, an estimate of what the share would have been in a tied game at a neutral site, and it is shown as a companion to the raw share, never in its place. It runs whichever way the game pushed: a team that led all night comes out higher than its raw share, a team that chased all night lower. It says nothing about which team deserved the result.
GF+ and GA+ (adjusting for the era)
Four goals a game in 1982 and four goals a game in 2004 are not the same accomplishment. League-wide scoring has swung from over eight goals per game in the early 1980s down under five and a half in the dead-puck years, and back up to about 6.2 today. GF+ rebaselines a team's goals for against the league average of its own season, with 100 set to average: a GF+ of 115 means the team scored 15% more than a league-average team did that year, 90 means 10% fewer. GA+ does the same for goals against, inverted so that a higher number is still better defense.
The math is just (team goals per game / league goals per game that season) x 100, against a fixed table of league scoring rates back to 1970-71. It's the hockey version of baseball's OPS+ or ERA+. Available as a stat on the playground's Season Trend and Compare Eras views, and on the Franchise History trend chart.
What is PDO?
A team's shooting percentage plus save percentage at 5-on-5, the two situational numbers added together so a perfectly break-even team reads as 1.000 (some sources scale this to 100 instead, describing the same thing). Shooting and save percentage both tend to even out for a team over a long enough stretch, so a team running well above 1.000 is generally getting some bounces it won't keep getting, and a team well below it is due for some better luck, more than either reflects real, repeatable team strength. It's a “regression” flag, not a measure of how good a team actually is.
Scoped to 5-on-5 only, not the whole game, since that's the situation PDO is meant to describe. Special-teams goals lean heavily on the shooting team's actual power-play or penalty-kill execution, not puck luck, and mixing them in would blur the signal.
Pulled directly from the NHL's own situational team stats, which only go back to the 2009-10 season. The league didn't track 5-on-5-specific splits at all before then, so PDO reads as unavailable (“–”) on this site for anything older. Available by season on Franchise History and Team Stats (chartable over time on Franchise History's trend chart), for a single game on any game page (computed live there from that game's own play-by-play, since the NHL's bulk situational report is season-level only), and on the home page's Buffalo History card.
Reading a PDO: the sustainable rate
Because PDO pulls so hard toward 1.000, a team's number partway through a season says little about the rest of it. A PDO far enough from 1.000 gets a projection: the observed figure regressed toward 1.000 by the amount the sample size supports, which the Team Stats summary prints beside the season's own figure, and beneath the snapshot on the home page with a rough count of how many standings points the run of luck has been worth so far.
The pull toward average is empirical-Bayes shrinkage. The observed PDO's standard error is about 0.075 / sqrt(games played); that's weighed against an estimated spread of real team-talent PDO of about 0.004, and the deviation from 1.000 is kept in proportion talent-variance / (talent-variance + error-variance). Early in a season that keeps almost none of the gap; even a full 82 games keeps only about a fifth of it. The points figure turns the raw gap into goals (roughly gap x 26 five-on-five shots per game x games) and then goals into points (about three goals per standings point), rounded hard so it never looks more precise than it is.
Pythagorean expectation
A team's record tends to track its goal differential, and the Pythagorean formula makes that precise: win% = GF^2 / (GF^2 + GA^2), then expected points = win% x 2 x games played. It shows as a projected point pace next to the actual total under the season snapshot on the home page, and as the xPTS column on Standings by Year. A team finishing well above its Pythagorean pace won more one-goal games and shootouts than its overall goal margin would suggest, the kind of edge that doesn't carry cleanly from one season to the next.
This site uses the classic goal-based version. An xG-based Pythagorean is on the list for later.
Hockey analytics glossary
A quick reference for the terms fans run into when reading about hockey analytics, and which of them this site actually tracks for the Sabres.
The probability, from 0 to 1, that a given shot becomes a goal, based on where and how it was taken. Summed across a game, it measures shot quality rather than raw shot volume.
Tracked for every Sabres game from 2016-17 on. See the season-by-season xG table or any individual game page.
The share of all shot attempts (goals, shots on goal, missed shots, and blocked shots) a team generates at a given game state, usually 5-on-5. A rough, easy-to-compute proxy for puck possession.
Tracked for every Sabres forward line and defense pair, not just the team as a whole. See the Shot Attempts section below, or jump straight to any game page's or the Team Stats Line Combinations table.
The same idea as Corsi, but excluding blocked shots. Just goals, shots on goal, and missed shots.
Tracked alongside Corsi in the same Shot Attempts section and Line Combinations tables, toggleable as its own column.
Shot-attempt share with every attempt weighted by how often teams in that score state, home or away, at that point of the game, take the attempts league-wide. An estimate of what the share would have been in a tied game at a neutral site, shown next to the raw share rather than instead of it.
Under the shot-attempt share on a game page's Game Control strip and as the Game Flow chart's Score-adjusted view. See the Score and venue adjustment section.
A skater's actual goals minus the total xG of their shots. Positive means finishing more than an average shooter would have from those chances.
See the Goals above expected section. On skater tables on Team Stats, on player pages, and behind the finishing-luck band on the Goals vs xG scatter.
The total xG of every shot a goalie faced minus the goals they allowed. Positive means stopping more than the shots on average should have gone in.
On the goalie tables on Team Stats and every goalie's player page.
A shot attempt worth 0.12 xG or more. One threshold sitewide, so a count means the same thing wherever it appears; the scope beside it (5-on-5, power play, all situations) is what changes.
The HDCF-HDCA column on Scores, Game Control and Stats by Period on every game and series page, and the shot-map filter.
A team's shooting percentage plus save percentage at 5-on-5, which this site prints on the 1.000 scale a break-even team reads at (some sources scale the same figure to 100, or to 1000). Treated as a rough "puck luck" indicator, since it tends to regress toward that baseline over a large enough sample.
See the PDO section above for this site's exact methodology and where it's tracked.
A projected point total from goal differential alone: win% = GF² / (GF² + GA²), times 2 points times games played. The gap to a team's actual points is close-game and shootout luck.
Shown as a projected point pace under the season snapshot on the home page. See the Pythagorean expectation section.
Goals for (or against) indexed to the league average of that same season, with 100 set to average. Adjusts for how much scoring levels differ across eras, like baseball's OPS+ or ERA+.
See the Era Adjustment section. Chartable on the playground and Franchise History.
A goalie's goals allowed per 60 minutes played.
Tracked for every Sabres goalie, every season. See any goalie's player page or the Team Stats goalie table.
The share of shots on goal a goalie stops.
Tracked alongside GAA on every goalie's player page, plus a team-wide version on the Team Stats page.
Power play percentage (goals scored per power play opportunity) and penalty kill percentage (opponent power plays successfully defended).
Tracked by season on Franchise History and Team Stats, chartable over time on Franchise History's trend chart.