{ "cells": [ { "cell_type": "markdown", "id": "4b5bd9cb", "metadata": {}, "source": [ "# 🏟️ Welcome to `sportsdataverse-py` β€” the cross-sport quickstart\n", "\n", "One `pip install`, **every** major league. `sportsdataverse` is a single Python\n", "package that speaks to the official, *premium* native data feeds across the\n", "sporting world β€” the same endpoints the leagues use to power their own apps β€”\n", "plus the **ESPN** mirror and pre-built **parquet release** loaders. Everything\n", "comes back as a tidy **polars** DataFrame, ready to model. πŸš€\n", "\n", "This page is your **map** to the whole package. By the end you'll be able to:\n", "\n", "1. πŸ—ΊοΈ **see every datasource** available for every league, with links straight\n", " to its tutorial and its reference index;\n", "2. 🧭 **predict function names you've never seen** β€” sportsdataverse uses one\n", " consistent naming contract, so *knowing one function tells you the others*;\n", "3. 🍳 cook through **~20 cross-sport recipes** that show the breadth in action.\n", "\n", "If you've used the R sisters β€” **hoopR, wehoop, cfbfastR, baseballr,\n", "fastRhockey, oddsapiR** β€” the names here will feel like home. Let's take the\n", "tour! 😊" ] }, { "cell_type": "markdown", "id": "7867992b", "metadata": {}, "source": [ "## πŸ—ΊοΈ 1 Β· The master index β€” every datasource, every league\n", "\n", "Here's the whole package on one page. Each row is a league (or the betting-odds\n", "module); each cell tells you which **datasource families** are wired up. πŸ’³ marks\n", "the **premium** native feeds (the leagues' own APIs / tracking systems / Statcast).\n", "Click a league's **tutorial** for the deep dive, or its **reference** for the full\n", "function index.\n", "\n", "| League | Tutorial Β· Reference | ESPN (`espn__*`) | Native premium API | Tracking / analytics | Release loaders (`load_*`) |\n", "|---|---|:---:|---|---|---|\n", "| πŸ€ **NBA** | [tutorial](../tutorials/04_nba_intro.md) Β· [ref](../nba/index.md) | βœ… | β€” | β€” | `load_nba_pbp`, `load_nba_team_boxscore` |\n", "| πŸ€ **WNBA** | [tutorial](../tutorials/08_wnba_intro.md) Β· [ref](../wnba/index.md) | βœ… | β€” | β€” | `load_wnba_pbp`, `load_wnba_player_boxscore` |\n", "| πŸ€ **MBB** (NCAA M) | [tutorial](../tutorials/06_mbb_intro.md) Β· [ref](../mbb/index.md) | βœ… | β€” | β€” | `load_mbb_pbp`, `load_mbb_team_boxscore` |\n", "| πŸ€ **WBB** (NCAA W) | [tutorial](../tutorials/05_wbb_intro.md) Β· [ref](../wbb/index.md) | βœ… | β€” | β€” | `load_wbb_pbp`, `load_wbb_team_boxscore` |\n", "| 🏈 **NFL** | [tutorial](../tutorials/03_nfl_intro.md) Β· [ref](../nfl/index.md) | βœ… | πŸ’³ `nfl_*` (`api.nfl.com`) | πŸ’³ Next Gen Stats `nfl_ngs_*` | `load_nfl_pbp`, `load_nfl_player_stats`, `load_injuries` |\n", "| 🏈 **CFB** (College) | [tutorial](../tutorials/02_cfb_intro.md) Β· [ref](../cfb/index.md) | βœ… | `yahoo_cfb_*`, `fox_cfb_*` | β€” | `load_cfb_pbp` |\n", "| ⚾ **MLB** | [tutorial](../tutorials/09_mlb_intro.md) Β· [ref](../mlb/index.md) | βœ… | πŸ’³ `mlb_*` (MLB Stats API) | πŸ’³ Statcast `mlb_statcast_*` | `load_mlb_pbp`, `load_mlb_team_boxscore` |\n", "| πŸ’ **NHL** | [tutorial](../tutorials/07_nhl_intro.md) Β· [ref](../nhl/index.md) | βœ… | πŸ’³ `nhl_*` (`api-web`) | πŸ’³ NHL EDGE `nhl_edge_*` | `load_nhl_pbp`, `load_nhl_team_boxscore` |\n", "| πŸ’ **PWHL** (Women's pro) | [tutorial](../tutorials/10_pwhl_intro.md) Β· [ref](../pwhl/index.md) | β€” | πŸ’³ `pwhl_*` (HockeyTech) | corsi / shifts / TOI | `load_pwhl_schedules` |\n", "| πŸ’ **AHL** (Minor pro) | [tutorial](../tutorials/11_junior_hockey_intro.md) Β· [ref](../ahl/index.md) | β€” | πŸ’³ `ahl_*` (HockeyTech) | corsi / shifts / TOI | β€” |\n", "| πŸ’ **OHL** (CHL junior) | [tutorial](../tutorials/11_junior_hockey_intro.md) Β· [ref](../ohl/index.md) | β€” | πŸ’³ `ohl_*` (HockeyTech) | corsi / shifts / TOI | β€” |\n", "| πŸ’ **WHL** (CHL junior) | [tutorial](../tutorials/11_junior_hockey_intro.md) Β· [ref](../whl/index.md) | β€” | πŸ’³ `whl_*` (HockeyTech) | corsi / shifts / TOI | β€” |\n", "| πŸ’ **QMJHL** (CHL junior) | [tutorial](../tutorials/11_junior_hockey_intro.md) Β· [ref](../qmjhl/index.md) | β€” | πŸ’³ `qmjhl_*` (HockeyTech) | corsi / shifts / TOI | β€” |\n", "| 🎲 **Betting odds** | [tutorial](../tutorials/12_odds_intro.md) Β· [ref](../odds/index.md) | β€” | πŸ’³ `toa_*` (The Odds API) | line history / props | β€” |\n", "\n", "> πŸ’‘ *HockeyTech leagues* (AHL/OHL/WHL/QMJHL/PWHL) ship public client keys β€” **no\n", "> setup needed**. Only the betting-odds module wants a free `ODDS_API_KEY`." ] }, { "cell_type": "markdown", "id": "52e2cd9f", "metadata": {}, "source": [ "### 🧩 The five function styles\n", "\n", "Across all those rows, only **five families** exist. Learn the shape of each\n", "once and you can read any function name in the package:\n", "\n", "1. **Live ESPN wrappers** β€” `espn__*` (e.g. `espn_nba_teams`,\n", " `espn_wbb_scoreboard`). The *same* set exists for every ESPN league: teams,\n", " rosters, scoreboards, standings, schedules, play-by-play, box scores. πŸͺž\n", "2. **Native premium API wrappers** β€” the league's own feed: `nfl_*` (`api.nfl.com`),\n", " `mlb_*` (MLB Stats API), `nhl_*` (`api-web`), `pwhl_*`/`ahl_*`/`ohl_*`/`whl_*`/`qmjhl_*`\n", " (HockeyTech), `toa_*` (The Odds API). πŸ’³\n", "3. **Tracking / analytics feeds** β€” the *really* premium stuff: `mlb_statcast_*`\n", " (Baseball Savant), `nhl_edge_*` (player tracking), `nfl_ngs_*` (Next Gen Stats).\n", "4. **Release / parquet loaders** β€” `load__*()` reads a pre-built parquet\n", " release (fast, reliable, whole-season-at-once): `load_nba_pbp`,\n", " `load_mlb_team_boxscore`, `load_pwhl_schedules`, …\n", "5. **Parser layer** β€” `parse_*` turns a raw native payload into a tidy frame\n", " (e.g. `parse_mlb_api_standings`). Most wrappers parse for you; the parsers are\n", " there when you fetch the raw `Dict` yourself.\n", "\n", "**The return contract never changes.** Every wrapper gives you **polars by\n", "default**; pass `return_as_pandas=True` for a pandas frame, and on the native\n", "APIs pass `return_parsed=False` for the raw JSON `Dict`. One contract, every\n", "sport. πŸŽ›οΈ" ] }, { "cell_type": "markdown", "id": "18491833", "metadata": {}, "source": [ "## πŸ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "# or\n", "uv add sportsdataverse\n", "```\n", "\n", "Every league is a submodule of the umbrella package, and the headline cross-league\n", "wrappers + discovery helpers are re-exported at the top level. Let's import it." ] }, { "cell_type": "code", "execution_count": null, "id": "b19c6a5a", "metadata": {}, "outputs": [], "source": [ "import os\n", "import polars as pl\n", "import sportsdataverse as sdv\n", "import sportsdataverse.odds as odds\n", "\n", "# Every league hangs off the top-level package:\n", "[m for m in dir(sdv) if m in\n", " (\"cfb\", \"nfl\", \"nba\", \"wnba\", \"mbb\", \"wbb\", \"nhl\", \"mlb\", \"pwhl\",\n", " \"ahl\", \"ohl\", \"whl\", \"qmjhl\", \"odds\")]" ] }, { "cell_type": "markdown", "id": "ba1e1c44", "metadata": {}, "source": [ "Live endpoints are seasonal and occasionally rate-limited, and the\n", "naming-convention loops below fan out **many** live calls at once β€” so a tiny\n", "`safe()` helper runs every 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", "That keeps this whole page runnable offline or in the off-season. πŸ›Ÿ" ] }, { "cell_type": "code", "execution_count": null, "id": "6bcb1336", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " '''Run a live call; return its result, or name the failure and return None.\n", "\n", " NoDataError means the fetch SUCCEEDED and there is nothing there (an\n", " out-of-season endpoint, a 404). AssetFetchError means the fetch FAILED and the\n", " answer is unknown (a 403, a rate limit, an exhausted retry budget). Collapsing\n", " the two into None is the silent-data-loss bug the error vocabulary exists to\n", " prevent, so the class is printed.\n", " '''\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", "# Odds is the only module that wants a (free) key β€” guard those cells:\n", "HAS_KEY = bool(os.environ.get(\"ODDS_API_KEY\"))\n", "print(\"ODDS_API_KEY set:\", HAS_KEY,\n", " \"β€” odds cells will\" + (\"\" if HAS_KEY else \" NOT\") + \" run live\")" ] }, { "cell_type": "markdown", "id": "189f4c69", "metadata": {}, "source": [ "## 🧭 2 Β· The naming-convention superpower\n", "\n", "Here's the centerpiece. **sportsdataverse names things so predictably that\n", "knowing one function name tells you the others.** The *same style* of data is\n", "exactly one rename away across every sport β€” swap the league slug and the call\n", "just works. Let's prove it. πŸͺ„" ] }, { "cell_type": "markdown", "id": "9664ffed", "metadata": {}, "source": [ "### πŸͺž The ESPN families are identical across every league\n", "\n", "`espn__teams`, `espn__team_roster`, `espn__scoreboard`,\n", "`espn__standings` exist for **every** ESPN league. A one-line helper +\n", "`getattr` tours them all and returns the **same shape** each time." ] }, { "cell_type": "code", "execution_count": null, "id": "a0ae466f", "metadata": {}, "outputs": [], "source": [ "def teams(league):\n", " '''Knowing one name (espn__teams) gives you all of them.'''\n", " return getattr(sdv, f\"espn_{league}_teams\")()\n", "\n", "rows = []\n", "for lg in [\"nba\", \"wnba\", \"nhl\", \"mlb\"]:\n", " df = safe(f\"espn_{lg}_teams\", lambda lg=lg: teams(lg))\n", " rows.append({\"league\": lg.upper(),\n", " \"fn\": f\"espn_{lg}_teams()\",\n", " \"n_teams\": None if df is None else df.height,\n", " \"n_cols\": None if df is None else df.width})\n", "\n", "pl.DataFrame(rows) # same columns, same shape β€” one contract, four leagues" ] }, { "cell_type": "markdown", "id": "e911e7c2", "metadata": {}, "source": [ "Same trick for the **scoreboard** and **standings** families β€” the call is\n", "identical, only the slug changes." ] }, { "cell_type": "code", "execution_count": null, "id": "5738ee73", "metadata": {}, "outputs": [], "source": [ "def call(family, league, **kw):\n", " '''Generic dispatcher: call(\"scoreboard\", \"nhl\") -> espn_nhl_scoreboard().'''\n", " return getattr(sdv, f\"espn_{league}_{family}\")(**kw)\n", "\n", "board = safe(\"espn_nfl_scoreboard\", lambda: call(\"scoreboard\", \"nfl\"))\n", "stand = safe(\"espn_nba_standings\", lambda: call(\"standings\", \"nba\"))\n", "print(\"NFL scoreboard rows:\", None if board is None else board.height,\n", " \"| NBA standings rows:\", None if stand is None else getattr(stand, \"height\", None))" ] }, { "cell_type": "markdown", "id": "06dbaceb", "metadata": {}, "source": [ "### πŸ“¦ The loaders follow one pattern too\n", "\n", "`load__pbp` and `load__team_boxscore` read pre-built parquet for\n", "**every** sport β€” same signature (`seasons=[...]`), same return type. Knowing\n", "`load_nba_pbp` means you already know `load_nhl_pbp` and `load_mlb_pbp`." ] }, { "cell_type": "code", "execution_count": null, "id": "1e750244", "metadata": {}, "outputs": [], "source": [ "# A single getattr loop loads play-by-play for four different sports:\n", "season = 2024\n", "for sport in [\"nba\", \"wnba\", \"nhl\"]:\n", " fn = getattr(sdv, f\"load_{sport}_pbp\")\n", " print(f\"load_{sport}_pbp(seasons=[{season}]) -> signature is identical for every sport\")\n", "# (we don't pull all of them here β€” that's a lot of parquet; Recipe 3 runs one.)" ] }, { "cell_type": "markdown", "id": "32d9c994", "metadata": {}, "source": [ "### πŸ’ The HockeyTech leagues share one surface\n", "\n", "AHL / OHL / WHL / QMJHL / PWHL all expose `_schedule`, `_standings`,\n", "`_teams`, `_team_roster`, and `most_recent__season`. Learn one, you\n", "learned all five." ] }, { "cell_type": "code", "execution_count": null, "id": "9bd43466", "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", "import sportsdataverse.pwhl as pwhl\n", "\n", "HOCKEYTECH = {\"ahl\": ahl, \"ohl\": ohl, \"whl\": whl, \"qmjhl\": qmjhl, \"pwhl\": pwhl}\n", "\n", "rows = []\n", "for lg, mod in HOCKEYTECH.items():\n", " season = safe(f\"most_recent_{lg}_season\", getattr(mod, f\"most_recent_{lg}_season\"))\n", " rows.append({\"league\": lg.upper(),\n", " \"schedule_fn\": f\"{lg}_schedule()\",\n", " \"standings_fn\": f\"{lg}_standings()\",\n", " \"season\": season})\n", "pl.DataFrame(rows)" ] }, { "cell_type": "markdown", "id": "4a028f05", "metadata": {}, "source": [ "### πŸ”Ž Discovery helpers β€” when you don't know the name yet\n", "\n", "Four top-level helpers let you *search* the surface instead of guessing:\n", "\n", "- `list_functions(league=None, search=..., parsers_only=..., wrappers_only=...)` β€” list/search every wrapper.\n", "- `function_count(league=None)` β€” how many functions each league exposes.\n", "- `find_team(name, league)` β€” fuzzy team lookup (returns the ESPN team dict + `id`).\n", "- `find_athlete(name, league)` β€” fuzzy player lookup." ] }, { "cell_type": "code", "execution_count": null, "id": "d58708e9", "metadata": {}, "outputs": [], "source": [ "# What does the package know about \"scoreboard\"? (grouped by league)\n", "hits = sdv.list_functions(search=\"scoreboard\")\n", "for lg, fns in hits.items():\n", " print(f\"{lg:>4}: {', '.join(fns)}\")" ] }, { "cell_type": "code", "execution_count": null, "id": "42785e4b", "metadata": {}, "outputs": [], "source": [ "# How big is each league's surface?\n", "counts = sdv.function_count()\n", "pl.DataFrame({\"league\": list(counts.keys()), \"n_functions\": list(counts.values())}) \\\n", " .sort(\"n_functions\", descending=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "1830fb66", "metadata": {}, "outputs": [], "source": [ "# Fuzzy lookups β€” no IDs to memorize:\n", "team = sdv.find_team(\"Lakers\", \"nba\")\n", "ath = sdv.find_athlete(\"LeBron\", \"nba\")\n", "print(\"team ->\", None if team is None else f\"{team['displayName']} (id={team['id']})\")\n", "print(\"athlete ->\", None if ath is None else f\"{ath['displayName']} (id={ath['id']})\")" ] }, { "cell_type": "markdown", "id": "2dec1550", "metadata": {}, "source": [ "## 🍳 3 Β· Twenty cross-sport recipes\n", "\n", "Now the fun part β€” **20 runnable recipes** that show the breadth *and* the\n", "overlap. Every recipe is defensively guarded, so a flaky network or off-season\n", "just prints a friendly note instead of erroring. Mix, match, and remix. πŸ‘‡" ] }, { "cell_type": "markdown", "id": "a4defa28", "metadata": {}, "source": [ "### Recipe 1 β€” Any league's teams πŸͺž\n", "\n", "`teams(\"\")` (our helper from above) hits `espn__teams` for any ESPN\n", "league. Here's the WBB team list." ] }, { "cell_type": "code", "execution_count": null, "id": "2df290f3", "metadata": {}, "outputs": [], "source": [ "wbb_teams = safe(\"espn_wbb_teams\", lambda: teams(\"wbb\"))\n", "cols = [\"team_id\", \"team_abbreviation\", \"team_display_name\", \"team_location\"]\n", "(wbb_teams.select([c for c in cols if c in wbb_teams.columns]).head()\n", " if wbb_teams is not None and wbb_teams.height else \"WBB teams unavailable right now\")" ] }, { "cell_type": "markdown", "id": "3fee5e2c", "metadata": {}, "source": [ "### Recipe 2 β€” Any league's scoreboard πŸ“‹\n", "\n", "`espn__scoreboard()` returns today's slate as a tidy frame. Same call for\n", "MLB, NBA, NHL β€” just change the slug." ] }, { "cell_type": "code", "execution_count": null, "id": "60fd5898", "metadata": {}, "outputs": [], "source": [ "sb = safe(\"espn_mlb_scoreboard\", lambda: sdv.espn_mlb_scoreboard())\n", "(sb.head() if sb is not None and getattr(sb, \"height\", 0)\n", " else \"no MLB games on the board right now\")" ] }, { "cell_type": "markdown", "id": "ade749b4", "metadata": {}, "source": [ "### Recipe 3 β€” Load any sport's season play-by-play πŸ“¦\n", "\n", "`load__pbp(seasons=[...])` reads the parquet release. One sport here\n", "(WNBA, a smaller season) to keep the download light." ] }, { "cell_type": "code", "execution_count": null, "id": "3f78c8f4", "metadata": {}, "outputs": [], "source": [ "wnba_pbp = safe(\"load_wnba_pbp([2024])\", lambda: sdv.load_wnba_pbp(seasons=[2024]))\n", "print(\"WNBA 2024 pbp rows:\", None if wnba_pbp is None else wnba_pbp.height)\n", "(wnba_pbp.select([c for c in [\"game_id\", \"period_number\", \"clock_display_value\", \"text\"]\n", " if c in wnba_pbp.columns]).head()\n", " if wnba_pbp is not None and wnba_pbp.height else \"pbp unavailable right now\")" ] }, { "cell_type": "markdown", "id": "3e3f4f03", "metadata": {}, "source": [ "### Recipe 4 β€” The same box-score shape for two different sports πŸͺž\n", "\n", "`load__team_boxscore` returns the same *kind* of frame for basketball and\n", "hockey. Load one season of each and compare the shapes." ] }, { "cell_type": "code", "execution_count": null, "id": "e3d3c0df", "metadata": {}, "outputs": [], "source": [ "nba_box = safe(\"load_nba_team_boxscore([2024])\", lambda: sdv.load_nba_team_boxscore(seasons=[2024]))\n", "nhl_box = safe(\"load_nhl_team_boxscore([2024])\", lambda: sdv.load_nhl_team_boxscore(seasons=[2024]))\n", "print(\"NBA team-box shape:\", None if nba_box is None else nba_box.shape)\n", "print(\"NHL team-box shape:\", None if nhl_box is None else nhl_box.shape)" ] }, { "cell_type": "markdown", "id": "43d979d6", "metadata": {}, "source": [ "### Recipe 5 β€” Standings for several leagues at once πŸ”\n", "\n", "One loop over `espn__standings` tours basketball, hockey, and baseball." ] }, { "cell_type": "code", "execution_count": null, "id": "852c9772", "metadata": {}, "outputs": [], "source": [ "rows = []\n", "for lg in [\"nba\", \"nhl\", \"mlb\"]:\n", " df = safe(f\"espn_{lg}_standings\", lambda lg=lg: getattr(sdv, f\"espn_{lg}_standings\")())\n", " rows.append({\"league\": lg.upper(),\n", " \"rows\": None if df is None else getattr(df, \"height\", None),\n", " \"cols\": None if df is None else getattr(df, \"width\", None)})\n", "pl.DataFrame(rows)" ] }, { "cell_type": "markdown", "id": "9f77caf0", "metadata": {}, "source": [ "### Recipe 6 β€” Find a team by name πŸ”Ž\n", "\n", "`find_team` fuzzy-matches across the ESPN leagues and hands back the team dict\n", "(with its `id`, ready to feed into a roster call)." ] }, { "cell_type": "code", "execution_count": null, "id": "5e0eaf2b", "metadata": {}, "outputs": [], "source": [ "for nm, lg in [(\"Patriots\", \"nfl\"), (\"Yankees\", \"mlb\"), (\"Bruins\", \"nhl\"), (\"Crimson Tide\", \"cfb\")]:\n", " t = sdv.find_team(nm, lg)\n", " print(f\"{lg:>3} {nm:<14} -> {None if t is None else t['displayName']} (id={None if t is None else t['id']})\")" ] }, { "cell_type": "markdown", "id": "7acc6c7f", "metadata": {}, "source": [ "### Recipe 7 β€” Find an athlete by name πŸƒ\n", "\n", "`find_athlete` does the same for players β€” great for grabbing an ESPN athlete\n", "`id` without leaving Python." ] }, { "cell_type": "code", "execution_count": null, "id": "9f593076", "metadata": {}, "outputs": [], "source": [ "for nm, lg in [(\"Caitlin Clark\", \"wnba\"), (\"Patrick Mahomes\", \"nfl\"), (\"Connor McDavid\", \"nhl\")]:\n", " a = sdv.find_athlete(nm, lg)\n", " print(f\"{lg:>4} {nm:<16} -> {None if a is None else a['displayName']} (id={None if a is None else a['id']})\")" ] }, { "cell_type": "markdown", "id": "a1319db0", "metadata": {}, "source": [ "### Recipe 8 β€” A team and its roster, end to end πŸ‘₯\n", "\n", "Chain `find_team` β†’ `espn__team_roster`: look up an ID by name, then pull the\n", "roster. The roster wrapper is parsed to polars by default." ] }, { "cell_type": "code", "execution_count": null, "id": "ca3b1716", "metadata": {}, "outputs": [], "source": [ "lal = sdv.find_team(\"Lakers\", \"nba\")\n", "roster = None\n", "if lal is not None:\n", " roster = safe(f\"espn_nba_team_roster(team_id={lal['id']})\",\n", " lambda: sdv.espn_nba_team_roster(team_id=lal[\"id\"], return_as_pandas=False))\n", "(roster.head() if roster is not None and getattr(roster, \"height\", 0)\n", " else \"roster unavailable right now\")" ] }, { "cell_type": "markdown", "id": "c35ecfe1", "metadata": {}, "source": [ "### Recipe 9 β€” polars β†’ pandas in one keyword 🐼\n", "\n", "Every wrapper honors `return_as_pandas=True`. Same data, different frame β€” handy\n", "when the next step (sklearn, statsmodels, seaborn) wants pandas." ] }, { "cell_type": "code", "execution_count": null, "id": "8c5ca6df", "metadata": {}, "outputs": [], "source": [ "teams_pl = safe(\"espn_wnba_teams (polars)\", lambda: sdv.espn_wnba_teams())\n", "teams_pd = safe(\"espn_wnba_teams (pandas)\", lambda: sdv.espn_wnba_teams(return_as_pandas=True))\n", "print(\"polars:\", type(teams_pl).__name__, None if teams_pl is None else teams_pl.shape)\n", "print(\"pandas:\", type(teams_pd).__name__, None if teams_pd is None else teams_pd.shape)" ] }, { "cell_type": "markdown", "id": "59415862", "metadata": {}, "source": [ "### Recipe 10 β€” The `return_parsed` toggle on a native API πŸŽ›οΈ\n", "\n", "Native API wrappers parse to polars by default; `return_parsed=False` hands back\n", "the raw JSON `Dict` straight from the league feed." ] }, { "cell_type": "code", "execution_count": null, "id": "e7a02a5c", "metadata": {}, "outputs": [], "source": [ "parsed = safe(\"nhl_standings (parsed)\", lambda: sdv.nhl.nhl_standings())\n", "raw = safe(\"nhl_standings (raw dict)\", lambda: sdv.nhl.nhl_standings(return_parsed=False))\n", "print(\"parsed ->\", type(parsed).__name__, None if parsed is None else getattr(parsed, \"shape\", None))\n", "print(\"raw ->\", type(raw).__name__, \"(top-level keys:\", None if not isinstance(raw, dict) else list(raw.keys())[:4], \")\")" ] }, { "cell_type": "markdown", "id": "696a4f3b", "metadata": {}, "source": [ "### Recipe 11 β€” 🏈 Premium NFL pull (`api.nfl.com`)\n", "\n", "`nfl_standings()` hits the league's own API and returns one tidy row per team." ] }, { "cell_type": "code", "execution_count": null, "id": "e1ac2a7d", "metadata": {}, "outputs": [], "source": [ "nfl_st = safe(\"nfl_standings (api.nfl.com)\", lambda: sdv.nfl.nfl_standings(season=2024, week=18))\n", "cols = [\"team_abbr\", \"team_full_name\", \"overall_wins\", \"overall_losses\",\n", " \"division_name\", \"conference_name\"]\n", "(nfl_st.select([c for c in cols if c in nfl_st.columns]).head(8)\n", " if nfl_st is not None and getattr(nfl_st, \"height\", 0) else \"NFL standings unavailable right now\")" ] }, { "cell_type": "markdown", "id": "3157a8f6", "metadata": {}, "source": [ "### Recipe 12 β€” ⚾ Premium MLB pull (MLB Stats API + parser)\n", "\n", "`mlb_*` wrappers return the raw `Dict`; pair them with the matching\n", "`parse_mlb_api_*` for a tidy frame. Here's division standings, parsed." ] }, { "cell_type": "code", "execution_count": null, "id": "412a029e", "metadata": {}, "outputs": [], "source": [ "def mlb_standings():\n", " raw = sdv.mlb.mlb_standings(league_id=\"103,104\", season=2024)\n", " return sdv.mlb.parse_mlb_api_standings(raw)\n", "\n", "mlb_st = safe(\"MLB standings (Stats API + parser)\", mlb_standings)\n", "keep = [\"standings_division_name\", \"team_name\", \"wins\", \"losses\", \"winning_percentage\", \"games_back\"]\n", "(mlb_st.select([c for c in keep if c in mlb_st.columns]).head(10)\n", " if mlb_st is not None and getattr(mlb_st, \"height\", 0) else \"MLB standings unavailable right now\")" ] }, { "cell_type": "markdown", "id": "6550ba80", "metadata": {}, "source": [ "### Recipe 13 β€” ⚾ MLB Statcast β€” the premium tracking firehose\n", "\n", "`mlb_statcast_search()` returns one row per pitch β€” the raw Baseball Savant tracking\n", "data. Grab a single day and pull a few of the most useful columns." ] }, { "cell_type": "code", "execution_count": null, "id": "a368dc36", "metadata": {}, "outputs": [], "source": [ "pitches = safe(\"mlb_statcast_search (1 day)\",\n", " lambda: sdv.mlb.mlb_statcast_search(start_dt=\"2024-07-01\", end_dt=\"2024-07-01\"))\n", "show = [c for c in [\"game_date\", \"player_name\", \"pitch_type\", \"release_speed\",\n", " \"launch_speed\", \"launch_angle\", \"events\"]\n", " if pitches is not None and c in pitches.columns]\n", "(pitches.select(show).head(10)\n", " if pitches is not None and getattr(pitches, \"height\", 0) else \"no Statcast rows for that day right now\")" ] }, { "cell_type": "markdown", "id": "78b91acb", "metadata": {}, "source": [ "### Recipe 14 β€” πŸ’ Premium NHL pull (`api-web`)\n", "\n", "`nhl_standings()` reads the modern NHL `api-web` feed β€” one row per team, parsed\n", "to polars." ] }, { "cell_type": "code", "execution_count": null, "id": "0af7aaea", "metadata": {}, "outputs": [], "source": [ "nhl_st = safe(\"nhl_standings (api-web)\", lambda: sdv.nhl.nhl_standings())\n", "keep = [\"team_abbrev\", \"team_name\", \"wins\", \"losses\", \"ot_losses\", \"points\",\n", " \"conference_name\", \"division_name\"]\n", "(nhl_st.select([c for c in keep if c in nhl_st.columns]).head(8)\n", " if nhl_st is not None and getattr(nhl_st, \"height\", 0) else \"NHL standings unavailable right now\")" ] }, { "cell_type": "markdown", "id": "d08ae67e", "metadata": {}, "source": [ "### Recipe 15 β€” πŸ’ NHL EDGE tracking leaderboard\n", "\n", "NHL EDGE is the league's player- and puck-tracking system. The\n", "`nhl_edge_skater_speed_top_10` board surfaces the fastest skating bursts." ] }, { "cell_type": "code", "execution_count": null, "id": "58e20224", "metadata": {}, "outputs": [], "source": [ "edge = safe(\"nhl_edge_skater_speed_top_10\",\n", " lambda: sdv.nhl.nhl_edge_skater_speed_top_10(positions=\"forwards\",\n", " sort_by=\"maxskatingspeed\"))\n", "(edge.head(10) if edge is not None and getattr(edge, \"height\", 0)\n", " else \"NHL EDGE leaderboard unavailable right now\")" ] }, { "cell_type": "markdown", "id": "f590d15b", "metadata": {}, "source": [ "### Recipe 16 β€” πŸ’ Premium PWHL pull (HockeyTech)\n", "\n", "The women's pro league rides the HockeyTech feed. `pwhl_standings()` returns the\n", "table; `load_pwhl_schedules()` reads the parquet release for a whole season." ] }, { "cell_type": "code", "execution_count": null, "id": "b06b921b", "metadata": {}, "outputs": [], "source": [ "pwhl_st = safe(\"pwhl_standings\", lambda: sdv.pwhl.pwhl_standings(season=sdv.pwhl.most_recent_pwhl_season()))\n", "pwhl_sched = safe(\"load_pwhl_schedules([2024])\", lambda: sdv.pwhl.load_pwhl_schedules(seasons=[2024]))\n", "print(\"standings rows:\", None if pwhl_st is None else getattr(pwhl_st, \"height\", None),\n", " \"| schedule rows:\", None if pwhl_sched is None else getattr(pwhl_sched, \"height\", None))\n", "(pwhl_st.head() if pwhl_st is not None and getattr(pwhl_st, \"height\", 0)\n", " else \"PWHL standings unavailable right now\")" ] }, { "cell_type": "markdown", "id": "604767b0", "metadata": {}, "source": [ "### Recipe 17 β€” πŸ’ Junior hockey: schedule for all four CHL/AHL loops πŸ”\n", "\n", "Because AHL/OHL/WHL/QMJHL share one surface, a single loop tours every league's\n", "schedule." ] }, { "cell_type": "code", "execution_count": null, "id": "82175b00", "metadata": {}, "outputs": [], "source": [ "rows = []\n", "for lg, mod in {\"ahl\": ahl, \"ohl\": ohl, \"whl\": whl, \"qmjhl\": qmjhl}.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\")())\n", " if season else None)\n", " rows.append({\"league\": lg.upper(), \"season\": season,\n", " \"games\": None if sch is None else getattr(sch, \"height\", None)})\n", "pl.DataFrame(rows)" ] }, { "cell_type": "markdown", "id": "c38a0457", "metadata": {}, "source": [ "### Recipe 18 β€” 🎲 A quick odds peek (key-guarded)\n", "\n", "`odds.toa_sports()` lists every in-season sport/league key β€” it's **free**\n", "(doesn't touch your quota). Set a free `ODDS_API_KEY` to light it up." ] }, { "cell_type": "code", "execution_count": null, "id": "ce65a117", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " sports = safe(\"odds.toa_sports\", lambda: odds.toa_sports(all_sports=False))\n", " out = (sports.select([c for c in [\"key\", \"group\", \"title\"] if c in sports.columns]).head(10)\n", " if sports is not None and getattr(sports, \"height\", 0) else \"no in-season sports returned\")\n", "else:\n", " out = \"set ODDS_API_KEY to run: odds.toa_sports() (free, doesn't touch quota)\"\n", "out" ] }, { "cell_type": "markdown", "id": "e927d2a3", "metadata": {}, "source": [ "### Recipe 19 β€” 🎲 Live odds for a league (key-guarded)\n", "\n", "`odds.toa_sports_odds()` returns **long-format** odds β€” one row per\n", "event Γ— book Γ— market Γ— outcome β€” exactly the shape you want for modelling." ] }, { "cell_type": "code", "execution_count": null, "id": "a8fd7e30", "metadata": {}, "outputs": [], "source": [ "if HAS_KEY:\n", " board = safe(\"odds.toa_sports_odds (NFL h2h)\",\n", " lambda: odds.toa_sports_odds(sport=\"americanfootball_nfl\", regions=\"us\", markets=\"h2h\"))\n", " keep = [\"home_team\", \"away_team\", \"bookmaker_key\", \"market_key\", \"outcome_name\", \"outcome_price\"]\n", " out = (board.select([c for c in keep if c in board.columns]).head(10)\n", " if board is not None and getattr(board, \"height\", 0) else \"no NFL odds on the board right now\")\n", "else:\n", " out = \"set ODDS_API_KEY to run: odds.toa_sports_odds(sport='americanfootball_nfl')\"\n", "out" ] }, { "cell_type": "markdown", "id": "53f33317", "metadata": {}, "source": [ "### Recipe 20 β€” Count the whole surface, per league πŸ”’\n", "\n", "`function_count()` returns the exposed-function tally for every league β€” a quick\n", "sense of how much each sport gives you. (HockeyTech + odds modules are counted in\n", "their own submodules.)" ] }, { "cell_type": "code", "execution_count": null, "id": "d7a1f089", "metadata": {}, "outputs": [], "source": [ "counts = sdv.function_count()\n", "df = (pl.DataFrame({\"league\": list(counts.keys()), \"n_functions\": list(counts.values())})\n", " .sort(\"n_functions\", descending=True))\n", "print(\"Total wrappers across the counted leagues:\", sum(counts.values()))\n", "df" ] }, { "cell_type": "markdown", "id": "3883e86c", "metadata": {}, "source": [ "## πŸŽ‰ Where to next\n", "\n", "You've now seen the **whole map** β€” every datasource, the naming contract that\n", "makes the package guessable, and 20 recipes spanning ten-plus leagues. Each\n", "sport has a dedicated tutorial that leads with its premium endpoints:\n", "\n", "- [`02_cfb_intro`](../tutorials/02_cfb_intro.md) β€” 🏈 college football\n", "- [`03_nfl_intro`](../tutorials/03_nfl_intro.md) β€” 🏈 NFL (`api.nfl.com` + nflverse)\n", "- [`04_nba_intro`](../tutorials/04_nba_intro.md) β€” πŸ€ NBA\n", "- [`05_wbb_intro`](../tutorials/05_wbb_intro.md) β€” πŸ€ NCAA women's basketball\n", "- [`06_mbb_intro`](../tutorials/06_mbb_intro.md) β€” πŸ€ NCAA men's basketball\n", "- [`07_nhl_intro`](../tutorials/07_nhl_intro.md) β€” πŸ’ NHL (`api-web` + EDGE + ESPN)\n", "- [`08_wnba_intro`](../tutorials/08_wnba_intro.md) β€” πŸ€ WNBA\n", "- [`09_mlb_intro`](../tutorials/09_mlb_intro.md) β€” ⚾ MLB (Stats API + Statcast + ESPN)\n", "- [`10_pwhl_intro`](../tutorials/10_pwhl_intro.md) β€” πŸ’ PWHL\n", "- [`11_junior_hockey_intro`](../tutorials/11_junior_hockey_intro.md) β€” πŸ’ AHL / OHL / WHL / QMJHL\n", "- [`12_odds_intro`](../tutorials/12_odds_intro.md) β€” 🎲 Betting odds (The Odds API)\n", "\n", "**Reference indexes:**\n", "[NBA](../nba/index.md) Β· [WNBA](../wnba/index.md) Β· [MBB](../mbb/index.md) Β·\n", "[WBB](../wbb/index.md) Β· [NFL](../nfl/index.md) Β· [CFB](../cfb/index.md) Β·\n", "[MLB](../mlb/index.md) Β· [NHL](../nhl/index.md) Β· [PWHL](../pwhl/index.md) Β·\n", "[AHL](../ahl/index.md) Β· [OHL](../ohl/index.md) Β· [WHL](../whl/index.md) Β·\n", "[QMJHL](../qmjhl/index.md) Β· [Odds](../odds/index.md).\n", "\n", "Part of the **[SportsDataverse](https://www.sportsdataverse.org)** β€” the names\n", "here mirror the R sisters (hoopR, wehoop, cfbfastR, baseballr, fastRhockey,\n", "oddsapiR). Now go build something great! πŸ†" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## πŸ“ˆ Derived surfaces\n", "\n", "Three package-level helpers that work on any frame you have already loaded β€”\n", "rolling form windows, rate curves along a continuous axis, and the metric registry\n", "that names and resolves every published metric (`sportsdataverse.registry`).\n" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sportsdataverse import metric_curves, rolling_windows\n", "import sportsdataverse.registry as registry\n", "\n", "print(\"rolling_windows:\", [n for n in dir(rolling_windows) if not n.startswith(\"_\")][:6])\n", "print(\"metric_curves: \", [n for n in dir(metric_curves) if not n.startswith(\"_\")][:6])\n", "print(\"registry: \", [n for n in dir(registry) if not n.startswith(\"_\")][:6])\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }