技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
xg-model-building【数据与研究】Builds expected goals (xG) models from NHL play-by-play shot event data using XGBoost or LightGBM. Use when user asks about expected goals, xG model, shot quality, building an xG model, xGF%, xGA, rebound detection, rush shot detection, or shot probability. Do not use for applying pre-built xG values to team or game analysis -- see team-analysis or h…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Important: PuckAPI does NOT have play-by-play data. The SDH database contains game-level data (scores, teams, odds, goalie starts) but no event-level shot data, coordinates, or play-by-play events.
Primary data source for xG: The NHL Stats API at
api-web.nhle.comprovides free play-by-play data with shot coordinates, event types, and strength state. No API key or credits required.PuckAPI is useful for: Validating your xG model output against team-level stats (
get_team_stats, 5 credits) and goalie stats (get_goalie_stats, 5 credits).For user's own shot data CSV/JSON: skip external sources, work with the file directly.
You are an expert in hockey expected goals modeling. Your goal is to build a shot-level xG model that estimates the probability any given shot results in a goal, controlling for shot quality rather than shot volume.
team-analysis or hockey-analyticsmodel-buildinggoalie-analysisfeature-engineeringThe NHL Stats API provides play-by-play event data for every game:
https://api-web.nhle.com/v1/gamecenter/{gameId}/play-by-play
Each play-by-play response includes shot events with:
This is the same data that MoneyPuck and Evolving Hockey use. No credits consumed.
SDH endpoints useful for validating your xG model output:
| Command | What It Does | Credits |
|---|---|---|
get_team_stats | Team-level shot and goal summaries (compare your xGF% to team performance) | 5 |
get_goalie_stats | Goalie save totals (for xGA validation) | 5 |
get_games | Game list to get game IDs for NHL API play-by-play pulls | 5 |
SDH does NOT provide: play-by-play events, shot coordinates, event types, shot-level data.
If user provides CSV/JSON:
x_coord, y_coord, shot_type, strength_state, period, game_seconds, event_type (SHOT/GOAL/MISS/BLOCK), shooter_id, goalie_id| Not Available in SDH | Where to Get It |
|---|---|
| Play-by-play events | NHL Stats API (api-web.nhle.com) -- free |
| Shot coordinates | NHL Stats API -- free |
| Shot event types | NHL Stats API -- free |
| Pre-computed xG values | Build the model yourself, or reference MoneyPuck/Evolving Hockey |
| Bulk shot coordinate export | Pull NHL API per game and aggregate |
Before building, establish:
Pull play-by-play from the NHL Stats API and filter to shot events:
SHOT (on goal, saved)GOALMISS (missed net)BLOCK (blocked before reaching goalie)Set target: is_goal = 1 if event_type is GOAL, else 0.
Important: Include blocked shots in the model but treat them carefully -- some public models exclude blocks (MoneyPuck excludes, Evolving Hockey includes). Document the choice.
See xg-features.md for the complete feature table with formulas. Core features:
Location:
shot_distance: Euclidean distance from shot coordinates to net centershot_angle: Absolute angle from net-center axisShot type: One-hot encode: wrist, slap, snap, backhand, tip, deflection, wrap-around. Backhand and tip/deflection have higher xG per distance than wrist shots.
Strength state: One-hot encode: 5v5, PP, SH, EN. Train separate models per strength state (Step 4).
Time context:
seconds_since_last_event: time elapsed since the prior eventdistance_from_last_event: Euclidean distance between shot coordinates and prior event coordinatesThese are the features that separate public xG models from naive distance-only models.
Rebound detection:
df['is_rebound'] = (
(df['seconds_since_last_event'] < 3) &
(df['last_event_type'].isin(['SHOT', 'GOAL', 'MISS']))
).astype(int)
Rush detection (zone entry proxy):
df['is_rush'] = (
(df['seconds_since_last_event'] < 4) &
(df['distance_from_last_event'] > 30) # feet -- large spatial jump = transition
).astype(int)
Shot angle change rate (MoneyPuck's "shotAnglePlusReboundSpeed"):
df['rebound_angle_speed'] = df['is_rebound'] * df['shot_angle_change'] / df['seconds_since_last_event'].clip(lower=0.1)
Train four separate XGBoost (or LightGBM) classifiers:
from xgboost import XGBClassifier
models = {}
for state in ['5v5', 'PP', 'SH', 'EN']:
subset = df[df['strength_state'] == state]
X = subset[FEATURE_COLS]
y = subset['is_goal']
models[state] = XGBClassifier(
n_estimators=300,
max_depth=4,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
eval_metric='logloss',
random_state=42
)
models[state].fit(X, y)
Shot-level evaluation:
Team-level validation:
# Sum shot-level xG per team per game -- compare to actual goals
df['xg'] = df.apply(lambda r: models[r['strength_state']].predict_proba([r[FEATURE_COLS]])[0][1], axis=1)
team_xg = df.groupby(['game_id', 'team'])['xg'].sum()
Compare xGF/xGA per team against public values from MoneyPuck or Evolving Hockey. Correlation should exceed 0.85.
You can also validate against SDH team stats: pull get_team_stats (5 credits) to compare your team-level xG aggregates against the xG values in the standings data.
# xGF% (5v5 only -- standard)
xg_5v5 = df[df['strength_state'] == '5v5'].groupby(['game_id', 'team'])['xg'].sum()
team_xgf_pct = xg_5v5 / (xg_5v5.groupby('game_id').transform('sum'))
xGF% is the primary team-level output. Values above 50% indicate a team that generates better shot quality than it allows at even strength.
| Operation | Credits | Notes |
|---|---|---|
| NHL Stats API play-by-play | 0 | Free -- no credits consumed |
| Full NHL regular season PBP | 0 | ~1,312 game API calls, all free |
get_team_stats (validation) | 5 | Compare your xGF% to SDH team stats |
get_goalie_stats (validation) | 5 | Compare your xGA to SDH goalie stats |
get_games (get game IDs) | 5 | Pull game list, then use IDs for NHL API |
| Rationalization | Why It's Wrong | Do This Instead |
|---|---|---|
| "One model for all strength states is simpler" | PP and SH have fundamentally different shot distributions; a combined model learns the average and is wrong for both | Train separate models per strength state |
| "Distance alone is good enough" | Distance-only models miss rebound and rush context, which account for ~15% of xG variance | Include rebound flag and rush flag at minimum |
| "k-fold cross-validation is fine for shot data" | Shots within a game are correlated; k-fold leaks game-level context across folds | Group k-fold by game_id or season |
| "Blocked shots should be included the same as saved shots" | Blocked shots are a different event with different outcome distribution; mixing them without a flag distorts calibration | Either exclude blocks or include a is_blocked indicator |
| "I'll validate against total goals only" | Total goals validation misses calibration problems at the shot level | Validate Brier score at shot level AND team-level xGF% correlation |
| "Screen/traffic data isn't available so skip it" | Acknowledging the gap is correct -- just document it as a known model limitation | Note it as a known gap; NHL EDGE tracking may expose it in future data |
| "I'll use PuckAPI get_game_detail for shot data" | SDH has game-level data only (scores, odds, goalies) -- no play-by-play or shot coordinates | Use the NHL Stats API for play-by-play data (free) |
The xG model produces:
game_id, event_id, shooter_id, team, strength_state, xg (float 0-1), and all computed features.xGF and xGA per team per game per strength state.team-analysis or feature-engineering..pkl or .json), feature names, and Brier score per strength state.| What You Found | Next Action | Skill |
|---|---|---|
| xG model built, want to use it as a prediction model feature | Add xGF% as a lag feature | feature-engineering |
| Team xGF% computed, want to analyze team quality | Use xGF% alongside Corsi and PDO | team-analysis |
| Goalie xGA computed, want to evaluate goalie quality | xGA is the input to GSAA calculation | goalie-analysis |
| Model is built, want to find betting edges using xG | Compare xG-implied win probability vs market odds | edge-detection |
| Want to validate the model with proper temporal splits | Hold out full seasons for out-of-sample testing | walk-forward-validation |
| Calibration curve is off | Recalibrate probabilities with isotonic regression or Platt scaling | probability-calibration |