{ "cells": [ { "cell_type": "markdown", "id": "a588d07c", "metadata": {}, "source": [ "# πŸ’ Junior & minor hockey with `sportsdataverse-py`\n", "\n", "Four leagues, **one toolkit**. `sportsdataverse` wraps the HockeyTech /\n", "LeagueStat feed behind the American Hockey League (**AHL**) and the three\n", "Canadian Hockey League major-junior loops β€” Ontario (**OHL**), Western\n", "(**WHL**) and Quebec Maritimes (**QMJHL**). They share an *identical* module\n", "shape, so learn one and you've learned all four. πŸŽ‰\n", "\n", "And the best part: **no API key needed** β€” the public HockeyTech client keys\n", "ship with the package. Let's go scout some future pros!" ] }, { "cell_type": "markdown", "id": "3ae514ce", "metadata": {}, "source": [ "## πŸ—ΊοΈ The four leagues\n", "\n", "| League | Module | Level |\n", "|---|---|---|\n", "| AHL | `sportsdataverse.hockey.ahl` | Minor pro (one rung below the NHL) |\n", "| OHL | `sportsdataverse.hockey.ohl` | CHL major junior |\n", "| WHL | `sportsdataverse.hockey.whl` | CHL major junior |\n", "| QMJHL | `sportsdataverse.hockey.qmjhl` | CHL major junior |\n", "\n", "Every module exposes the same `_*` surface. Here's the AHL kit β€”\n", "swap `ahl_` for `ohl_` / `whl_` / `qmjhl_` and it all works identically.\n", "Every accessor returns **polars** by default (`return_as_pandas=True` for pandas):\n", "\n", "| Function | What it gives you |\n", "|---|---|\n", "| [`ahl_schedule`](../ahl/reference/additional.md#ahl_schedule) | Games + results, one row per game |\n", "| [`ahl_standings`](../ahl/reference/additional.md#ahl_standings) | Team standings |\n", "| [`ahl_teams`](../ahl/reference/additional.md#ahl_teams) | Teams in a season (grab `team_id`s) |\n", "| [`ahl_team_roster`](../ahl/reference/additional.md#ahl_team_roster) | A team's roster |\n", "| [`ahl_pbp`](../ahl/reference/additional.md#ahl_pbp) | Event-level play-by-play |\n", "| [`ahl_player_stats`](../ahl/reference/additional.md#ahl_player_stats) | A player's full season-by-season stat line |\n", "| [`ahl_leaders`](../ahl/reference/additional.md#ahl_leaders) | Statistical leaders |\n", "| [`ahl_game_summary`](../ahl/reference/additional.md#ahl_game_summary) | Box-score summary for a game (goals / penalties / stars) |\n", "| [`ahl_game_corsi`](../ahl/reference/additional.md#ahl_game_corsi) | Corsi / Fenwick shot-attempt metrics |\n", "| [`ahl_game_shifts`](../ahl/reference/additional.md#ahl_game_shifts) | Shift charts |\n", "| [`ahl_player_toi`](../ahl/reference/additional.md#ahl_player_toi) | Time-on-ice |\n", "| [`ahl_season_id`](../ahl/reference/additional.md#ahl_season_id) Β· `most_recent_ahl_season` | Season helpers |" ] }, { "cell_type": "markdown", "id": "5bef709a", "metadata": {}, "source": [ "## πŸ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "569e4fe2", "metadata": {}, "outputs": [], "source": [ "import sportsdataverse.hockey.ahl as ahl\n", "import sportsdataverse.hockey.ohl as ohl\n", "import sportsdataverse.hockey.whl as whl\n", "import sportsdataverse.hockey.qmjhl as qmjhl\n", "\n", "LEAGUES = {\"ahl\": ahl, \"ohl\": ohl, \"whl\": whl, \"qmjhl\": qmjhl}" ] }, { "cell_type": "markdown", "id": "1041c812", "metadata": {}, "source": [ "Junior/minor feeds are seasonal and occasionally rate-limited, so a tiny\n", "`safe()` helper runs each call defensively β€” you get the frame when the feed\n", "is up, and a friendly one-liner when it isn't (never a scary traceback). πŸ›Ÿ" ] }, { "cell_type": "code", "execution_count": null, "id": "424bcd3a", "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" ] }, { "cell_type": "markdown", "id": "bb2b9142", "metadata": {}, "source": [ "## πŸ“… Schedule\n", "\n", "[`ahl_schedule`](../ahl/reference/additional.md#ahl_schedule) (and its siblings) returns one row per game.\n", "Pass `season=` or let it default to the current season." ] }, { "cell_type": "code", "execution_count": null, "id": "af5f891c", "metadata": {}, "outputs": [], "source": [ "sched = safe(\"AHL schedule\", lambda: ahl.ahl_schedule(season=ahl.most_recent_ahl_season()))\n", "sched.shape if sched is not None else None" ] }, { "cell_type": "code", "execution_count": null, "id": "14c8c474", "metadata": {}, "outputs": [], "source": [ "cols = [\"game_id\", \"game_date\", \"home_team\", \"away_team\", \"home_score\", \"away_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": "381d3005", "metadata": {}, "source": [ "## 🍳 Cookbook: common hockey tasks\n", "\n", "The shared surface makes these recipes work the same in every league β€” just\n", "swap the module. A dozen recipes follow, from leaderboards to box scores to\n", "pandas hand-offs; mix and match them however your scouting report needs. πŸ“‹" ] }, { "cell_type": "markdown", "id": "ebd92e7a", "metadata": {}, "source": [ "### Recipe 1 β€” Standings table πŸ†\n", "\n", "[`ohl_standings`](../ahl/reference/additional.md#ahl_standings) (shown for the OHL) gives one row per team." ] }, { "cell_type": "code", "execution_count": null, "id": "d1332e80", "metadata": {}, "outputs": [], "source": [ "standings = safe(\"OHL standings\", lambda: ohl.ohl_standings(season=ohl.most_recent_ohl_season()))\n", "cols = [\"team\", \"games_played\", \"wins\", \"losses\", \"ot_losses\", \"points\", \"goals_for\", \"goals_against\"]\n", "(standings.select([c for c in cols if c in standings.columns]).head()\n", " if standings is not None and standings.height else \"standings unavailable\")" ] }, { "cell_type": "markdown", "id": "f35a546a", "metadata": {}, "source": [ "### Recipe 2 β€” A team and its roster πŸ‘₯\n", "\n", "List teams with [`whl_teams`](../ahl/reference/additional.md#ahl_teams), grab a `team_id`, then pull the\n", "roster with [`whl_team_roster`](../ahl/reference/additional.md#ahl_team_roster)." ] }, { "cell_type": "code", "execution_count": null, "id": "cc6450f5", "metadata": {}, "outputs": [], "source": [ "teams = safe(\"WHL teams\", lambda: whl.whl_teams(season=whl.most_recent_whl_season()))\n", "if teams is not None and teams.height:\n", " tid_col = next((c for c in (\"team_id\", \"id\") if c in teams.columns), None)\n", " tid = int(teams[tid_col][0]) if tid_col else None\n", " roster = safe(f\"WHL roster {tid}\", lambda: whl.whl_team_roster(team_id=tid)) if tid else None\n", " out = roster.head() if roster is not None and roster.height else teams.head()\n", "else:\n", " out = \"teams unavailable\"\n", "out" ] }, { "cell_type": "markdown", "id": "1ba4b0a3", "metadata": {}, "source": [ "### Recipe 3 β€” A game's play-by-play + shot attempts πŸ“ˆ\n", "\n", "Take a `game_id` from the schedule, then [`ahl_pbp`](../ahl/reference/additional.md#ahl_pbp) for events and\n", "[`ahl_game_corsi`](../ahl/reference/additional.md#ahl_game_corsi) for Corsi/Fenwick β€” derived from the same feed." ] }, { "cell_type": "code", "execution_count": null, "id": "f4c0013a", "metadata": {}, "outputs": [], "source": [ "import polars as pl\n", "\n", "def latest_game_id(sched):\n", " \"\"\"Most-recent game_id available in a schedule frame (or None).\"\"\"\n", " if sched is None or not sched.height or \"game_id\" not in sched.columns:\n", " return None\n", " return int(sched.select(pl.col(\"game_id\").cast(pl.Int64, strict=False)).max().item())\n", "\n", "gid = latest_game_id(sched)\n", "if gid is not None:\n", " pbp = safe(f\"AHL pbp {gid}\", lambda: ahl.ahl_pbp(game_id=gid))\n", " corsi = safe(f\"AHL corsi {gid}\", lambda: ahl.ahl_game_corsi(game_id=gid))\n", " print(\"pbp rows:\", None if pbp is None else pbp.height,\n", " \"| corsi rows:\", None if corsi is None else corsi.height)\n", "else:\n", " print(\"no schedule rows to pick a game_id from\")" ] }, { "cell_type": "markdown", "id": "edb6cbd1", "metadata": {}, "source": [ "### Recipe 4 β€” Compare all four leagues at once πŸ”\n", "\n", "Because the surface is identical, one loop tours every league." ] }, { "cell_type": "code", "execution_count": null, "id": "006208d2", "metadata": {}, "outputs": [], "source": [ "rows = []\n", "for lg, mod in LEAGUES.items():\n", " season = safe(f\"{lg} season\", getattr(mod, f'most_recent_{lg}_season'))\n", " sch = safe(f\"{lg} schedule\", lambda mod=mod, lg=lg: getattr(mod, f'{lg}_schedule')()) if season else None\n", " rows.append({\"league\": lg.upper(), \"season\": season, \"games\": None if sch is None else sch.height})\n", "pl.DataFrame(rows)" ] }, { "cell_type": "markdown", "id": "9bb43452", "metadata": {}, "source": [ "### Recipe 5 β€” The scoring race πŸ₯‡\n", "\n", "[`qmjhl_leaders`](../ahl/reference/additional.md#ahl_leaders) hands back a ready-made leaderboard β€”\n", "`rank`, `name`, `team_code`, the `stat_formatted` value and what it measures\n", "(`type_formatted`). Same call, swap the module, and you've got every league's\n", "points race." ] }, { "cell_type": "code", "execution_count": null, "id": "98da0049", "metadata": {}, "outputs": [], "source": [ "leaders = safe(\"QMJHL leaders\", lambda: qmjhl.qmjhl_leaders(season=qmjhl.most_recent_qmjhl_season()))\n", "cols = [\"rank\", \"name\", \"team_code\", \"position\", \"stat_formatted\", \"type_formatted\"]\n", "(leaders.select([c for c in cols if c in leaders.columns]).head(10)\n", " if leaders is not None and leaders.height else \"leaders unavailable (offseason?)\")" ] }, { "cell_type": "markdown", "id": "b3599d49", "metadata": {}, "source": [ "### Recipe 6 β€” Who's hot, who's not 🌑️\n", "\n", "Standings come back unsorted-ish β€” one `sort` on `points` puts the contenders\n", "on top and the lottery teams on the bottom. We also add a quick goal-differential\n", "column so you can see *why* a team is where it is." ] }, { "cell_type": "code", "execution_count": null, "id": "ba4f9289", "metadata": {}, "outputs": [], "source": [ "st = safe(\"AHL standings\", lambda: ahl.ahl_standings(season=ahl.most_recent_ahl_season()))\n", "if st is not None and st.height and {\"points\", \"goals_for\", \"goals_against\"}.issubset(st.columns):\n", " ranked = (\n", " st.with_columns(\n", " goal_diff=(pl.col(\"goals_for\").cast(pl.Int64, strict=False)\n", " - pl.col(\"goals_against\").cast(pl.Int64, strict=False))\n", " )\n", " .sort(pl.col(\"points\").cast(pl.Int64, strict=False), descending=True)\n", " .select([c for c in [\"team\", \"points\", \"goals_for\", \"goals_against\", \"goal_diff\"]\n", " if c in st.columns or c == \"goal_diff\"])\n", " )\n", " out = ranked.head()\n", "else:\n", " out = \"standings unavailable\"\n", "out" ] }, { "cell_type": "markdown", "id": "b3622ad5", "metadata": {}, "source": [ "### Recipe 7 β€” A player's career stat line πŸ“Š\n", "\n", "Grab any `player_id` (the leaderboard is a handy source) and\n", "[`whl_player_stats`](../ahl/reference/additional.md#ahl_player_stats) returns every season they've played β€”\n", "regular season, playoffs and exhibition, tagged by `stat_type`." ] }, { "cell_type": "code", "execution_count": null, "id": "a7d2e49d", "metadata": {}, "outputs": [], "source": [ "wlead = safe(\"WHL leaders\", lambda: whl.whl_leaders(season=whl.most_recent_whl_season()))\n", "if wlead is not None and wlead.height and \"player_id\" in wlead.columns:\n", " pid = int(wlead[\"player_id\"][0])\n", " who = wlead[\"name\"][0] if \"name\" in wlead.columns else pid\n", " stats = safe(f\"WHL stats for {who}\", lambda: whl.whl_player_stats(player_id=pid))\n", " cols = [\"season_name\", \"team_name\", \"games_played\", \"goals\", \"assists\", \"points\", \"stat_type\"]\n", " out = (stats.select([c for c in cols if c in stats.columns]).head()\n", " if stats is not None and stats.height else \"no career rows\")\n", "else:\n", " out = \"leaders unavailable to source a player_id\"\n", "out" ] }, { "cell_type": "markdown", "id": "2246ee92", "metadata": {}, "source": [ "### Recipe 8 β€” The box score: goals & three stars 🌟\n", "\n", "[`ohl_game_summary`](../ahl/reference/additional.md#ahl_game_summary) returns a **dict of frames** β€”\n", "`game`, `goals`, `penalties`, `shots_by_period` and `three_stars`. Here's the\n", "scoring summary plus the post-game stars for one game." ] }, { "cell_type": "code", "execution_count": null, "id": "dbd0575f", "metadata": {}, "outputs": [], "source": [ "osched = safe(\"OHL schedule\", lambda: ohl.ohl_schedule(season=ohl.most_recent_ohl_season()))\n", "ogid = latest_game_id(osched)\n", "if ogid is not None:\n", " summ = safe(f\"OHL game summary {ogid}\", lambda: ohl.ohl_game_summary(game_id=ogid))\n", " if summ is not None:\n", " goals = summ.get(\"goals\")\n", " gcols = [\"period_id\", \"time\", \"goal_scorer_team_code\",\n", " \"goal_scorer_first_name\", \"goal_scorer_last_name\", \"power_play\", \"empty_net\"]\n", " out = (goals.select([c for c in gcols if c in goals.columns]).head()\n", " if goals is not None and goals.height else \"no goals frame\")\n", " else:\n", " out = \"summary unavailable\"\n", "else:\n", " out = \"no OHL game_id available\"\n", "out" ] }, { "cell_type": "markdown", "id": "501529ae", "metadata": {}, "source": [ "And the three-star selections from the same dict β€” no extra call needed:" ] }, { "cell_type": "code", "execution_count": null, "id": "475d8926", "metadata": {}, "outputs": [], "source": [ "if ogid is not None and 'summ' in dir() and summ is not None and summ.get(\"three_stars\") is not None:\n", " stars = summ[\"three_stars\"]\n", " scols = [\"first_name\", \"last_name\", \"jersey_number\", \"home\"]\n", " out = (stars.select([c for c in scols if c in stars.columns])\n", " if stars.height else \"no three-star data for this game\")\n", "else:\n", " out = \"summary unavailable\"\n", "out" ] }, { "cell_type": "markdown", "id": "f18d3f0e", "metadata": {}, "source": [ "### Recipe 9 β€” Slice the play-by-play: just the goals β›³\n", "\n", "The play-by-play frame carries a boolean `goal` flag, so pulling the scoring\n", "plays (with scorer names and the period clock) is a one-line filter." ] }, { "cell_type": "code", "execution_count": null, "id": "974051da", "metadata": {}, "outputs": [], "source": [ "if gid is not None:\n", " pbp9 = safe(f\"AHL pbp {gid}\", lambda: ahl.ahl_pbp(game_id=gid))\n", " if pbp9 is not None and pbp9.height and \"goal\" in pbp9.columns:\n", " goals_only = pbp9.filter(pl.col(\"goal\") == True) # noqa: E712 -- polars boolean\n", " cols = [\"period_of_game\", \"clock\", \"team_id\",\n", " \"player_name_first\", \"player_name_last\"]\n", " out = (goals_only.select([c for c in cols if c in goals_only.columns])\n", " if goals_only.height else \"no goals parsed for this game\")\n", " else:\n", " out = \"play-by-play unavailable\"\n", "else:\n", " out = \"no game_id available\"\n", "out" ] }, { "cell_type": "markdown", "id": "7ca9e18b", "metadata": {}, "source": [ "### Recipe 10 β€” Head-to-head history 🀝\n", "\n", "The schedule is just a frame, so a rivalry view is two `str.contains` filters.\n", "Pick the most-frequent home team, then every game where its top opponent\n", "visited β€” final scores included." ] }, { "cell_type": "code", "execution_count": null, "id": "7f5426f2", "metadata": {}, "outputs": [], "source": [ "if sched is not None and sched.height and \"home_team\" in sched.columns:\n", " top_home = sched[\"home_team\"].value_counts(sort=True)[\"home_team\"][0]\n", " vs = sched.filter(pl.col(\"home_team\") == top_home)\n", " top_away = vs[\"away_team\"].value_counts(sort=True)[\"away_team\"][0] if vs.height else None\n", " h2h = vs.filter(pl.col(\"away_team\") == top_away) if top_away else vs\n", " print(f\"{top_home} hosting {top_away}:\")\n", " out = h2h.select([c for c in [\"game_date\", \"home_team\", \"away_team\", \"home_score\", \"away_score\"]\n", " if c in h2h.columns]).head()\n", "else:\n", " out = \"schedule unavailable\"\n", "out" ] }, { "cell_type": "markdown", "id": "c9204c0f", "metadata": {}, "source": [ "### Recipe 11 β€” Scout the roster: shooters & positions πŸ”\n", "\n", "The roster frame is loaded with scouting fields. Here we break a team down by\n", "handedness (`shoots`) β€” a quick way to eyeball a power-play unit β€” straight\n", "from [`whl_team_roster`](../ahl/reference/additional.md#ahl_team_roster)." ] }, { "cell_type": "code", "execution_count": null, "id": "78682cd5", "metadata": {}, "outputs": [], "source": [ "rteams = safe(\"WHL teams\", lambda: whl.whl_teams(season=whl.most_recent_whl_season()))\n", "if rteams is not None and rteams.height and \"team_id\" in rteams.columns:\n", " rtid = int(rteams[\"team_id\"][0])\n", " ros = safe(f\"WHL roster {rtid}\", lambda: whl.whl_team_roster(team_id=rtid))\n", " if ros is not None and ros.height and \"shoots\" in ros.columns:\n", " out = ros.group_by(\"shoots\").len().sort(\"len\", descending=True).rename({\"len\": \"players\"})\n", " elif ros is not None and ros.height:\n", " out = ros.head()\n", " else:\n", " out = \"roster empty for this team/season\"\n", "else:\n", " out = \"teams unavailable\"\n", "out" ] }, { "cell_type": "markdown", "id": "a69a56df", "metadata": {}, "source": [ "### Recipe 12 β€” Hand off to pandas 🐼\n", "\n", "Every accessor takes `return_as_pandas=True`, so the whole toolkit drops\n", "straight into a pandas workflow. And [`ahl_season_id`](../ahl/reference/additional.md#ahl_season_id)\n", "lists every season with its end-year and game-type label β€” handy for pinning\n", "an exact `season_id` instead of guessing the end year." ] }, { "cell_type": "code", "execution_count": null, "id": "68a26d93", "metadata": {}, "outputs": [], "source": [ "seasons = safe(\"AHL season_id\", lambda: ahl.ahl_season_id(return_as_pandas=True))\n", "if seasons is not None and len(seasons):\n", " keep = [c for c in [\"season_id\", \"season_name\", \"season_yr\", \"game_type_label\"]\n", " if c in seasons.columns]\n", " recent = seasons[keep].sort_values(\"season_id\", ascending=False).head()\n", " print(type(recent).__module__) # -> pandas.core.frame\n", " out = recent\n", "else:\n", " out = \"season list unavailable\"\n", "out" ] }, { "cell_type": "markdown", "id": "ad81ba43", "metadata": {}, "source": [ "## πŸ₯… On-ice analytics\n", "\n", "Beyond the box score, the package derives advanced metrics from the same\n", "play-by-play feed:\n", "\n", "| Function | Metric |\n", "|---|---|\n", "| [`ahl_game_corsi`](../ahl/reference/additional.md#ahl_game_corsi) | Corsi / Fenwick shot-attempt share |\n", "| [`ahl_game_shifts`](../ahl/reference/additional.md#ahl_game_shifts) | shift charts (who's on the ice) |\n", "| [`ahl_player_toi`](../ahl/reference/additional.md#ahl_player_toi) | time-on-ice per player |\n", "| [`ahl_leaders`](../ahl/reference/additional.md#ahl_leaders) | statistical leaders |\n", "\n", "Heads-up: shift-derived metrics (`*_game_shifts`, `*_player_toi`,\n", "`*_game_corsi`) depend on the league publishing a shift feed for that game β€”\n", "when it's absent you'll get an empty frame rather than an error, which is why\n", "the recipes above guard on `height` before using them. πŸ›Ÿ" ] }, { "cell_type": "markdown", "id": "2b52342a", "metadata": {}, "source": [ "## πŸŽ‰ Where to next\n", "\n", "- The same `_*` calls work for **`ahl`**, **`ohl`**, **`whl`** and\n", " **`qmjhl`** β€” just swap the module.\n", "- Women's pro hockey? See the dedicated **PWHL** tutorial.\n", "- Full reference: the **AHL / OHL / WHL / QMJHL** pages in the sidebar.\n", "- Override a league's public key only if it rotates: `SDV__API_KEY`.\n", "\n", "Now go find the next first-overall pick! πŸ’" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🌍 All 19 minor & junior families\n", "\n", "`ahl`, `ohl`, `whl` and `qmjhl` above are four of the **19** HockeyTech families\n", "(PWHL has its own notebook). The other fifteen: `echl`, `sphl`, `chl`, `ushl`,\n", "`bchl`, `ajhl`, `sjhl`, `ojhl`, `cchl`, `gojhl`, `mhl`, `nojhl`, `vijhl`, `kijhl`\n", "and `mjhl`. Every family is minted by the same\n", "`hockeytech.build_family(league)`, so each one exposes the identical 13 callables β€”\n", "`_schedule`, `_pbp`, `_standings`, `_teams`, `_team_roster`,\n", "`_player_stats`, `_leaders`, `_game_summary`, `_game_shifts`,\n", "`_player_toi`, `_game_corsi`, `_season_id` and\n", "`most_recent__season`.\n", "\n", "**Two pbp caveats.** `ushl` and `mjhl` gamecenter feeds carry goals, penalties and\n", "goalie changes only β€” no coordinates. `mjhl` additionally denies `gc/gamesummary`,\n", "so `mjhl_game_summary` returns empty event frames and `mjhl_pbp` returns plays\n", "without game metadata.\n" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "import sportsdataverse as sdv\n", "from sportsdataverse.hockeytech._leagues import LEAGUES\n", "\n", "families = sorted(lg for lg in LEAGUES if lg != \"pwhl\")\n", "print(f\"{len(families)} families:\", \", \".join(families))\n", "\n", "# One standings call per family: the registry is the whole surface.\n", "for lg in families:\n", " fn = getattr(sdv, f\"{lg}_standings\", None)\n", " if fn is None:\n", " print(f\" {lg}: not exported at the top level\")\n", " continue\n", " safe(f\"{lg}_standings\", lambda fn=fn: fn())\n" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "# The two small-canvas / partial-feed families, called explicitly so the caveat is visible.\n", "for lg in (\"ushl\", \"mjhl\"):\n", " pbp = safe(f\"{lg}_pbp\", lambda lg=lg: getattr(sdv, f\"{lg}_pbp\")(\n", " game_id=getattr(sdv, f\"{lg}_schedule\")().row(0, named=True)[\"game_id\"]\n", " ))\n", " if pbp is not None:\n", " has_xy = {\"x_location\", \"y_location\"} & set(pbp.columns)\n", " print(f\" {lg}: {pbp.height} plays, coordinates present: {bool(has_xy)}\")\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }