{ "cells": [ { "cell_type": "markdown", "id": "35c72d8e", "metadata": {}, "source": [ "# πŸ€ Men's college basketball with `sportsdataverse-py`\n", "\n", "Welcome to **Selection-Sunday-grade** hoops data! πŸŽ‰ In a handful of lines of\n", "Python you're about to pull NCAA Division I men's basketball β€” full schedules,\n", "play-by-play, standings, rosters, statistical leaders and multi-season parquet\n", "archives β€” and get it all back as tidy **polars** DataFrames ready to model.\n", "\n", "`sportsdataverse.mbb` leads with two premium sources:\n", "\n", "- πŸŸ₯ **ESPN** (`espn_mbb_*`) β€” the site + core APIs behind ESPN.com: live\n", " scoreboards, schedules, standings, rankings, box scores, win probability and\n", " play-by-play.\n", "- 🦊 **FoxSports** (`fox_mbb_*`) β€” FoxSports' league-leader, standings, roster,\n", " boxscore and odds feeds.\n", "\n", "Plus πŸ“¦ **release loaders** (`load_mbb_*`) that hand you whole seasons of\n", "play-by-play, box scores, shots and schedules from the data repo in one call.\n", "\n", "R user? The men's-basketball companion is\n", "[hoopR](https://hoopR.sportsdataverse.org) (NBA + NCAA). Let's tip off! πŸ€" ] }, { "cell_type": "markdown", "id": "511e142b", "metadata": {}, "source": [ "## 🧰 The toolbox\n", "\n", "Every accessor returns a tidy **polars** `DataFrame` by default β€” pass\n", "`return_as_pandas=True` for pandas. The richest live surfaces are ESPN and Fox;\n", "the `load_*` loaders read pre-built parquet from the data release (rock-solid,\n", "no live API). Click any name for the full reference.\n", "\n", "| Function | What it gives you | Source |\n", "|---|---|---|\n", "| [`espn_mbb_teams`](../mbb/reference/additional.md#espn_mbb_teams) | Every D-I team (grab `team_id`s) | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_schedule`](../mbb/reference/additional.md#espn_mbb_schedule) | Games + results for a date / window | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_scoreboard`](../mbb/reference/site.md#espn_mbb_scoreboard) | Rich scoreboard for a date (status, lines, odds) | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_standings`](../mbb/reference/site.md#espn_mbb_standings) | Conference standings, one row per team | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_rankings`](../mbb/reference/site.md#espn_mbb_rankings) | AP / Coaches poll (in-season) | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_summary`](../mbb/reference/site.md#espn_mbb_summary) | Full game summary: box, plays, win prob | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_team_roster`](../mbb/reference/site.md#espn_mbb_team_roster) | A team's roster | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_pbp`](../mbb/reference/additional.md#espn_mbb_pbp) | Event-level play-by-play for a game | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_game_rosters`](../mbb/reference/additional.md#espn_mbb_game_rosters) | Who dressed + started for one game | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_player_stats`](../mbb/reference/additional.md#espn_mbb_player_stats) | A player's season stat line | πŸŸ₯ ESPN ⭐ |\n", "| [`fox_mbb_league_leaders`](../mbb/reference/additional.md#fox_mbb_league_leaders) | Stat leaders (scoring, rebounds, …) | 🦊 Fox ⭐ |\n", "| [`fox_mbb_standings`](../mbb/reference/additional.md#fox_mbb_standings) | Fox conference standings for a team | 🦊 Fox ⭐ |\n", "| [`fox_mbb_team_roster`](../mbb/reference/additional.md#fox_mbb_team_roster) | Fox roster for a team | 🦊 Fox |\n", "| [`espn_mbb_team_schedule`](../mbb/reference/additional.md#espn_mbb_team_schedule) | One team's full season schedule | πŸŸ₯ ESPN ⭐ |\n", "| [`espn_mbb_conferences`](../mbb/reference/additional.md#espn_mbb_conferences) | Conference / group catalog | πŸŸ₯ ESPN ⭐ |\n", "| [`load_mbb_schedule`](../mbb/reference/loaders.md#load_mbb_schedule) | Whole-season schedule parquet | πŸ“¦ loader |\n", "| [`load_mbb_player_boxscore`](../mbb/reference/loaders.md#load_mbb_player_boxscore) | Season player box scores | πŸ“¦ loader |\n", "| [`load_mbb_team_boxscore`](../mbb/reference/loaders.md#load_mbb_team_boxscore) | Season team box scores | πŸ“¦ loader |\n", "| [`load_mbb_pbp`](../mbb/reference/loaders.md#load_mbb_pbp) | Season play-by-play parquet | πŸ“¦ loader |\n", "| [`most_recent_mbb_season`](../mbb/reference/additional.md#most_recent_mbb_season) | Current season-year helper | πŸ› οΈ helper |\n", "\n", "⭐ = premium live source." ] }, { "cell_type": "markdown", "id": "e264b512", "metadata": {}, "source": [ "## πŸ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "No API key needed β€” ESPN, Fox and the parquet loaders are all open. 😊" ] }, { "cell_type": "code", "execution_count": null, "id": "23a5d04d", "metadata": {}, "outputs": [], "source": [ "import polars as pl\n", "import sportsdataverse as sdv\n", "\n", "pl.Config.set_tbl_rows(10)\n", "print(\"most recent MBB season:\", sdv.mbb.most_recent_mbb_season())" ] }, { "cell_type": "markdown", "id": "157ffa24", "metadata": {}, "source": [ "ESPN's *live* endpoints (scoreboard, rankings, standings, a single game's\n", "play-by-play) are seasonal β€” in the offseason a poll or scoreboard can come\n", "back empty. So we use a tiny `safe()` helper: you get the frame when the feed\n", "is up, and a friendly one-liner when it isn't β€” never a scary traceback. πŸ›Ÿ\n", "The `load_*` parquet loaders are stable year-round, so we call those directly." ] }, { "cell_type": "code", "execution_count": null, "id": "7c0e7df9", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " \"\"\"Run a live call defensively; return its result or None with a note.\"\"\"\n", " try:\n", " out = thunk()\n", " ok = out is not None and (not hasattr(out, \"height\") or out.height)\n", " print(f\"{'βœ…' if ok else 'ℹ️ '} {label}{'' if ok else ' β€” no rows right now'}\")\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": "e27ca653", "metadata": {}, "source": [ "## 🏟️ Every team in Division I\n", "\n", "Start with [`espn_mbb_teams`](../mbb/reference/additional.md#espn_mbb_teams) β€”\n", "one row per program, with the `team_id` you'll pass into roster, schedule and\n", "summary calls. This is a plain catalog fetch, so it's reliable year-round." ] }, { "cell_type": "code", "execution_count": null, "id": "cc26a766", "metadata": {}, "outputs": [], "source": [ "teams = sdv.mbb.espn_mbb_teams()\n", "print(\"teams:\", teams.shape)\n", "teams.select([\"team_id\", \"team_location\", \"team_name\", \"team_abbreviation\", \"team_is_active\"]).head()" ] }, { "cell_type": "markdown", "id": "0c14a093", "metadata": {}, "source": [ "## πŸ“… Schedule & scores for a date window\n", "\n", "[`espn_mbb_schedule`](../mbb/reference/additional.md#espn_mbb_schedule) takes a\n", "single `dates=YYYYMMDD` or a `'YYYYMMDD-YYYYMMDD'` window and returns one row\n", "per game with final scores. Here's championship day of the 2024 tournament." ] }, { "cell_type": "code", "execution_count": null, "id": "04a10db8", "metadata": {}, "outputs": [], "source": [ "sched = safe(\n", " \"schedule 2024-04-08\",\n", " lambda: sdv.mbb.espn_mbb_schedule(dates=20240408),\n", ")\n", "(sched.select([\"id\", \"home_display_name\", \"away_display_name\", \"home_score\", \"away_score\"]).head()\n", " if sched is not None and sched.height else \"schedule unavailable\")" ] }, { "cell_type": "markdown", "id": "40e7f7f3", "metadata": {}, "source": [ "## πŸ“Š The rich scoreboard\n", "\n", "[`espn_mbb_scoreboard`](../mbb/reference/site.md#espn_mbb_scoreboard) is the\n", "deluxe version: for a given date it returns status, broadcast, betting lines and\n", "team line scores β€” 50 columns wide. Defaults to polars; we peek at a tidy slice." ] }, { "cell_type": "code", "execution_count": null, "id": "c56300c5", "metadata": {}, "outputs": [], "source": [ "sb = safe(\n", " \"scoreboard 2024-04-08\",\n", " lambda: sdv.mbb.espn_mbb_scoreboard(dates=20240408, return_as_pandas=False),\n", ")\n", "if sb is not None and getattr(sb, \"height\", 0):\n", " keep = [\"game_id\", \"short_name\", \"status_type_description\",\n", " \"home_team_short_display_name\", \"away_team_short_display_name\"]\n", " out = sb.select([c for c in keep if c in sb.columns]).head()\n", "else:\n", " out = \"scoreboard empty right now (offseason)\"\n", "out" ] }, { "cell_type": "markdown", "id": "5675dc5a", "metadata": {}, "source": [ "## πŸ† Conference standings\n", "\n", "[`espn_mbb_standings`](../mbb/reference/site.md#espn_mbb_standings) returns one\n", "row per team for a season with wins, losses, win pct, point differential and\n", "conference grouping. Great for a quick power look across the league." ] }, { "cell_type": "code", "execution_count": null, "id": "658b0503", "metadata": {}, "outputs": [], "source": [ "standings = safe(\n", " \"standings 2024\",\n", " lambda: sdv.mbb.espn_mbb_standings(season=2024, return_as_pandas=False),\n", ")\n", "if standings is not None and getattr(standings, \"height\", 0):\n", " keep = [\"team_display_name\", \"group_name\", \"wins\", \"losses\",\n", " \"win_percent\", \"point_differential\"]\n", " out = (standings.select([c for c in keep if c in standings.columns])\n", " .sort(\"win_percent\", descending=True).head(10))\n", "else:\n", " out = \"standings unavailable\"\n", "out" ] }, { "cell_type": "markdown", "id": "3db3d78a", "metadata": {}, "source": [ "## 🍳 Cookbook: common MBB tasks\n", "\n", "Now the fun part β€” real tasks you'll reach for constantly, each built on a\n", "premium ESPN or Fox wrapper. Every recipe is guarded so a transient or\n", "offseason hiccup prints a note instead of breaking the page." ] }, { "cell_type": "markdown", "id": "01a95c8e", "metadata": {}, "source": [ "### Recipe 1 β€” National scoring leaders πŸ₯‡ (FoxSports)\n", "\n", "[`fox_mbb_league_leaders`](../mbb/reference/additional.md#fox_mbb_league_leaders)\n", "serves the leaderboard direct from FoxSports β€” pick a `category` (`scoring`,\n", "`rebounds`, `assists`, …) and `who` (`player` or `team`). No IDs needed." ] }, { "cell_type": "code", "execution_count": null, "id": "adbbb0cf", "metadata": {}, "outputs": [], "source": [ "leaders = safe(\n", " \"fox scoring leaders\",\n", " lambda: sdv.mbb.fox_mbb_league_leaders(category=\"scoring\", who=\"player\"),\n", ")\n", "if leaders is not None and getattr(leaders, \"height\", 0):\n", " keep = [\"players\", \"gp\", \"mpg\", \"ppg\", \"pts\"]\n", " out = leaders.select([c for c in keep if c in leaders.columns]).head(10)\n", "else:\n", " out = \"Fox leaders unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "f0ddd705", "metadata": {}, "source": [ "### Recipe 2 β€” Look up a team's roster πŸ‘₯ (ESPN)\n", "\n", "Grab a `team_id` from `espn_mbb_teams`, then\n", "[`espn_mbb_team_roster`](../mbb/reference/site.md#espn_mbb_team_roster) returns\n", "the current roster. Here we resolve UConn (the 2024 champs) by abbreviation so\n", "the recipe is self-contained." ] }, { "cell_type": "code", "execution_count": null, "id": "47379409", "metadata": {}, "outputs": [], "source": [ "row = teams.filter(pl.col(\"team_abbreviation\") == \"CONN\")\n", "tid = int(row[\"team_id\"][0]) if row.height else 41 # 41 = UConn fallback\n", "roster = safe(\n", " f\"roster team_id={tid}\",\n", " lambda: sdv.mbb.espn_mbb_team_roster(team_id=tid, return_as_pandas=False),\n", ")\n", "if roster is not None and getattr(roster, \"height\", 0):\n", " keep = [\"full_name\", \"jersey\", \"display_height\", \"display_weight\"]\n", " out = roster.select([c for c in keep if c in roster.columns]).head(12)\n", "else:\n", " out = \"roster unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "24efcbbb", "metadata": {}, "source": [ "### Recipe 3 β€” Season scoring leaderboard from parquet πŸ“¦\n", "\n", "The `load_*` loaders pull whole seasons from the data release β€” perfect for\n", "analysis that shouldn't depend on a live endpoint.\n", "[`load_mbb_player_boxscore`](../mbb/reference/loaders.md#load_mbb_player_boxscore)\n", "gives every player-game; we aggregate to a per-player points-per-game board." ] }, { "cell_type": "code", "execution_count": null, "id": "006e98a2", "metadata": {}, "outputs": [], "source": [ "pbox = sdv.mbb.load_mbb_player_boxscore(seasons=[2024])\n", "print(\"player box rows:\", pbox.shape)\n", "(pbox\n", " .filter(pl.col(\"points\").is_not_null())\n", " .group_by([\"athlete_display_name\", \"team_short_display_name\"])\n", " .agg(\n", " pl.len().alias(\"g\"),\n", " pl.col(\"points\").cast(pl.Float64, strict=False).mean().round(1).alias(\"ppg\"),\n", " )\n", " .filter(pl.col(\"g\") >= 20)\n", " .sort(\"ppg\", descending=True)\n", " .head(10))" ] }, { "cell_type": "markdown", "id": "e8beba8b", "metadata": {}, "source": [ "### Recipe 4 β€” Play-by-play slice for one game 🎬 (ESPN)\n", "\n", "[`espn_mbb_pbp`](../mbb/reference/additional.md#espn_mbb_pbp) returns a dict;\n", "its `plays` list is event-level. We frame it and pull just the scoring plays of\n", "the 2024 national championship (UConn vs. Purdue, `game_id=401638636`)." ] }, { "cell_type": "code", "execution_count": null, "id": "bf20fe2f", "metadata": {}, "outputs": [], "source": [ "pbp = safe(\"pbp 401638636\", lambda: sdv.mbb.espn_mbb_pbp(game_id=401638636))\n", "if isinstance(pbp, dict) and pbp.get(\"plays\"):\n", " plays = pl.DataFrame(pbp[\"plays\"], infer_schema_length=None)\n", " keep = [\"period.number\", \"clock.displayValue\", \"text\", \"scoringPlay\",\n", " \"homeScore\", \"awayScore\"]\n", " out = (plays.select([c for c in keep if c in plays.columns])\n", " .filter(pl.col(\"scoringPlay\") == True) # noqa: E712\n", " .head(10))\n", "else:\n", " out = \"play-by-play unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "977084c7", "metadata": {}, "source": [ "### Recipe 5 β€” Best net scoring margin πŸ“Š (parquet)\n", "\n", "[`load_mbb_team_boxscore`](../mbb/reference/loaders.md#load_mbb_team_boxscore) gives one row per team-game with the opponent's score attached, so a single group-by ranks every program by points scored minus points allowed β€” the cleanest one-number power proxy. Pure parquet, no live endpoint." ] }, { "cell_type": "code", "execution_count": null, "id": "04489a8f", "metadata": {}, "outputs": [], "source": [ "tbox = sdv.mbb.load_mbb_team_boxscore(seasons=[2024])\n", "print(\"team box rows:\", tbox.shape)\n", "(tbox\n", " .group_by(\"team_display_name\")\n", " .agg(\n", " pl.len().alias(\"g\"),\n", " pl.col(\"team_score\").cast(pl.Float64, strict=False).mean().round(1).alias(\"ppg\"),\n", " pl.col(\"opponent_team_score\").cast(pl.Float64, strict=False).mean().round(1).alias(\"opp_ppg\"),\n", " )\n", " .with_columns((pl.col(\"ppg\") - pl.col(\"opp_ppg\")).round(1).alias(\"net_margin\"))\n", " .filter(pl.col(\"g\") >= 25)\n", " .sort(\"net_margin\", descending=True)\n", " .head(10))" ] }, { "cell_type": "markdown", "id": "9f6a0030", "metadata": {}, "source": [ "### Recipe 6 β€” Best 3-point shooting teams 🎯 (parquet)\n", "\n", "Same team-box parquet, different question: sum makes and attempts across the season, then divide. A `min attempts` filter keeps small-sample flukes off the board so the leaders are real volume shooters." ] }, { "cell_type": "code", "execution_count": null, "id": "5d94e6a9", "metadata": {}, "outputs": [], "source": [ "(tbox\n", " .group_by(\"team_display_name\")\n", " .agg(\n", " pl.col(\"three_point_field_goals_made\")\n", " .cast(pl.Float64, strict=False).sum().alias(\"tpm\"),\n", " pl.col(\"three_point_field_goals_attempted\")\n", " .cast(pl.Float64, strict=False).sum().alias(\"tpa\"),\n", " )\n", " .with_columns((pl.col(\"tpm\") / pl.col(\"tpa\") * 100).round(1).alias(\"three_pct\"))\n", " .filter(pl.col(\"tpa\") >= 500)\n", " .sort(\"three_pct\", descending=True)\n", " .select([\"team_display_name\", \"tpm\", \"tpa\", \"three_pct\"])\n", " .head(10))" ] }, { "cell_type": "markdown", "id": "adbfb286", "metadata": {}, "source": [ "### Recipe 7 β€” Most efficient scorers ⚑ (true shooting %)\n", "\n", "Points-per-game rewards volume; **true shooting %** rewards *efficiency* β€” it folds threes and free throws into one rate via `TS% = PTS / (2 Β· (FGA + 0.44Β·FTA))`. We compute it straight from [`load_mbb_player_boxscore`](../mbb/reference/loaders.md#load_mbb_player_boxscore), keeping only high-usage scorers." ] }, { "cell_type": "code", "execution_count": null, "id": "a5ce7e9f", "metadata": {}, "outputs": [], "source": [ "pbox = sdv.mbb.load_mbb_player_boxscore(seasons=[2024])\n", "(pbox\n", " .filter(pl.col(\"points\").is_not_null())\n", " .group_by([\"athlete_display_name\", \"team_abbreviation\"])\n", " .agg(\n", " pl.len().alias(\"g\"),\n", " pl.col(\"points\").cast(pl.Float64, strict=False).sum().alias(\"pts\"),\n", " pl.col(\"field_goals_attempted\").cast(pl.Float64, strict=False).sum().alias(\"fga\"),\n", " pl.col(\"free_throws_attempted\").cast(pl.Float64, strict=False).sum().alias(\"fta\"),\n", " )\n", " .with_columns(\n", " (pl.col(\"pts\") / (2 * (pl.col(\"fga\") + 0.44 * pl.col(\"fta\"))) * 100)\n", " .round(1).alias(\"ts_pct\"))\n", " .filter((pl.col(\"g\") >= 25) & (pl.col(\"pts\") >= 400))\n", " .sort(\"ts_pct\", descending=True)\n", " .select([\"athlete_display_name\", \"team_abbreviation\", \"g\", \"pts\", \"ts_pct\"])\n", " .head(10))" ] }, { "cell_type": "markdown", "id": "425edcb3", "metadata": {}, "source": [ "### Recipe 8 β€” One conference's power board 🏟️ (ESPN, join)\n", "\n", "[`espn_mbb_conferences`](../mbb/reference/additional.md#espn_mbb_conferences) is the group catalog; [`espn_mbb_standings`](../mbb/reference/site.md#espn_mbb_standings) carries a `group_name` per team. Filter standings to a single league β€” here the Big 12 β€” to get a clean intra-conference pecking order." ] }, { "cell_type": "code", "execution_count": null, "id": "39e823c8", "metadata": {}, "outputs": [], "source": [ "confs = safe(\"conferences\", lambda: sdv.mbb.espn_mbb_conferences())\n", "if confs is not None and getattr(confs, \"height\", 0):\n", " print(\"some conferences:\",\n", " confs.filter(pl.col(\"is_conference\"))[\"name\"].to_list()[:8])\n", "st = safe(\"standings 2024\", lambda: sdv.mbb.espn_mbb_standings(season=2024))\n", "if st is not None and getattr(st, \"height\", 0) and \"group_name\" in st.columns:\n", " keep = [\"team_display_name\", \"wins\", \"losses\", \"win_percent\", \"point_differential\"]\n", " out = (st.filter(pl.col(\"group_name\").str.contains(\"Big 12\"))\n", " .select([c for c in keep if c in st.columns])\n", " .sort(\"win_percent\", descending=True)\n", " .head(12))\n", " out = out if out.height else st.select(\n", " [c for c in keep if c in st.columns]).sort(\n", " \"win_percent\", descending=True).head(12)\n", "else:\n", " out = \"standings unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "4856d47f", "metadata": {}, "source": [ "### Recipe 9 β€” A team's full season schedule πŸ—“οΈ (ESPN)\n", "\n", "[`espn_mbb_team_schedule`](../mbb/reference/additional.md#espn_mbb_team_schedule) returns every game on one team's slate for a season β€” matchup name, week and season type β€” perfect for building an opponent list. We use UConn's 2024 championship run." ] }, { "cell_type": "code", "execution_count": null, "id": "0f9e7017", "metadata": {}, "outputs": [], "source": [ "tid_sched = int(row[\"team_id\"][0]) if row.height else 41 # UConn fallback\n", "tsched = safe(\n", " f\"team schedule {tid_sched}\",\n", " lambda: sdv.mbb.espn_mbb_team_schedule(team_id=tid_sched, season=2024),\n", ")\n", "if tsched is not None and getattr(tsched, \"height\", 0):\n", " keep = [\"id\", \"short_name\", \"season_type_name\", \"week_text\"]\n", " out = tsched.select([c for c in keep if c in tsched.columns]).head(12)\n", "else:\n", " out = \"team schedule unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "53a874c1", "metadata": {}, "source": [ "### Recipe 10 β€” Top rebounding teams 🧲 (FoxSports)\n", "\n", "[`fox_mbb_league_leaders`](../mbb/reference/additional.md#fox_mbb_league_leaders) isn't just a player board β€” flip `who=\"team\"` and pick `category=\"rebounds\"` to rank programs on the glass straight from FoxSports. No IDs needed." ] }, { "cell_type": "code", "execution_count": null, "id": "710a9e82", "metadata": {}, "outputs": [], "source": [ "team_reb = safe(\n", " \"fox team rebounds\",\n", " lambda: sdv.mbb.fox_mbb_league_leaders(category=\"rebounds\", who=\"team\"),\n", ")\n", "if team_reb is not None and getattr(team_reb, \"height\", 0):\n", " keep = [\"teams\", \"gp\", \"w\", \"l\", \"ppg\", \"ppg_diff\"]\n", " out = team_reb.select([c for c in keep if c in team_reb.columns]).head(10)\n", "else:\n", " out = \"Fox team leaders unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "3ff0572a", "metadata": {}, "source": [ "### Recipe 11 β€” Crunch-time buckets πŸ”₯ (parquet PBP)\n", "\n", "[`load_mbb_pbp`](../mbb/reference/loaders.md#load_mbb_pbp) is the whole season's play-by-play in one parquet β€” no live game needed. We slice it to scoring plays in the final minute of the second half: every late-game dagger across the year." ] }, { "cell_type": "code", "execution_count": null, "id": "828b623a", "metadata": {}, "outputs": [], "source": [ "season_pbp = sdv.mbb.load_mbb_pbp(seasons=[2024])\n", "print(\"season pbp rows:\", season_pbp.shape)\n", "(season_pbp\n", " .filter(\n", " (pl.col(\"scoring_play\") == True) # noqa: E712\n", " & (pl.col(\"period_number\") >= 2)\n", " & (pl.col(\"end_period_seconds_remaining\").cast(pl.Float64, strict=False) <= 60)\n", " )\n", " .select([\"game_id\", \"period_display_value\", \"clock_display_value\",\n", " \"text\", \"home_score\", \"away_score\"])\n", " .head(10))" ] }, { "cell_type": "markdown", "id": "7667119c", "metadata": {}, "source": [ "### Recipe 12 β€” Double-double leaders 🐼 (pandas interop)\n", "\n", "Prefer pandas? Pass `return_as_pandas=True` to any loader and stay in your comfort zone. Here we count games where a player hit double digits in at least two of points / rebounds / assists β€” the classic double-double β€” entirely in pandas." ] }, { "cell_type": "code", "execution_count": null, "id": "a4cc87ef", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "pbox_pd = sdv.mbb.load_mbb_player_boxscore(seasons=[2024], return_as_pandas=True)\n", "for col in [\"points\", \"rebounds\", \"assists\"]:\n", " pbox_pd[col] = pd.to_numeric(pbox_pd[col], errors=\"coerce\")\n", "pbox_pd[\"is_dd\"] = (pbox_pd[[\"points\", \"rebounds\", \"assists\"]] >= 10).sum(axis=1) >= 2\n", "(pbox_pd[pbox_pd[\"is_dd\"]]\n", " .groupby([\"athlete_display_name\", \"team_abbreviation\"])\n", " .size()\n", " .reset_index(name=\"double_doubles\")\n", " .sort_values(\"double_doubles\", ascending=False)\n", " .head(10)\n", " .reset_index(drop=True))" ] }, { "cell_type": "markdown", "id": "867e73e1", "metadata": {}, "source": [ "## 🧾 One call, the whole game: `espn_mbb_summary`\n", "\n", "[`espn_mbb_summary`](../mbb/reference/site.md#espn_mbb_summary) is the Swiss\n", "army knife β€” a single `event_id` returns a dict with team & player box scores,\n", "play-by-play, win probability, leaders, officials and more. Let's grab the team\n", "box score from that 2024 title game." ] }, { "cell_type": "code", "execution_count": null, "id": "05cf9b08", "metadata": {}, "outputs": [], "source": [ "summ = safe(\"summary 401638636\", lambda: sdv.mbb.espn_mbb_summary(event_id=401638636))\n", "if isinstance(summ, dict) and summ.get(\"boxscore_team\") is not None:\n", " tb = summ[\"boxscore_team\"]\n", " tb = tb if isinstance(tb, pl.DataFrame) else pl.DataFrame(tb)\n", " print(\"box score sections available:\", [k for k in summ.keys()][:8])\n", " out = tb.head()\n", "else:\n", " out = \"summary unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "4469b4a7", "metadata": {}, "source": [ "## πŸ™Œ Who suited up: game rosters\n", "\n", "[`espn_mbb_game_rosters`](../mbb/reference/additional.md#espn_mbb_game_rosters)\n", "returns one row per dressed player for a game, flagging starters β€” handy for\n", "joining onto play-by-play or box scores." ] }, { "cell_type": "code", "execution_count": null, "id": "2adecbb8", "metadata": {}, "outputs": [], "source": [ "gr = safe(\"game rosters 401638636\", lambda: sdv.mbb.espn_mbb_game_rosters(game_id=401638636))\n", "if gr is not None and getattr(gr, \"height\", 0):\n", " keep = [\"athlete_display_name\", \"team_abbreviation\", \"starter\"]\n", " out = gr.select([c for c in keep if c in gr.columns]).head(10)\n", "else:\n", " out = \"game rosters unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "f49a44f0", "metadata": {}, "source": [ "## πŸ”§ A multi-season pipeline: highest-scoring tournament games\n", "\n", "The schedule loader is stable, so here's a pure-polars analysis with no live\n", "dependency. We load the 2024 season schedule and rank games by combined\n", "points β€” March Madness shootouts float right to the top." ] }, { "cell_type": "code", "execution_count": null, "id": "b63c4811", "metadata": {}, "outputs": [], "source": [ "schedule_2024 = sdv.mbb.load_mbb_schedule(seasons=[2024])\n", "print(\"season schedule rows:\", schedule_2024.shape)\n", "(schedule_2024\n", " .with_columns(\n", " (pl.col(\"home_score\").cast(pl.Int64, strict=False)\n", " + pl.col(\"away_score\").cast(pl.Int64, strict=False)).alias(\"total\"))\n", " .filter(pl.col(\"total\").is_not_null())\n", " .sort(\"total\", descending=True)\n", " .select([\"game_date\", \"home_display_name\", \"away_display_name\",\n", " \"home_score\", \"away_score\", \"total\"])\n", " .head(10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## πŸ›οΈ stats.ncaa.org β€” the `ncaa_mbb_*` family (bigballR parity)\n", "\n", "a 16-function port of bigballR wired to **stats.ncaa.org** β€”\n", "schedules, rosters, box scores, play-by-play, lineups, possessions, on/off\n", "splits, and shot locations (`ncaa_mbb_team_schedule`, `ncaa_mbb_game_pbp`,\n", "`ncaa_mbb_lineups`, `ncaa_mbb_possessions`, `ncaa_mbb_on_off`, ...). The\n", "`(team, season) β†’ stats.ncaa.org id` crosswalk ships with the package, so it\n", "works offline:" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sportsdataverse.mbb import ncaa_mbb_team_ids\n", "\n", "ids = ncaa_mbb_team_ids()\n", "print(\"team-id crosswalk:\", ids.shape)\n", "ids.filter(pl.col(\"season\") == \"2025-26\").head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Live game fetches go through the shared proxy-bound NCAA fetch layer\n", "(stats.ncaa.org is IP-ban-happy β€” configure `SDV_PY_NCAA_*` /\n", "`SDV_PY_PROXYBONANZA_*` before fetching):\n", "\n", "```python\n", "from sportsdataverse.mbb import ncaa_mbb_game_pbp, ncaa_mbb_lineups\n", "\n", "pbp = ncaa_mbb_game_pbp(game_id) # one row per event\n", "lineups = ncaa_mbb_lineups(game_id) # five-player stints\n", "```" ] }, { "cell_type": "markdown", "id": "b9b052f7", "metadata": {}, "source": [ "## πŸŽ‰ Where to next\n", "\n", "- πŸŸ₯ **ESPN** wrappers (`espn_mbb_*`) cover the live site + core APIs β€”\n", " scoreboards, standings, rankings, summaries, play-by-play and more. See the\n", " [additional](../mbb/reference/additional.md) and\n", " [site](../mbb/reference/site.md) reference pages.\n", "- 🦊 **FoxSports** wrappers (`fox_mbb_*`) β€” leaders, standings, rosters,\n", " boxscores and odds in [additional](../mbb/reference/additional.md).\n", "- πŸ“¦ **Loaders** (`load_mbb_*`) read whole seasons of parquet β€” see\n", " [loaders](../mbb/reference/loaders.md). Pass `return_as_pandas=True` anywhere\n", " for pandas instead of polars.\n", "- πŸ€ R user? The same surface lives in\n", " [hoopR](https://hoopR.sportsdataverse.org) (NBA + NCAA men's basketball).\n", "- 🚺 Women's hoops? Check out the **WBB** module and its companion\n", " [wehoop](https://wehoop.sportsdataverse.org).\n", "\n", "Now go bracket something! πŸ€πŸ”₯" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## πŸ“Š Torvik and KenPom\n", "\n", "`torvik_*` (barttorvik.com, free) and `kenpom_*` (kenpom.com, **subscription** β€”\n", "the cell is a no-op without credentials).\n" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "import os\n", "\n", "from sportsdataverse.mbb import torvik_ratings\n", "\n", "safe(\"torvik ratings\", lambda: torvik_ratings(year=2025))\n", "\n", "if os.environ.get(\"KENPOM_EMAIL\") and os.environ.get(\"KENPOM_PASSWORD\"):\n", " from sportsdataverse.mbb import kenpom_ratings\n", "\n", " safe(\"kenpom ratings\", lambda: kenpom_ratings(season=2025))\n", "else:\n", " print(\"no KenPom credentials - skipping kenpom_* (set KENPOM_EMAIL / KENPOM_PASSWORD)\")\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }