界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
数据与研究 · SKILL.md

xg-model-building

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 快照 · 自动分类 · 以原作者文档为准

技能说明

GitHub

name: xg-model-building description: "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 hockey-analytics. Do not use for general game prediction models -- see model-building. Do not use for goalie evaluation using xGA -- see goalie-analysis." metadata: version: 1.1.0 author: PuckAPI

xG Model Building

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.com provides 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.

When to Use

  • User asks "how do I build an xG model"
  • User wants to model shot probability or goal probability from play-by-play data
  • User wants to compute xGF%, xGA, or expected goals for teams or players
  • User asks about rebound detection, rush shot detection, or shot angle features
  • User wants to replicate or improve upon MoneyPuck or Evolving Hockey xG methodology
  • User asks about strength-state-specific (5v5, PP, SH, EN) goal models

When NOT to Use

  • Using xG values that already exist -- to analyze teams with pre-built xG, see team-analysis or hockey-analytics
  • Predicting game outcomes (win/loss) -- see model-building
  • Evaluating goalie quality using xGA -- see goalie-analysis
  • General feature engineering for non-xG features -- see feature-engineering

Data Sources

NHL Stats API (primary, free)

The 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:

  • Event type (SHOT, GOAL, MISS, BLOCK)
  • x/y coordinates (NHL coordinate system, feet, center ice = 0,0)
  • Shot type (wrist, slap, snap, backhand, tip, deflection, wrap-around)
  • Strength state (5v5, PP, SH, EN)
  • Period and game time
  • Shooter and goalie IDs

This is the same data that MoneyPuck and Evolving Hockey use. No credits consumed.

PuckAPI (validation only)

SDH endpoints useful for validating your xG model output:

CommandWhat It DoesCredits
get_team_statsTeam-level shot and goal summaries (compare your xGF% to team performance)5
get_goalie_statsGoalie save totals (for xGA validation)5
get_gamesGame list to get game IDs for NHL API play-by-play pulls5

SDH does NOT provide: play-by-play events, shot coordinates, event types, shot-level data.

Your Own Data

If user provides CSV/JSON:

  1. Verify required columns: x_coord, y_coord, shot_type, strength_state, period, game_seconds, event_type (SHOT/GOAL/MISS/BLOCK), shooter_id, goalie_id
  2. Verify coordinate system is consistent (NHL uses feet, center ice = 0,0)
  3. Flag missing coordinates -- do not silently drop
  4. No credits consumed

Commands That Do NOT Exist (in PuckAPI)

Not Available in SDHWhere to Get It
Play-by-play eventsNHL Stats API (api-web.nhle.com) -- free
Shot coordinatesNHL Stats API -- free
Shot event typesNHL Stats API -- free
Pre-computed xG valuesBuild the model yourself, or reference MoneyPuck/Evolving Hockey
Bulk shot coordinate exportPull NHL API per game and aggregate

Initial Assessment

Before building, establish:

  1. What is the data source? (NHL Stats API free, or user's own data)
  2. How many seasons? (1 season minimum; 3+ seasons produces more stable coefficients)
  3. What is the end use? (team-level xGF%, player-level scoring, goalie evaluation -- determines how to aggregate shot-level predictions)

How It Works

Step 1: Collect Shot Events

Pull play-by-play from the NHL Stats API and filter to shot events:

  • SHOT (on goal, saved)
  • GOAL
  • MISS (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.

Step 2: Compute Core Features

See xg-features.md for the complete feature table with formulas. Core features:

Location:

  • shot_distance: Euclidean distance from shot coordinates to net center
  • shot_angle: Absolute angle from net-center axis

Shot 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 event
  • distance_from_last_event: Euclidean distance between shot coordinates and prior event coordinates

Step 3: Derive Rebound and Rush Features

These 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)

Step 4: Train Separate Models Per Strength State

Train four separate XGBoost (or LightGBM) classifiers:

  1. 5v5 (largest sample, most stable)
  2. PP (power play -- angle matters more, distance less because shots cluster high slot)
  3. SH (shorthanded -- small sample, may need regularization)
  4. EN (empty net -- high conversion; EN shots are nearly deterministic by distance)
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)

Step 5: Evaluate the Model

Shot-level evaluation:

  • Brier score: lower is better. A distance-only logistic baseline scores ~0.055. A good xG model scores ~0.048-0.050.
  • Calibration curve: plot predicted xG decile vs actual goal rate. A well-calibrated model's curve should lie on the diagonal.
  • Log loss: compare to public benchmarks.

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.

Step 6: Aggregate to Team Level

# 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.

Season Resolution

  • October through December: current calendar year is the season start (e.g., 2026-27 season)
  • January through September: previous calendar year is the season start (e.g., 2025-26 season)
  • "This season" = season currently in progress or most recently completed
  • NHL regular season: October to April. Playoffs: April to June.
  • Play-by-play is available within hours of game completion.

Credit Usage

OperationCreditsNotes
NHL Stats API play-by-play0Free -- no credits consumed
Full NHL regular season PBP0~1,312 game API calls, all free
get_team_stats (validation)5Compare your xGF% to SDH team stats
get_goalie_stats (validation)5Compare your xGA to SDH goalie stats
get_games (get game IDs)5Pull game list, then use IDs for NHL API

Anti-patterns

RationalizationWhy It's WrongDo 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 bothTrain separate models per strength state
"Distance alone is good enough"Distance-only models miss rebound and rush context, which account for ~15% of xG varianceInclude 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 foldsGroup 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 calibrationEither exclude blocks or include a is_blocked indicator
"I'll validate against total goals only"Total goals validation misses calibration problems at the shot levelValidate 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 limitationNote 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 coordinatesUse the NHL Stats API for play-by-play data (free)

Output Format

The xG model produces:

  1. Shot-level xG values: DataFrame with game_id, event_id, shooter_id, team, strength_state, xg (float 0-1), and all computed features.
  2. Team-game xG aggregates: xGF and xGA per team per game per strength state.
  3. Season xGF% (5v5): one value per team, used as input to team-analysis or feature-engineering.
  4. Model artifacts: serialized XGBoost models per strength state (.pkl or .json), feature names, and Brier score per strength state.
  5. Calibration report: predicted vs actual goal rate by decile for each strength state model.

What to Do Next

What You FoundNext ActionSkill
xG model built, want to use it as a prediction model featureAdd xGF% as a lag featurefeature-engineering
Team xGF% computed, want to analyze team qualityUse xGF% alongside Corsi and PDOteam-analysis
Goalie xGA computed, want to evaluate goalie qualityxGA is the input to GSAA calculationgoalie-analysis
Model is built, want to find betting edges using xGCompare xG-implied win probability vs market oddsedge-detection
Want to validate the model with proper temporal splitsHold out full seasons for out-of-sample testingwalk-forward-validation
Calibration curve is offRecalibrate probabilities with isotonic regression or Platt scalingprobability-calibration
我的收藏