{ "cells": [ { "cell_type": "markdown", "id": "28856ae2", "metadata": {}, "source": [ "# πŸ’ NHL hockey with `sportsdataverse-py`\n", "\n", "Welcome to the show! πŸŽ‰ `sportsdataverse.nhl` gives you the **NHL's own modern\n", "feed** β€” the same `api-web.nhle.com` data that powers NHL.com β€” plus the shiny\n", "**NHL EDGE** puck-and-player tracking layer, the `api.nhle.com` stats-REST and\n", "records flat APIs, an ESPN fallback, and fast parquet loaders. All of it hands\n", "you tidy **polars** DataFrames, ready to model. πŸš€\n", "\n", "We'll **lead with the premium native wrappers** (the `nhl_*` and `nhl_edge_*`\n", "functions) β€” they're the league's first-party data, no key required β€” and keep\n", "ESPN (`espn_nhl_*`) as a friendly secondary path.\n", "\n", "R user? The companion package is\n", "[fastRhockey](https://fastRhockey.sportsdataverse.org) (NHL + PWHL). Let's drop\n", "the puck! πŸ’" ] }, { "cell_type": "markdown", "id": "25f19861", "metadata": {}, "source": [ "## 🧰 The toolbox\n", "\n", "Every native call returns a tidy **polars** `DataFrame` by default β€” pass\n", "`return_as_pandas=True` for pandas, or `return_parsed=False` for the raw JSON.\n", "Here's the kit we'll use (click any name for the full reference). The ⭐ rows\n", "are the **premium native NHL feed** β€” start there.\n", "\n", "| Function | What it gives you | Source |\n", "|---|---|---|\n", "| [`nhl_web_schedule`](../nhl/reference/nhl_api_web.md#nhl_web_schedule) | A day's games + scores, native `id`s | ⭐ NHL api-web |\n", "| [`nhl_web_pbp`](../nhl/reference/nhl_api_web.md#nhl_web_pbp) | Event-level play-by-play (one row per event) | ⭐ NHL api-web |\n", "| [`nhl_boxscore`](../nhl/reference/nhl_api_web.md#nhl_boxscore) | One row per player (skaters + goalies) | ⭐ NHL api-web |\n", "| [`nhl_standings`](../nhl/reference/nhl_api_web.md#nhl_standings) | Team standings with conference/division | ⭐ NHL api-web |\n", "| [`nhl_roster`](../nhl/reference/nhl_api_web.md#nhl_roster) | A club's roster for a season | ⭐ NHL api-web |\n", "| [`nhl_club_schedule_season`](../nhl/reference/nhl_api_web.md#nhl_club_schedule_season) | A team's full-season schedule | ⭐ NHL api-web |\n", "| [`nhl_player_game_log`](../nhl/reference/nhl_api_web.md#nhl_player_game_log) | A player's game-by-game line | ⭐ NHL api-web |\n", "| [`nhl_player_landing`](../nhl/reference/nhl_api_web.md#nhl_player_landing) | A player's bio + career snapshot | ⭐ NHL api-web |\n", "| [`nhl_skater_leaders`](../nhl/reference/nhl_api_web.md#nhl_skater_leaders) | Season skater leaderboard | ⭐ NHL api-web |\n", "| [`nhl_goalie_leaders`](../nhl/reference/nhl_api_web.md#nhl_goalie_leaders) | Season goalie leaderboard | ⭐ NHL api-web |\n", "| [`nhl_club_stats`](../nhl/reference/nhl_api_web.md#nhl_club_stats) | A club's full skater + goalie stat lines | ⭐ NHL api-web |\n", "| [`nhl_player_landing`](../nhl/reference/nhl_api_web.md#nhl_player_landing) | A player's bio + career snapshot | ⭐ NHL api-web |\n", "| [`nhl_score`](../nhl/reference/nhl_api_web.md#nhl_score) | A day's final scores + series context | ⭐ NHL api-web |\n", "| [`nhl_draft_picks`](../nhl/reference/nhl_api_web.md#nhl_draft_picks) | Draft board for a year/round | ⭐ NHL api-web |\n", "| [`nhl_edge_skater_skating_speed_detail`](../nhl/reference/nhl_edge.md#nhl_edge_skater_skating_speed_detail) | A skater's tracked speed vs league avg + percentile | ⭐ NHL EDGE |\n", "| [`nhl_edge_skater_landing`](../nhl/reference/nhl_edge.md#nhl_edge_skater_landing) | EDGE skater leaderboards (hardest shot, top speed…) | ⭐ NHL EDGE |\n", "| [`nhl_edge_team_landing`](../nhl/reference/nhl_edge.md#nhl_edge_team_landing) | EDGE team-level tracking leaders | ⭐ NHL EDGE |\n", "| [`nhl_edge_goalie_landing`](../nhl/reference/nhl_edge.md#nhl_edge_goalie_landing) | EDGE goalie tracking leaders | ⭐ NHL EDGE |\n", "| [`nhl_stats_rest_leaders_skaters`](../nhl/reference/nhl_stats_rest.md#nhl_stats_rest_leaders_skaters) | Stats-REST top-10 skaters by attribute | ⭐ NHL stats-REST |\n", "| [`nhl_stats_rest_leaders_goalies`](../nhl/reference/nhl_stats_rest.md#nhl_stats_rest_leaders_goalies) | Stats-REST top-10 goalies by attribute | ⭐ NHL stats-REST |\n", "| [`nhl_records_franchises`](../nhl/reference/nhl_records.md#nhl_records_franchises) | Every franchise in NHL history (Records API) | ⭐ NHL records |\n", "| [`nhl_records_franchise_team_totals`](../nhl/reference/nhl_records.md#nhl_records_franchise_team_totals) | All-time W/L/points per franchise | ⭐ NHL records |\n", "| [`load_nhl_schedule`](../nhl/reference/loaders.md#load_nhl_schedule) | Pre-built schedule parquet (offline-friendly) | πŸ“¦ loader |\n", "| [`load_nhl_team_box`](../nhl/reference/additional.md#load_nhl_team_box) | Pre-built team box parquet | πŸ“¦ loader |\n", "| [`load_nhl_player_box`](../nhl/reference/additional.md#load_nhl_player_box) | Pre-built player box parquet | πŸ“¦ loader |\n", "| [`espn_nhl_teams`](../nhl/reference/additional.md#espn_nhl_teams) | ESPN team directory | ESPN |\n", "| [`espn_nhl_schedule`](../nhl/reference/additional.md#espn_nhl_schedule) | ESPN schedule for a date | ESPN |\n", "| [`espn_nhl_pbp`](../nhl/reference/additional.md#espn_nhl_pbp) | ESPN play-by-play (a dict) | ESPN |\n", "| [`espn_nhl_standings`](../nhl/reference/site.md#espn_nhl_standings) | ESPN standings | ESPN |\n" ] }, { "cell_type": "markdown", "id": "a5f4bd52", "metadata": {}, "source": [ "## πŸ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "No API key needed β€” the NHL's public feeds ship ready to go. 😊" ] }, { "cell_type": "code", "execution_count": null, "id": "019cfdbf", "metadata": {}, "outputs": [], "source": [ "import polars as pl\n", "import sportsdataverse as sdv\n", "import sportsdataverse.nhl as nhl" ] }, { "cell_type": "markdown", "id": "d7ca1a44", "metadata": {}, "source": [ "The native feeds are live and seasonal (and occasionally throttle), so a tiny\n", "`safe()` helper runs each network call defensively β€” you get the frame when the\n", "feed is up, and a friendly one-liner when it isn't (never a scary traceback). πŸ›Ÿ\n", "\n", "We'll reference the **2024 Stanley Cup Final Game 7** throughout: Florida\n", "Panthers 2, Edmonton Oilers 1 (June 24, 2024). Note the native game id\n", "`2023030417` (season + game-type + sequence) is **different** from ESPN's\n", "`401675111` for the very same game." ] }, { "cell_type": "code", "execution_count": null, "id": "2f568259", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " try:\n", " out = thunk()\n", " print(f'βœ… {label}')\n", " return out\n", " except (NoDataError, AssetFetchError) as e:\n", " print(f\"\\u23ed\\ufe0f {label}: {type(e).__name__}: {e}\")\n", " return None\n", "\n", "\n", "# Game 7, 2024 Stanley Cup Final β€” two ids for the same game\n", "NATIVE_GAME = 2023030417 # api-web.nhle.com\n", "ESPN_GAME = 401675111 # ESPN\n", "SEASON = 20232024 # NHL season strings are start+end years" ] }, { "cell_type": "markdown", "id": "104790e7", "metadata": {}, "source": [ "## ⭐ The premium native feed (`nhl_*`)\n", "\n", "These wrappers hit the league's own `api-web.nhle.com`. They're first-party,\n", "richly detailed, and return polars directly. Let's tour the headline calls.\n", "\n", "### πŸ“… Schedule\n", "\n", "[`nhl_web_schedule(date='YYYY-MM-DD')`](../nhl/reference/nhl_api_web.md#nhl_web_schedule)\n", "returns a day's games with `home_team_*` / `away_team_*` columns and the native `id`." ] }, { "cell_type": "code", "execution_count": null, "id": "ceb6b4e6", "metadata": {}, "outputs": [], "source": [ "sched = safe('native schedule', lambda: nhl.nhl_web_schedule(date='2024-06-24'))\n", "cols = ['id', 'game_state', 'home_team_abbrev', 'home_team_score',\n", " 'away_team_abbrev', 'away_team_score']\n", "(sched.select([c for c in cols if c in sched.columns]).head()\n", " if sched is not None else 'schedule unavailable')" ] }, { "cell_type": "markdown", "id": "b3b19a09", "metadata": {}, "source": [ "### πŸ₯… Play-by-play\n", "\n", "[`nhl_web_pbp(game_id=...)`](../nhl/reference/nhl_api_web.md#nhl_web_pbp) returns\n", "one row per event in clean `snake_case` β€” `type_desc_key`, `time_in_period`,\n", "`period_descriptor_number`, plus shot coordinates `details_x_coord` /\n", "`details_y_coord`. That coordinate pair is your gateway to shot maps. πŸ—ΊοΈ" ] }, { "cell_type": "code", "execution_count": null, "id": "c6414542", "metadata": {}, "outputs": [], "source": [ "pbp = safe('native pbp', lambda: nhl.nhl_web_pbp(game_id=NATIVE_GAME))\n", "if pbp is not None:\n", " print('pbp shape:', pbp.shape)\n", " show = ['period_descriptor_number', 'time_in_period', 'type_desc_key',\n", " 'details_event_owner_team_id', 'details_x_coord', 'details_y_coord']\n", " out = pbp.select([c for c in show if c in pbp.columns]).head()\n", "else:\n", " out = 'pbp unavailable'\n", "out" ] }, { "cell_type": "code", "execution_count": null, "id": "3a948a7b", "metadata": {}, "outputs": [], "source": [ "# Event-type mix for the game β€” native uses `type_desc_key`\n", "(pbp.group_by('type_desc_key').agg(pl.len().alias('events'))\n", " .sort('events', descending=True).head(10)\n", " if pbp is not None else 'pbp unavailable')" ] }, { "cell_type": "markdown", "id": "0dc6fbf5", "metadata": {}, "source": [ "### πŸ“Š Boxscore\n", "\n", "[`nhl_boxscore(game_id=...)`](../nhl/reference/nhl_api_web.md#nhl_boxscore) gives\n", "one row per player (skaters + goalies) with `home_away`, `position`, and the\n", "per-player stat line. Let's pull the night's top scorers." ] }, { "cell_type": "code", "execution_count": null, "id": "89009de5", "metadata": {}, "outputs": [], "source": [ "box = safe('native boxscore', lambda: nhl.nhl_boxscore(game_id=NATIVE_GAME))\n", "if box is not None:\n", " out = (box.filter(pl.col('position') != 'G')\n", " .select(['name_default', 'home_away', 'position',\n", " 'goals', 'assists', 'points', 'sog', 'toi'])\n", " .sort('points', descending=True).head())\n", "else:\n", " out = 'boxscore unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "0517ba83", "metadata": {}, "source": [ "### πŸ† Standings\n", "\n", "[`nhl_standings(date='YYYY-MM-DD')`](../nhl/reference/nhl_api_web.md#nhl_standings)\n", "returns one row per team with conference/division context and points β€” pass any\n", "date to get the table *as of* that day." ] }, { "cell_type": "code", "execution_count": null, "id": "a4add917", "metadata": {}, "outputs": [], "source": [ "standings = safe('native standings', lambda: nhl.nhl_standings(date='2024-04-15'))\n", "if standings is not None:\n", " out = (standings.select(['team_name_default', 'conference_name', 'division_name',\n", " 'games_played', 'wins', 'losses', 'points'])\n", " .sort('points', descending=True).head())\n", "else:\n", " out = 'standings unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "b3d31360", "metadata": {}, "source": [ "## πŸ›°οΈ NHL EDGE β€” player & puck tracking\n", "\n", "EDGE is the league's tracking layer: skating speed, shot speed, zone time,\n", "skating distance β€” all measured by sensors. The `*_detail` calls return a\n", "player's tracked values **alongside the league average and percentile**, and\n", "the `*_landing` calls return wide leaderboard frames.\n", "\n", "| Function | Tracking metric |\n", "|---|---|\n", "| [`nhl_edge_skater_skating_speed_detail`](../nhl/reference/nhl_edge.md#nhl_edge_skater_skating_speed_detail) | top speed, speed bursts, vs league avg |\n", "| [`nhl_edge_skater_landing`](../nhl/reference/nhl_edge.md#nhl_edge_skater_landing) | skater leaders (hardest shot, top speed…) |\n", "| [`nhl_edge_team_landing`](../nhl/reference/nhl_edge.md#nhl_edge_team_landing) | team-level tracking leaders |\n", "\n", "Here's Connor McDavid's (`8478402`) skating-speed detail for 2023-24 β€” how does\n", "the fastest man in the league stack up? ⚑" ] }, { "cell_type": "code", "execution_count": null, "id": "962a0308", "metadata": {}, "outputs": [], "source": [ "edge = safe('EDGE skating speed',\n", " lambda: nhl.nhl_edge_skater_skating_speed_detail(player_id=8478402, season=SEASON))\n", "if edge is not None:\n", " keep = [c for c in (\n", " 'skating_speed_details_max_skating_speed_imperial',\n", " 'skating_speed_details_max_skating_speed_league_avg_imperial',\n", " 'skating_speed_details_max_skating_speed_percentile',\n", " 'skating_speed_details_bursts_over22_value',\n", " 'skating_speed_details_bursts_over22_percentile',\n", " ) if c in edge.columns]\n", " out = edge.select(keep) if keep else edge.head()\n", "else:\n", " out = 'EDGE detail unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "a162f93c", "metadata": {}, "source": [ "## πŸ“ˆ Stats-REST & Records flat APIs\n", "\n", "Two more first-party surfaces round out the kit:\n", "\n", "- **Stats-REST** (`api.nhle.com/stats/rest`) β€” clean leaderboard frames.\n", " [`nhl_stats_rest_leaders_skaters(attribute=...)`](../nhl/reference/nhl_stats_rest.md#nhl_stats_rest_leaders_skaters)\n", " returns a tidy top-10 for any attribute (`goals`, `points`, `assists`, …);\n", " [`nhl_stats_rest_leaders_goalies`](../nhl/reference/nhl_stats_rest.md#nhl_stats_rest_leaders_goalies)\n", " is the goalie twin.\n", "- **Records** (`records.nhl.com`) β€” historical reference data, e.g.\n", " [`nhl_records_franchises`](../nhl/reference/nhl_records.md#nhl_records_franchises)." ] }, { "cell_type": "code", "execution_count": null, "id": "5eea14df", "metadata": {}, "outputs": [], "source": [ "leaders = safe('stats-rest goal leaders',\n", " lambda: nhl.nhl_stats_rest_leaders_skaters(attribute='goals'))\n", "if leaders is not None:\n", " keep = ['player_full_name', 'player_position_code', 'team_tri_code', 'goals']\n", " out = leaders.select([c for c in keep if c in leaders.columns]).head(10)\n", "else:\n", " out = 'leaders unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "7529100d", "metadata": {}, "source": [ "## 🍳 Cookbook: common NHL tasks\n", "\n", "Now the fun part β€” a dozen recipes you'll reach for constantly, almost all\n", "built on the **premium native feed**. Each one is a copy-paste starting\n", "point: a game pull, a team view, a player line, leaderboards, splits,\n", "joins, season-to-date aggregates, the draft board, franchise history, and\n", "EDGE tracking β€” every call wrapped in `safe()` so an offseason or a\n", "throttle never costs you a traceback. 🍳" ] }, { "cell_type": "markdown", "id": "b34051b3", "metadata": {}, "source": [ "### Recipe 1 β€” A game's boxscore + play-by-play 🎯\n", "\n", "Grab a `game_id` from [`nhl_web_schedule`](../nhl/reference/nhl_api_web.md#nhl_web_schedule),\n", "then pull the [`nhl_boxscore`](../nhl/reference/nhl_api_web.md#nhl_boxscore) and\n", "[`nhl_web_pbp`](../nhl/reference/nhl_api_web.md#nhl_web_pbp) together β€” the box\n", "for the line score, the pbp for the event stream." ] }, { "cell_type": "code", "execution_count": null, "id": "83a33074", "metadata": {}, "outputs": [], "source": [ "if sched is not None and sched.height:\n", " gid = int(sched['id'][0])\n", " r_box = safe(f'boxscore {gid}', lambda: nhl.nhl_boxscore(game_id=gid))\n", " r_pbp = safe(f'pbp {gid}', lambda: nhl.nhl_web_pbp(game_id=gid))\n", " print('players in box:', None if r_box is None else r_box.height,\n", " '| pbp events:', None if r_pbp is None else r_pbp.height)\n", "else:\n", " print('no schedule rows to pick a game_id from')" ] }, { "cell_type": "markdown", "id": "657c2d74", "metadata": {}, "source": [ "### Recipe 2 β€” A team, its schedule & its roster πŸ‘₯\n", "\n", "Use the team tri-code (e.g. `FLA`) with\n", "[`nhl_club_schedule_season`](../nhl/reference/nhl_api_web.md#nhl_club_schedule_season)\n", "for the full slate and [`nhl_roster`](../nhl/reference/nhl_api_web.md#nhl_roster)\n", "for the player list." ] }, { "cell_type": "code", "execution_count": null, "id": "4889f71d", "metadata": {}, "outputs": [], "source": [ "TEAM = 'FLA'\n", "club_sched = safe(f'{TEAM} schedule',\n", " lambda: nhl.nhl_club_schedule_season(team=TEAM, season=SEASON))\n", "roster = safe(f'{TEAM} roster', lambda: nhl.nhl_roster(team=TEAM, season=SEASON))\n", "print('games:', None if club_sched is None else club_sched.height,\n", " '| roster size:', None if roster is None else roster.height)\n", "if roster is not None and roster.height:\n", " cols = ['id', 'first_name_default', 'last_name_default',\n", " 'sweater_number', 'position_code', 'shoots_catches']\n", " out = roster.select([c for c in cols if c in roster.columns]).head()\n", "else:\n", " out = 'roster unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "add8b748", "metadata": {}, "source": [ "### Recipe 3 β€” A player's game log + the league leaderboard ⚑\n", "\n", "Pair a single player's [`nhl_player_game_log`](../nhl/reference/nhl_api_web.md#nhl_player_game_log)\n", "(game-by-game) with the season-wide\n", "[`nhl_skater_leaders`](../nhl/reference/nhl_api_web.md#nhl_skater_leaders) board\n", "to see where they rank. McDavid is `8478402`." ] }, { "cell_type": "code", "execution_count": null, "id": "3cdd7a6f", "metadata": {}, "outputs": [], "source": [ "gamelog = safe('McDavid game log',\n", " lambda: nhl.nhl_player_game_log(player_id=8478402, season=SEASON))\n", "if gamelog is not None and gamelog.height:\n", " cols = ['game_date', 'opponent_abbrev', 'goals', 'assists', 'points', 'shots', 'toi']\n", " out = gamelog.select([c for c in cols if c in gamelog.columns]).head()\n", "else:\n", " out = 'game log unavailable'\n", "out" ] }, { "cell_type": "code", "execution_count": null, "id": "d87455e9", "metadata": {}, "outputs": [], "source": [ "board = safe('skater leaders', lambda: nhl.nhl_skater_leaders(season=SEASON))\n", "if board is not None and board.height:\n", " cols = ['category', 'first_name_default', 'last_name_default', 'team_abbrev', 'value']\n", " out = board.select([c for c in cols if c in board.columns]).head(10)\n", "else:\n", " out = 'leaders unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "a2bc166c", "metadata": {}, "source": [ "### Recipe 4 β€” An EDGE tracking leaderboard πŸ›°οΈ\n", "\n", "[`nhl_edge_skater_landing`](../nhl/reference/nhl_edge.md#nhl_edge_skater_landing)\n", "returns a wide single-row frame of EDGE *leaders* β€” hardest shot, fastest\n", "skater, and more. Here we surface who owned the hardest shot in 2023-24." ] }, { "cell_type": "code", "execution_count": null, "id": "a13c2e4e", "metadata": {}, "outputs": [], "source": [ "el = safe('EDGE skater leaders', lambda: nhl.nhl_edge_skater_landing(season=SEASON))\n", "if el is not None:\n", " keep = [c for c in el.columns if c.startswith('leaders_hardest_shot_player_')\n", " and ('first_name' in c or 'last_name' in c or 'team_abbrev' in c\n", " or c.endswith('position'))]\n", " out = el.select(keep) if keep else el.head()\n", "else:\n", " out = 'EDGE leaders unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "0cf0868b", "metadata": {}, "source": [ "### Recipe 5 β€” Who's hot? Standings by last-10 form πŸ”₯\n", "\n", "The native [`nhl_standings`](../nhl/reference/nhl_api_web.md#nhl_standings)\n", "frame carries rich split columns β€” `l10_*` (last ten games) and `streak_*` β€”\n", "so you can rank teams by *recent* form instead of season-long points." ] }, { "cell_type": "code", "execution_count": null, "id": "845e0f0c", "metadata": {}, "outputs": [], "source": [ "hot = safe('standings as-of date',\n", " lambda: nhl.nhl_standings(date='2024-04-15'))\n", "if hot is not None and hot.height:\n", " cols = ['team_name_default', 'l10_wins', 'l10_losses', 'l10_ot_losses',\n", " 'l10_points', 'streak_code', 'streak_count', 'points']\n", " out = (hot.select([c for c in cols if c in hot.columns])\n", " .sort(['l10_points', 'points'], descending=True).head(8))\n", "else:\n", " out = 'standings unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "2ce10959", "metadata": {}, "source": [ "### Recipe 6 β€” A whole team's stat lines in one call πŸ“‹\n", "\n", "[`nhl_club_stats`](../nhl/reference/nhl_api_web.md#nhl_club_stats) returns a\n", "**dict** with `skaters` and `goalies` frames β€” the entire roster's season\n", "totals, no looping over players. Here are the Panthers' top point-getters." ] }, { "cell_type": "code", "execution_count": null, "id": "ed8a6807", "metadata": {}, "outputs": [], "source": [ "cs = safe('FLA club stats',\n", " lambda: nhl.nhl_club_stats(team='FLA', season=SEASON))\n", "if isinstance(cs, dict) and isinstance(cs.get('skaters'), pl.DataFrame) and cs['skaters'].height:\n", " sk = cs['skaters']\n", " cols = ['first_name_default', 'last_name_default', 'position_code',\n", " 'games_played', 'goals', 'assists', 'points', 'shots',\n", " 'avg_time_on_ice_per_game']\n", " out = (sk.select([c for c in cols if c in sk.columns])\n", " .sort('points', descending=True).head(8))\n", "else:\n", " out = 'club stats unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "7f89decb", "metadata": {}, "source": [ "### Recipe 7 β€” Goalie leaderboard + a netminder's bio πŸ₯…\n", "\n", "Pair the season-wide\n", "[`nhl_goalie_leaders`](../nhl/reference/nhl_api_web.md#nhl_goalie_leaders)\n", "board (it bundles wins, save %, GAA and shutouts in one frame, tagged by\n", "`category`) with a single goalie's\n", "[`nhl_player_landing`](../nhl/reference/nhl_api_web.md#nhl_player_landing)\n", "bio card." ] }, { "cell_type": "code", "execution_count": null, "id": "789857bf", "metadata": {}, "outputs": [], "source": [ "gboard = safe('goalie leaders', lambda: nhl.nhl_goalie_leaders(season=SEASON))\n", "if gboard is not None and gboard.height:\n", " cols = ['category', 'first_name_default', 'last_name_default',\n", " 'team_abbrev', 'value']\n", " out = (gboard.filter(pl.col('category') == 'wins')\n", " .select([c for c in cols if c in gboard.columns])\n", " .sort('value', descending=True).head(5)\n", " if 'category' in gboard.columns else gboard.head())\n", "else:\n", " out = 'goalie leaders unavailable'\n", "out" ] }, { "cell_type": "code", "execution_count": null, "id": "17b4b492", "metadata": {}, "outputs": [], "source": [ "# Bobrovsky's bio card (player_id 8475683) β€” one wide row\n", "bio = safe('goalie landing', lambda: nhl.nhl_player_landing(player_id=8475683))\n", "if bio is not None and bio.height:\n", " cols = ['first_name_default', 'last_name_default', 'position',\n", " 'current_team_abbrev', 'height_in_inches', 'weight_in_pounds',\n", " 'birth_city_default', 'birth_country', 'draft_details_year',\n", " 'draft_details_overall_pick']\n", " out = bio.select([c for c in cols if c in bio.columns])\n", "else:\n", " out = 'player landing unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "2b8fc543", "metadata": {}, "source": [ "### Recipe 8 β€” Home vs road splits, derived from a schedule 🏠✈️\n", "\n", "No splits endpoint? No problem β€” pull a club's full season with\n", "[`nhl_club_schedule_season`](../nhl/reference/nhl_api_web.md#nhl_club_schedule_season),\n", "tag each finished game as home or road, and let **polars** roll up\n", "goals-for / goals-against per game. A pattern you'll reuse everywhere." ] }, { "cell_type": "code", "execution_count": null, "id": "a8a45972", "metadata": {}, "outputs": [], "source": [ "TEAM = 'FLA'\n", "cs2 = safe(f'{TEAM} season schedule',\n", " lambda: nhl.nhl_club_schedule_season(team=TEAM, season=SEASON))\n", "need = {'home_team_abbrev', 'away_team_abbrev', 'home_team_score', 'away_team_score'}\n", "if cs2 is not None and need.issubset(cs2.columns):\n", " g = cs2.filter(pl.col('home_team_score').is_not_null())\n", " if 'game_type' in g.columns:\n", " g = g.filter(pl.col('game_type') == 2) # regular season only\n", " g = g.with_columns([\n", " pl.when(pl.col('home_team_abbrev') == TEAM).then(pl.lit('home'))\n", " .otherwise(pl.lit('road')).alias('venue'),\n", " pl.when(pl.col('home_team_abbrev') == TEAM)\n", " .then(pl.col('home_team_score')).otherwise(pl.col('away_team_score')).alias('gf'),\n", " pl.when(pl.col('home_team_abbrev') == TEAM)\n", " .then(pl.col('away_team_score')).otherwise(pl.col('home_team_score')).alias('ga'),\n", " ])\n", " out = (g.group_by('venue').agg([\n", " pl.len().alias('gp'),\n", " (pl.col('gf') > pl.col('ga')).sum().alias('wins'),\n", " pl.col('gf').mean().round(2).alias('gf_per_game'),\n", " pl.col('ga').mean().round(2).alias('ga_per_game'),\n", " ]).sort('venue'))\n", "else:\n", " out = 'club schedule unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "6678005b", "metadata": {}, "source": [ "### Recipe 9 β€” Pull a draft board 🎟️\n", "\n", "[`nhl_draft_picks`](../nhl/reference/nhl_api_web.md#nhl_draft_picks) returns one\n", "row per selection for a given `year` (and optional `round_`) β€” overall pick,\n", "team, position, and the player's amateur club. Here's the 2023 first round." ] }, { "cell_type": "code", "execution_count": null, "id": "13e34f3e", "metadata": {}, "outputs": [], "source": [ "draft = safe('2023 draft round 1',\n", " lambda: nhl.nhl_draft_picks(year=2023, round_=1))\n", "if draft is not None and draft.height:\n", " cols = ['overall_pick', 'team_abbrev', 'first_name_default',\n", " 'last_name_default', 'position_code', 'amateur_club_name',\n", " 'amateur_league']\n", " out = (draft.select([c for c in cols if c in draft.columns])\n", " .sort('overall_pick').head(10))\n", "else:\n", " out = 'draft board unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "cd1b5a1d", "metadata": {}, "source": [ "### Recipe 10 β€” Season-to-date team aggregates (loader + pandas) πŸ“¦πŸΌ\n", "\n", "For **multi-game** rollups, the offline-friendly\n", "[`load_nhl_team_box`](../nhl/reference/additional.md#load_nhl_team_box) parquet\n", "release is your friend: one row per team per game. Group it in polars, then\n", "`.to_pandas()` to hand the result to the rest of the PyData stack." ] }, { "cell_type": "code", "execution_count": null, "id": "64c7ece3", "metadata": {}, "outputs": [], "source": [ "tb = safe('team box 2024', lambda: nhl.load_nhl_team_box(seasons=[2024]))\n", "if tb is not None and tb.height and {'tri_code', 'shots', 'hits', 'goals'}.issubset(tb.columns):\n", " agg = (tb.group_by('tri_code').agg([\n", " pl.len().alias('games'),\n", " pl.col('goals').cast(pl.Float64).mean().round(2).alias('goals_pg'),\n", " pl.col('shots').cast(pl.Float64).mean().round(1).alias('shots_pg'),\n", " pl.col('hits').cast(pl.Float64).mean().round(1).alias('hits_pg'),\n", " ]).sort('goals_pg', descending=True).head(8))\n", " pdf = agg.to_pandas() # hand off to pandas for plotting/modeling\n", " print('pandas frame:', type(pdf).__name__, pdf.shape)\n", " out = agg\n", "else:\n", " out = 'team box loader unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "e4c9565d", "metadata": {}, "source": [ "### Recipe 11 β€” All-time franchise standings (Records API join) πŸ›οΈ\n", "\n", "The Records flat API never goes offseason. Join\n", "[`nhl_records_franchise_team_totals`](../nhl/reference/nhl_records.md#nhl_records_franchise_team_totals)\n", "(all-time W/L/points, regular season `game_type_id == 2`) onto\n", "[`nhl_records_franchises`](../nhl/reference/nhl_records.md#nhl_records_franchises)\n", "for the names β€” the winningest clubs in league history." ] }, { "cell_type": "code", "execution_count": null, "id": "20f0cd5c", "metadata": {}, "outputs": [], "source": [ "totals = safe('franchise team totals',\n", " lambda: nhl.nhl_records_franchise_team_totals())\n", "names = safe('franchises', lambda: nhl.nhl_records_franchises())\n", "if (totals is not None and totals.height and names is not None and names.height\n", " and 'franchise_id' in totals.columns and 'id' in names.columns):\n", " reg = totals.filter(pl.col('game_type_id') == 2) if 'game_type_id' in totals.columns else totals\n", " keep_n = [c for c in ('id', 'full_name', 'team_abbrev') if c in names.columns]\n", " out = (reg.join(names.select(keep_n), left_on='franchise_id', right_on='id', how='left')\n", " .select([c for c in ('full_name', 'games_played', 'wins',\n", " 'losses', 'points', 'cups')\n", " if c in reg.columns or c == 'full_name'])\n", " .sort('wins', descending=True).head(10))\n", "else:\n", " out = 'franchise records unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "416c1390", "metadata": {}, "source": [ "### Recipe 12 β€” EDGE tracking leaders: team & goalie πŸ›°οΈ\n", "\n", "Round out the tour with two more EDGE *landing* boards. Each is a wide\n", "single-row frame of leaders; pluck the columns for one metric to see who\n", "tops it. Here: the team that piled up the most 90+ mph shot attempts, and\n", "the goalie with the best high-danger save percentage." ] }, { "cell_type": "code", "execution_count": null, "id": "28ca8ec9", "metadata": {}, "outputs": [], "source": [ "tl = safe('EDGE team leaders', lambda: nhl.nhl_edge_team_landing(season=SEASON))\n", "if tl is not None and tl.height:\n", " keep = [c for c in tl.columns\n", " if c.startswith('leaders_shot_attempts_over90_')\n", " and ('team_abbrev' in c or 'common_name_default' in c\n", " or c.endswith('_attempts'))]\n", " out = tl.select(keep) if keep else tl.head()\n", "else:\n", " out = 'EDGE team leaders unavailable'\n", "out" ] }, { "cell_type": "code", "execution_count": null, "id": "cf358741", "metadata": {}, "outputs": [], "source": [ "gl = safe('EDGE goalie leaders', lambda: nhl.nhl_edge_goalie_landing(season=SEASON))\n", "if gl is not None and gl.height:\n", " keep = [c for c in gl.columns\n", " if c.startswith('leaders_high_danger_save_pctg_')\n", " and ('player_first_name_default' in c\n", " or 'player_last_name_default' in c\n", " or 'player_team_abbrev' in c\n", " or c.endswith('_save_pctg'))]\n", " out = gl.select(keep) if keep else gl.head()\n", "else:\n", " out = 'EDGE goalie leaders unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "407849a0", "metadata": {}, "source": [ "## πŸ›Ÿ ESPN NHL (`espn_nhl_*`) β€” the secondary path\n", "\n", "Prefer the native feed above, but ESPN is a handy fallback and matches the\n", "conventions used across every other league in the package. Team names are\n", "`home_display_name` / `away_display_name`, scores come back as **strings** (cast\n", "before arithmetic), and [`espn_nhl_pbp`](../nhl/reference/additional.md#espn_nhl_pbp)\n", "returns a **dict** whose `plays` use raw ESPN dot-notation. ESPN game ids look\n", "like `401675111`.\n", "\n", "| Function | What it gives you |\n", "|---|---|\n", "| [`espn_nhl_teams`](../nhl/reference/additional.md#espn_nhl_teams) | ESPN team directory |\n", "| [`espn_nhl_schedule`](../nhl/reference/additional.md#espn_nhl_schedule) | schedule for a date |\n", "| [`espn_nhl_pbp`](../nhl/reference/additional.md#espn_nhl_pbp) | play-by-play (a dict) |\n", "| [`espn_nhl_standings`](../nhl/reference/site.md#espn_nhl_standings) | standings |\n" ] }, { "cell_type": "code", "execution_count": null, "id": "d1747977", "metadata": {}, "outputs": [], "source": [ "teams = safe('ESPN teams', lambda: nhl.espn_nhl_teams())\n", "if teams is not None:\n", " cols = ['team_id', 'team_location', 'team_name', 'team_abbreviation', 'team_display_name']\n", " out = teams.select([c for c in cols if c in teams.columns]).head()\n", "else:\n", " out = 'ESPN teams unavailable'\n", "out" ] }, { "cell_type": "code", "execution_count": null, "id": "4706aad2", "metadata": {}, "outputs": [], "source": [ "espn_pbp = safe(f'ESPN pbp {ESPN_GAME}', lambda: nhl.espn_nhl_pbp(game_id=ESPN_GAME))\n", "if espn_pbp is not None and espn_pbp.get('plays'):\n", " plays = pl.DataFrame(espn_pbp['plays'], infer_schema_length=None)\n", " show = [c for c in ['period.number', 'clock.displayValue', 'text', 'type.text', 'scoringPlay']\n", " if c in plays.columns]\n", " print('ESPN plays:', plays.height)\n", " out = plays.select(show).head()\n", "else:\n", " out = 'ESPN pbp unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "bd3e4b30", "metadata": {}, "source": [ "## πŸ“¦ Parquet loaders (`load_nhl_*`)\n", "\n", "When you want **multi-season** data fast and offline-friendly, the `load_nhl_*`\n", "loaders read pre-built parquet data releases (fastRhockey-era schema) and return\n", "polars frames. Pass `seasons=[...]`; add `return_as_pandas=True` for pandas.\n", "\n", "| Function | Release |\n", "|---|---|\n", "| [`load_nhl_schedule`](../nhl/reference/loaders.md#load_nhl_schedule) | schedules |\n", "| [`load_nhl_team_box`](../nhl/reference/additional.md#load_nhl_team_box) | team box |\n", "| [`load_nhl_player_box`](../nhl/reference/additional.md#load_nhl_player_box) | player box |\n", "| [`load_nhl_pbp`](../nhl/reference/loaders.md#load_nhl_pbp) | play-by-play |\n" ] }, { "cell_type": "code", "execution_count": null, "id": "423ef0b4", "metadata": {}, "outputs": [], "source": [ "rel = safe('load schedule 2024', lambda: nhl.load_nhl_schedule(seasons=[2024]))\n", "if rel is not None:\n", " print('release schedule shape:', rel.shape)\n", " cols = ['game_id', 'game_date', 'home_team_name', 'away_team_name', 'home_score', 'away_score']\n", " out = rel.select([c for c in cols if c in rel.columns]).head()\n", "else:\n", " out = 'release loader unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "391d0490", "metadata": {}, "source": [ "## πŸŽ‰ Where to next\n", "\n", "You just toured the **premium native NHL feed** end to end β€” schedule,\n", "play-by-play, boxscores, standings, rosters, leaderboards, **EDGE tracking**,\n", "the **stats-REST** and **Records** flat APIs β€” plus the ESPN fallback and the\n", "parquet loaders. A few parting tips:\n", "\n", "- Pass `return_as_pandas=True` on any native call for a pandas frame, or\n", " `return_parsed=False` for the raw JSON.\n", "- Native game ids (`2023030417`) β‰  ESPN game ids (`401675111`) β€” same game,\n", " different namespaces. 🧭\n", "- Full reference, by source:\n", " [NHL Web API](../nhl/reference/nhl_api_web.md) Β·\n", " [NHL EDGE](../nhl/reference/nhl_edge.md) Β·\n", " [Stats-REST](../nhl/reference/nhl_stats_rest.md) Β·\n", " [Records](../nhl/reference/nhl_records.md) Β·\n", " [loaders](../nhl/reference/loaders.md) Β·\n", " [additional / ESPN](../nhl/reference/additional.md)\n", "- Women's pro hockey? See the **PWHL** tutorial (`10_pwhl_intro.ipynb`).\n", "- R user? The same surface lives in\n", " [fastRhockey](https://fastRhockey.sportsdataverse.org).\n", "\n", "Now go build something great β€” and may your save percentage be ever high! πŸ₯…" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }