{ "cells": [ { "cell_type": "markdown", "id": "c533e327", "metadata": {}, "source": [ "# ๐Ÿ€ Women's college basketball with `sportsdataverse-py`\n", "\n", "Welcome to the **women's college hoops** corner of the SportsDataverse! ๐ŸŽ‰ In a handful of lines you're about to pull rosters, schedules, play-by-play, live scoreboards, AP rankings, **ESPN's Basketball Power Index (BPI)**, in-game **win-probability** curves, and season-long parquet releases โ€” all returned as tidy [polars](https://pola.rs) DataFrames that are ready to model. ๐Ÿš€\n", "\n", "`sportsdataverse.wbb` leads with ESPN's deep **`espn_wbb_*`** women's-college-basketball surface โ€” over a hundred endpoints โ€” plus blazing-fast `load_wbb_*` data loaders. If you know the R package [wehoop](https://wehoop.sportsdataverse.org), these names will feel like home. Let's go scout some hoopers! ๐Ÿ€" ] }, { "cell_type": "markdown", "id": "fa237e54", "metadata": {}, "source": [ "## ๐Ÿงฐ The toolbox\n", "\n", "Every accessor returns a tidy **polars** `DataFrame` by default โ€” pass `return_as_pandas=True` for pandas. The โญ rows are the **premium ESPN analytics** surfaces we lead with. Click any name for the full reference:\n", "\n", "| Function | What it gives you | Source |\n", "|---|---|---|\n", "| [`espn_wbb_teams`](../wbb/reference/additional.md#espn_wbb_teams) | Every D-I program, one wide row each | ESPN |\n", "| [`espn_wbb_team_roster`](../wbb/reference/site.md#espn_wbb_team_roster) | A team's roster, one row per player | ESPN |\n", "| [`espn_wbb_schedule`](../wbb/reference/additional.md#espn_wbb_schedule) | Games for a date / date-range | ESPN |\n", "| [`espn_wbb_team_schedule`](../wbb/reference/site.md#espn_wbb_team_schedule) | One program's full season slate | ESPN |\n", "| [`espn_wbb_scoreboard`](../wbb/reference/site.md#espn_wbb_scoreboard) | โญ Live + final scoreboard, one row per game | ESPN |\n", "| [`espn_wbb_pbp`](../wbb/reference/additional.md#espn_wbb_pbp) | Full play-by-play + boxscore for a game | ESPN |\n", "| [`espn_wbb_player_gamelog`](../wbb/reference/web.md#espn_wbb_player_gamelog) | A player's game-by-game log | ESPN |\n", "| [`espn_wbb_player_splits`](../wbb/reference/web.md#espn_wbb_player_splits) | A player's situational stat splits | ESPN |\n", "| [`espn_wbb_team_stats`](../wbb/reference/additional.md#espn_wbb_team_stats) | A team's season stat splits | ESPN |\n", "| [`espn_wbb_standings`](../wbb/reference/site.md#espn_wbb_standings) | Conference standings + records | ESPN |\n", "| [`espn_wbb_conferences`](../wbb/reference/site.md#espn_wbb_conferences) | Conference groups + group ids | ESPN |\n", "| [`espn_wbb_rankings`](../wbb/reference/site.md#espn_wbb_rankings) | โญ AP / Coaches poll rankings | ESPN |\n", "| [`espn_wbb_leaders`](../wbb/reference/web.md#espn_wbb_leaders) | โญ League statistical leaders | ESPN |\n", "| [`espn_wbb_injuries`](../wbb/reference/site.md#espn_wbb_injuries) | โญ Active injury report | ESPN |\n", "| [`espn_wbb_season_powerindex`](../wbb/reference/core.md#espn_wbb_season_powerindex) | โญ **BPI** ratings, one row per team | ESPN |\n", "| [`espn_wbb_season_powerindex_leaders`](../wbb/reference/core.md#espn_wbb_season_powerindex_leaders) | โญ BPI / SOS / SOR category leaders | ESPN |\n", "| [`espn_wbb_game_predictor`](../wbb/reference/core.md#espn_wbb_game_predictor) | โญ BPI matchup projection for a game | ESPN |\n", "| [`espn_wbb_game_probabilities`](../wbb/reference/core.md#espn_wbb_game_probabilities) | โญ Play-by-play win-probability curve | ESPN |\n", "| [`espn_wbb_calendar`](../wbb/reference/site.md#espn_wbb_calendar) | Valid game dates for a season | ESPN |\n", "| [`load_wbb_schedule`](../wbb/reference/loaders.md#load_wbb_schedule) | Season-long schedule (parquet release) | release |\n", "| [`load_wbb_pbp`](../wbb/reference/loaders.md#load_wbb_pbp) | Season-long play-by-play (parquet, back to 2002) | release |\n", "| [`load_wbb_team_boxscore`](../wbb/reference/loaders.md#load_wbb_team_boxscore) | Season-long team boxscores (parquet) | release |\n", "| [`load_wbb_player_boxscore`](../wbb/reference/loaders.md#load_wbb_player_boxscore) | Season-long player boxscores (parquet) | release |" ] }, { "cell_type": "markdown", "id": "8257dac4", "metadata": {}, "source": [ "## ๐Ÿ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "**No API key needed** โ€” the ESPN endpoints and the parquet releases are all public. ๐Ÿ˜Š" ] }, { "cell_type": "code", "execution_count": null, "id": "fefc01de", "metadata": {}, "outputs": [], "source": [ "import polars as pl\n", "import sportsdataverse as sdv\n", "import sportsdataverse.wbb as wbb\n", "\n", "SEASON = 2025 # the 2024-25 season โ€” UConn's title run\n", "print('most recent wbb season:', wbb.most_recent_wbb_season())" ] }, { "cell_type": "markdown", "id": "71312b5f", "metadata": {}, "source": [ "ESPN's live endpoints are **seasonal** โ€” polls, injuries, and live scoreboards go quiet in the offseason, and any network call can hiccup. So we use a tiny `safe()` helper: you get the frame when the feed is up, and a friendly one-liner when it isn't (never a scary traceback). ๐Ÿ›Ÿ The `load_wbb_*` parquet loaders are rock-solid year-round, so we lean on those for anything historical." ] }, { "cell_type": "code", "execution_count": null, "id": "96a2306e", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " \"\"\"Run a live call defensively; return None (with a note) if it can't.\"\"\"\n", " try:\n", " out = thunk()\n", " ok = out is not None and (not hasattr(out, 'height') or out.height > 0)\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\n", "\n", "\n", "def has_rows(df):\n", " return df is not None and hasattr(df, 'height') and df.height > 0" ] }, { "cell_type": "markdown", "id": "6c695034", "metadata": {}, "source": [ "## ๐ŸŸ๏ธ Teams\n", "\n", "[`espn_wbb_teams`](../wbb/reference/additional.md#espn_wbb_teams) returns one wide row per Division-I program. The `team_id` here is the key you'll feed to roster, stats, and leader endpoints. (NCAA team frames carry no conference column โ€” that comes from `espn_wbb_standings()` / `espn_wbb_conferences()` below.)" ] }, { "cell_type": "code", "execution_count": null, "id": "a6aa5de4", "metadata": {}, "outputs": [], "source": [ "teams = safe('teams', wbb.espn_wbb_teams)\n", "(teams.select(['team_id', 'team_location', 'team_name', 'team_abbreviation', 'team_display_name']).head(10)\n", " if has_rows(teams) else 'teams unavailable')" ] }, { "cell_type": "markdown", "id": "62bf0327", "metadata": {}, "source": [ "## ๐Ÿ‘ฅ Team roster\n", "\n", "[`espn_wbb_team_roster`](../wbb/reference/site.md#espn_wbb_team_roster) takes a `team_id` and `season` and returns one row per player. Here's the 2024-25 **UConn Huskies** (`team_id=2509`) โ€” the eventual national champions, led by Paige Bueckers." ] }, { "cell_type": "code", "execution_count": null, "id": "4b8dc19f", "metadata": {}, "outputs": [], "source": [ "uconn = safe('UConn roster', lambda: wbb.espn_wbb_team_roster(team_id=2509, season=SEASON))\n", "(uconn.select(['athlete_id', 'full_name', 'jersey', 'position_abbreviation', 'display_height', 'display_weight']).head(12)\n", " if has_rows(uconn) else 'roster unavailable')" ] }, { "cell_type": "markdown", "id": "ce8e5b55", "metadata": {}, "source": [ "## ๐Ÿ“… Schedule & scoreboard\n", "\n", "Two complementary views of a slate:\n", "\n", "| Function | Best for |\n", "|---|---|\n", "| [`espn_wbb_schedule`](../wbb/reference/additional.md#espn_wbb_schedule) | a clean game list for a date or `'YYYYMMDD-YYYYMMDD'` range |\n", "| [`espn_wbb_scoreboard`](../wbb/reference/site.md#espn_wbb_scoreboard) | โญ a richer live/final scoreboard (status, venue, scores) |\n", "\n", "April 4, 2025 was the women's **Final Four**. Note: `home_score` / `away_score` from `espn_wbb_schedule` arrive as **strings**, so cast before arithmetic." ] }, { "cell_type": "code", "execution_count": null, "id": "fb220a6b", "metadata": {}, "outputs": [], "source": [ "final_four = safe('Final Four schedule', lambda: wbb.espn_wbb_schedule(dates=20250404))\n", "(final_four.select(['id', 'date', 'away_display_name', 'away_score', 'home_display_name', 'home_score', 'status_type_completed'])\n", " if has_rows(final_four) else 'schedule unavailable')" ] }, { "cell_type": "code", "execution_count": null, "id": "1530d0d5", "metadata": {}, "outputs": [], "source": [ "# โญ The scoreboard view of the same date โ€” richer game-state columns\n", "board = safe('Final Four scoreboard', lambda: wbb.espn_wbb_scoreboard(dates=20250404))\n", "keep = ['game_id', 'short_name', 'status_type_completed', 'home_team_short_display_name',\n", " 'home_team_score', 'away_team_short_display_name', 'away_team_score']\n", "(board.select([c for c in keep if c in board.columns])\n", " if has_rows(board) else 'scoreboard unavailable')" ] }, { "cell_type": "markdown", "id": "c3022095", "metadata": {}, "source": [ "## ๐ŸŽฌ Play-by-play\n", "\n", "[`espn_wbb_pbp`](../wbb/reference/additional.md#espn_wbb_pbp) returns a **dict** of game components (`plays`, `boxscore`, `header`, `winprobability`, โ€ฆ). The `plays` value is a list of dicts โ€” build a frame with `pl.DataFrame(pbp['plays'], infer_schema_length=None)`. Columns use ESPN dot-notation (`period.number`, `clock.displayValue`, `type.text`, `scoringPlay`).\n", "\n", "Game `401746075` is the **2025 national championship**: South Carolina vs. UConn." ] }, { "cell_type": "code", "execution_count": null, "id": "6ab4c227", "metadata": {}, "outputs": [], "source": [ "pbp = safe('championship pbp', lambda: wbb.espn_wbb_pbp(game_id=401746075))\n", "plays = None\n", "if pbp is not None and isinstance(pbp, dict) and pbp.get('plays'):\n", " plays = pl.DataFrame(pbp['plays'], infer_schema_length=None)\n", " print('plays shape:', plays.shape, '| components:', list(pbp.keys())[:8])\n", "(plays.select(['period.number', 'clock.displayValue', 'type.text', 'scoringPlay', 'text']).head()\n", " if plays is not None else 'pbp unavailable')" ] }, { "cell_type": "code", "execution_count": null, "id": "c473d721", "metadata": {}, "outputs": [], "source": [ "# Scoring plays only, with the running score\n", "(plays.filter(pl.col('scoringPlay') == True)\n", " .select(['period.number', 'clock.displayValue', 'awayScore', 'homeScore', 'text']).head(8)\n", " if plays is not None else 'pbp unavailable')" ] }, { "cell_type": "markdown", "id": "02e0fa15", "metadata": {}, "source": [ "## โญ Premium ESPN analytics\n", "\n", "This is where `espn_wbb_*` shines. Three live league-wide feeds, each one line:\n", "\n", "| Function | Gives you |\n", "|---|---|\n", "| [`espn_wbb_rankings`](../wbb/reference/site.md#espn_wbb_rankings) | the current AP / Coaches poll |\n", "| [`espn_wbb_leaders`](../wbb/reference/web.md#espn_wbb_leaders) | league statistical leaders (PPG, RPG, APG, โ€ฆ) |\n", "| [`espn_wbb_injuries`](../wbb/reference/site.md#espn_wbb_injuries) | the active injury report |\n", "\n", "These are **in-season** feeds, so out of season they return empty โ€” our `safe()` helper handles that gracefully." ] }, { "cell_type": "code", "execution_count": null, "id": "86158509", "metadata": {}, "outputs": [], "source": [ "rankings = safe('rankings (AP/Coaches poll)', wbb.espn_wbb_rankings)\n", "(rankings.head(12) if has_rows(rankings)\n", " else 'no poll published right now (offseason) โ€” try during the season')" ] }, { "cell_type": "code", "execution_count": null, "id": "812b48f2", "metadata": {}, "outputs": [], "source": [ "injuries = safe('injury report', wbb.espn_wbb_injuries)\n", "(injuries.head(10) if has_rows(injuries)\n", " else 'no active injuries posted right now (offseason)')" ] }, { "cell_type": "markdown", "id": "ae9f3339", "metadata": {}, "source": [ "## ๐Ÿ“Š Basketball Power Index (BPI)\n", "\n", "ESPN's **BPI** is a forward-looking team-strength rating โ€” expected point margin per 70 possessions against an average opponent on a neutral floor. [`espn_wbb_season_powerindex`](../wbb/reference/core.md#espn_wbb_season_powerindex) returns one row per ranked team, with a nested `stats` list (BPI, BPI rank, SOS, SOR, โ€ฆ). Let's unnest it into a clean BPI leaderboard for 2024-25." ] }, { "cell_type": "code", "execution_count": null, "id": "792463dd", "metadata": {}, "outputs": [], "source": [ "import ast\n", "\n", "spi = safe('season BPI', lambda: wbb.espn_wbb_season_powerindex(season=SEASON))\n", "\n", "\n", "def pick(stats, name):\n", " # The nested `stats` value arrives as a Python-repr string โ€” parse it safely\n", " if isinstance(stats, str):\n", " try:\n", " stats = ast.literal_eval(stats)\n", " except (ValueError, SyntaxError):\n", " return None\n", " for s in (stats or []):\n", " if isinstance(s, dict) and s.get('name') == name:\n", " return s.get('value')\n", " return None\n", "\n", "\n", "if has_rows(spi):\n", " rows = [\n", " {\n", " 'bpi_rank': pick(r['stats'], 'bpirank'),\n", " 'bpi': pick(r['stats'], 'bpi'),\n", " 'conference_id': r.get('conference_id'),\n", " 'team_ref': r.get('team_$ref'),\n", " }\n", " for r in spi.to_dicts()\n", " ]\n", " out = pl.DataFrame(rows).sort('bpi', descending=True, nulls_last=True).head(12)\n", "else:\n", " out = 'BPI unavailable right now'\n", "out" ] }, { "cell_type": "markdown", "id": "bfa5a1a8", "metadata": {}, "source": [ "And [`espn_wbb_season_powerindex_leaders`](../wbb/reference/core.md#espn_wbb_season_powerindex_leaders) lists the category leaders โ€” who tops BPI, strength-of-schedule, strength-of-record, and more." ] }, { "cell_type": "code", "execution_count": null, "id": "a1250494", "metadata": {}, "outputs": [], "source": [ "spi_leaders = safe('BPI category leaders', lambda: wbb.espn_wbb_season_powerindex_leaders(season=SEASON))\n", "(spi_leaders.select(['name', 'display_name']).head(10)\n", " if has_rows(spi_leaders) else 'BPI leaders unavailable')" ] }, { "cell_type": "markdown", "id": "3fb983d7", "metadata": {}, "source": [ "## ๐Ÿ† Standings & conferences\n", "\n", "[`espn_wbb_standings`](../wbb/reference/site.md#espn_wbb_standings) returns one wide row per team โ€” records, win %, points for/against, **and** conference membership. [`espn_wbb_conferences`](../wbb/reference/site.md#espn_wbb_conferences) lists the conference groups with their `group_id`s (handy for filtering)." ] }, { "cell_type": "code", "execution_count": null, "id": "d0b6c7d7", "metadata": {}, "outputs": [], "source": [ "standings = safe('2025 standings', lambda: wbb.espn_wbb_standings(season=SEASON))\n", "(standings.select(['team_display_name', 'conference_abbreviation', 'wins', 'losses', 'win_percent', 'points_for', 'points_against'])\n", " .sort('win_percent', descending=True, nulls_last=True).head(10)\n", " if has_rows(standings) else 'standings unavailable')" ] }, { "cell_type": "code", "execution_count": null, "id": "6e83e781", "metadata": {}, "outputs": [], "source": [ "conferences = safe('conferences', wbb.espn_wbb_conferences)\n", "(conferences.select(['group_id', 'name', 'abbreviation', 'short_name']).head(12)\n", " if has_rows(conferences) else 'conferences unavailable')" ] }, { "cell_type": "markdown", "id": "b481f81d", "metadata": {}, "source": [ "## ๐Ÿณ Cookbook: common WBB tasks\n", "\n", "Now the fun part โ€” real tasks you'll reach for constantly, each built on the premium functions above. The `load_wbb_*` loaders below read pre-built parquet releases from [wehoop-wbb-data](https://github.com/sportsdataverse/wehoop-wbb-data), so they're fast and reliable year-round. We base most season-wide recipes on **2024** because that release is fully published; swap the season once newer parquet drops." ] }, { "cell_type": "markdown", "id": "cd33fdb3", "metadata": {}, "source": [ "First, pull the three season-long parquet releases we'll lean on across the\n", "Cookbook โ€” player boxscores, team boxscores, and play-by-play for 2024. One\n", "load, many recipes." ] }, { "cell_type": "code", "execution_count": null, "id": "48902a51", "metadata": {}, "outputs": [], "source": [ "player_box = wbb.load_wbb_player_boxscore(seasons=[2024])\n", "team_box = wbb.load_wbb_team_boxscore(seasons=[2024])\n", "season_pbp = wbb.load_wbb_pbp(seasons=[2024])\n", "print('player_box:', player_box.shape, '| team_box:', team_box.shape, '| pbp:', season_pbp.shape)" ] }, { "cell_type": "markdown", "id": "4bfef2fc", "metadata": {}, "source": [ "### Recipe 1 โ€” Win-probability ride of a championship ๐Ÿ“ˆ\n", "\n", "[`espn_wbb_game_probabilities`](../wbb/reference/core.md#espn_wbb_game_probabilities) returns ESPN's play-by-play win-probability snapshots for a game. Let's watch how UConn's win odds evolved through the 2025 title game (event `401746075`)." ] }, { "cell_type": "code", "execution_count": null, "id": "0b0224d8", "metadata": {}, "outputs": [], "source": [ "wp = safe('win probability', lambda: wbb.espn_wbb_game_probabilities(event_id=401746075))\n", "if has_rows(wp):\n", " ride = wp.select(['sequence_number', 'home_win_percentage', 'away_win_percentage', 'tie_percentage'])\n", " print('snapshots:', ride.height,\n", " '| opening home win%:', round(float(ride['home_win_percentage'][0]) * 100, 1),\n", " '| final home win%:', round(float(ride['home_win_percentage'][-1]) * 100, 1))\n", " out = ride.head(6)\n", "else:\n", " out = 'win probability unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "5a9b65ce", "metadata": {}, "source": [ "### Recipe 2 โ€” BPI matchup preview for a game ๐Ÿ”ฎ\n", "\n", "[`espn_wbb_game_predictor`](../wbb/reference/core.md#espn_wbb_game_predictor) gives ESPN's BPI-based projection for a single game โ€” matchup quality, projected game score, and each side's predicted point total. Here's the championship preview." ] }, { "cell_type": "code", "execution_count": null, "id": "f6bde07e", "metadata": {}, "outputs": [], "source": [ "pred = safe('game predictor (BPI)', lambda: wbb.espn_wbb_game_predictor(event_id=401746075))\n", "if has_rows(pred):\n", " home_stats = pred['home_team_statistics'][0]\n", " if isinstance(home_stats, str): # arrives as a Python-repr string\n", " home_stats = ast.literal_eval(home_stats)\n", " preview = pl.DataFrame([\n", " {'stat': s.get('displayName'), 'value': s.get('displayValue')}\n", " for s in home_stats if isinstance(s, dict)\n", " ])\n", " out = preview.head(10)\n", "else:\n", " out = 'predictor unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "21333ab9", "metadata": {}, "source": [ "### Recipe 3 โ€” Top scorers of a full season ๐Ÿฅ‡\n", "\n", "Take the season-long player boxscore and aggregate with polars to find the highest per-game scorers (min. 20 games)." ] }, { "cell_type": "code", "execution_count": null, "id": "72849976", "metadata": {}, "outputs": [], "source": [ "top_scorers = (\n", " player_box\n", " .group_by(['athlete_id', 'athlete_display_name', 'team_short_display_name'])\n", " .agg(\n", " games=pl.len(),\n", " total_points=pl.col('points').sum(),\n", " ppg=pl.col('points').mean().round(1),\n", " )\n", " .filter(pl.col('games') >= 20)\n", " .sort('ppg', descending=True)\n", " .head(10)\n", ")\n", "top_scorers" ] }, { "cell_type": "markdown", "id": "f4de820b", "metadata": {}, "source": [ "### Recipe 4 โ€” Best scoring offenses, joined to records ๐Ÿค\n", "\n", "Aggregate the team boxscore to rank programs by points per game, then attach each team's W-L from the live standings." ] }, { "cell_type": "code", "execution_count": null, "id": "2343b6fc", "metadata": {}, "outputs": [], "source": [ "offense = (\n", " team_box\n", " .group_by(['team_id', 'team_display_name'])\n", " .agg(games=pl.len(), ppg=pl.col('team_score').mean().round(1))\n", " .filter(pl.col('games') >= 20)\n", " .sort('ppg', descending=True)\n", " .head(10)\n", ")\n", "if has_rows(standings):\n", " recs = standings.select(['team_id', 'wins', 'losses']).with_columns(pl.col('team_id').cast(pl.Int64, strict=False))\n", " offense = offense.with_columns(pl.col('team_id').cast(pl.Int64, strict=False)).join(recs, on='team_id', how='left')\n", "offense" ] }, { "cell_type": "markdown", "id": "f593d7b9", "metadata": {}, "source": [ "### Recipe 5 โ€” A program's full season slate ๐Ÿ—“๏ธ\n", "\n", "[`espn_wbb_team_schedule`](../wbb/reference/site.md#espn_wbb_team_schedule) returns one program's complete season โ€” every game with its date, matchup short name, and season type. Here's UConn's 2024-25 road to the title (`team_id=2509`)." ] }, { "cell_type": "code", "execution_count": null, "id": "f2c055eb", "metadata": {}, "outputs": [], "source": [ "tsched = safe('UConn team schedule', lambda: wbb.espn_wbb_team_schedule(team_id=2509, season=SEASON))\n", "if has_rows(tsched):\n", " keep = ['id', 'date', 'short_name', 'season_type_name', 'week_text']\n", " out = tsched.select([c for c in keep if c in tsched.columns]).head(12)\n", " print('games on the slate:', tsched.height)\n", "else:\n", " out = 'team schedule unavailable (offseason) โ€” try during the season'\n", "out" ] }, { "cell_type": "markdown", "id": "6571c4b6", "metadata": {}, "source": [ "### Recipe 6 โ€” Deadliest three-point shooting teams ๐ŸŽฏ\n", "\n", "Roll the team boxscore up to season totals and compute each program's three-point percentage. Made รท attempted, sorted, min. 20 games." ] }, { "cell_type": "code", "execution_count": null, "id": "9b419c99", "metadata": {}, "outputs": [], "source": [ "three_pt = (\n", " team_box\n", " .group_by(['team_id', 'team_display_name'])\n", " .agg(\n", " games=pl.len(),\n", " tpm=pl.col('three_point_field_goals_made').sum(),\n", " tpa=pl.col('three_point_field_goals_attempted').sum(),\n", " )\n", " .filter((pl.col('games') >= 20) & (pl.col('tpa') > 0))\n", " .with_columns((pl.col('tpm') / pl.col('tpa') * 100).round(1).alias('three_pct'))\n", " .sort('three_pct', descending=True)\n", " .head(10)\n", ")\n", "three_pt" ] }, { "cell_type": "markdown", "id": "5165ed38", "metadata": {}, "source": [ "### Recipe 7 โ€” Clutch shot-makers โฑ๏ธ\n", "\n", "Slice the season-long play-by-play to scoring plays in the **final two minutes of the 4th quarter (or overtime)**, total each player's clutch points, and name them via the player boxscore. Pure ice in the veins." ] }, { "cell_type": "code", "execution_count": null, "id": "36fc97e1", "metadata": {}, "outputs": [], "source": [ "name_lookup = player_box.select(\n", " ['athlete_id', 'athlete_display_name', 'team_short_display_name']\n", ").unique(subset=['athlete_id'])\n", "\n", "clutch = (\n", " season_pbp\n", " .filter(\n", " (pl.col('period_number') >= 4)\n", " & (pl.col('scoring_play') == True)\n", " & (pl.col('start_game_seconds_remaining') <= 120)\n", " & pl.col('athlete_id_1').is_not_null()\n", " )\n", " .group_by('athlete_id_1')\n", " .agg(clutch_points=pl.col('score_value').sum(), clutch_plays=pl.len())\n", " .rename({'athlete_id_1': 'athlete_id'})\n", " .join(name_lookup, on='athlete_id', how='left')\n", " .sort('clutch_points', descending=True)\n", " .select(['athlete_display_name', 'team_short_display_name', 'clutch_plays', 'clutch_points'])\n", " .head(10)\n", ")\n", "clutch" ] }, { "cell_type": "markdown", "id": "84093a64", "metadata": {}, "source": [ "### Recipe 8 โ€” Where the buckets come from (shot-zone mix) ๐Ÿ—บ๏ธ\n", "\n", "The play-by-play carries `coordinate_x` / `coordinate_y` for shots and a `score_value` (2 or 3). Bucket every made field goal into a zone and see how a season's points break down by shot location." ] }, { "cell_type": "code", "execution_count": null, "id": "aba4e6be", "metadata": {}, "outputs": [], "source": [ "shot_zones = (\n", " season_pbp\n", " .filter(\n", " (pl.col('scoring_play') == True)\n", " & (pl.col('score_value') >= 2)\n", " & pl.col('coordinate_y').is_not_null()\n", " )\n", " .with_columns(\n", " pl.when(pl.col('score_value') == 3).then(pl.lit('3-pointer'))\n", " .when(pl.col('coordinate_y') <= 8).then(pl.lit('2pt โ€” at the rim'))\n", " .otherwise(pl.lit('2pt โ€” jumper')).alias('shot_zone')\n", " )\n", " .group_by('shot_zone')\n", " .agg(made_field_goals=pl.len(), points=pl.col('score_value').sum())\n", " .with_columns(\n", " (pl.col('made_field_goals') / pl.col('made_field_goals').sum() * 100).round(1).alias('share_pct')\n", " )\n", " .sort('made_field_goals', descending=True)\n", ")\n", "shot_zones" ] }, { "cell_type": "markdown", "id": "86b0229a", "metadata": {}, "source": [ "### Recipe 9 โ€” Double-double machines ๐Ÿ”„\n", "\n", "Flag every player-game with at least two double-digit categories (points / rebounds / assists), then count who racked up the most double-doubles across the season." ] }, { "cell_type": "code", "execution_count": null, "id": "9d3b6afb", "metadata": {}, "outputs": [], "source": [ "dd = (\n", " player_box\n", " .with_columns(\n", " (\n", " (pl.col('points') >= 10).cast(pl.Int8)\n", " + (pl.col('rebounds') >= 10).cast(pl.Int8)\n", " + (pl.col('assists') >= 10).cast(pl.Int8)\n", " ).alias('double_digit_cats')\n", " )\n", " .filter(pl.col('double_digit_cats') >= 2)\n", " .group_by(['athlete_id', 'athlete_display_name', 'team_short_display_name'])\n", " .agg(double_doubles=pl.len())\n", " .sort('double_doubles', descending=True)\n", " .head(10)\n", ")\n", "dd" ] }, { "cell_type": "markdown", "id": "b8f7a8e2", "metadata": {}, "source": [ "### Recipe 10 โ€” Find the best defenses (fewest points allowed) ๐Ÿ›ก๏ธ\n", "\n", "Every team boxscore row carries the **opponent's** score, so a single group-by yields points allowed per game. Lowest-scoring opponents = stingiest defenses." ] }, { "cell_type": "code", "execution_count": null, "id": "7d4a08f5", "metadata": {}, "outputs": [], "source": [ "defense = (\n", " team_box\n", " .group_by(['team_id', 'team_display_name'])\n", " .agg(\n", " games=pl.len(),\n", " opp_ppg=pl.col('opponent_team_score').mean().round(1),\n", " own_ppg=pl.col('team_score').mean().round(1),\n", " )\n", " .filter(pl.col('games') >= 20)\n", " .with_columns((pl.col('own_ppg') - pl.col('opp_ppg')).round(1).alias('net_ppg'))\n", " .sort('opp_ppg')\n", " .head(10)\n", ")\n", "defense" ] }, { "cell_type": "markdown", "id": "c73e2a1c", "metadata": {}, "source": [ "### Recipe 11 โ€” Rolling form: a team's last 10 games ๐Ÿ“Š\n", "\n", "Filter the team boxscore to one program, sort by date, and take the tail โ€” a quick \"how did they finish the year?\" view with the scoring margin per game. Here's UConn (`team_id=2509`)." ] }, { "cell_type": "code", "execution_count": null, "id": "9bb2cac7", "metadata": {}, "outputs": [], "source": [ "last10 = (\n", " team_box\n", " .filter(pl.col('team_id') == 2509)\n", " .with_columns((pl.col('team_score') - pl.col('opponent_team_score')).alias('margin'))\n", " .sort('game_date')\n", " .tail(10)\n", " .select(['game_date', 'opponent_team_short_display_name', 'team_score', 'opponent_team_score', 'margin'])\n", ")\n", "print('average margin over last 10:', round(last10['margin'].mean(), 1) if last10.height else 'n/a')\n", "last10" ] }, { "cell_type": "markdown", "id": "6f8fa6c8", "metadata": {}, "source": [ "### Recipe 12 โ€” Pandas interop: a season's play-type mix ๐Ÿผ\n", "\n", "Every loader and accessor takes `return_as_pandas=True`. Pull the play-by-play\n", "as pandas, tally the most common play types with a one-liner `value_counts()`,\n", "and you're back in familiar territory for downstream tooling." ] }, { "cell_type": "code", "execution_count": null, "id": "3365d695", "metadata": {}, "outputs": [], "source": [ "pbp_pd = wbb.load_wbb_pbp(seasons=[2024], return_as_pandas=True)\n", "play_mix = (\n", " pbp_pd['type_text']\n", " .value_counts()\n", " .head(10)\n", " .rename_axis('play_type')\n", " .reset_index(name='count')\n", ")\n", "play_mix" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ๐Ÿ›๏ธ stats.ncaa.org โ€” the `ncaa_wbb_*` family (bigballR parity)\n", "\n", "a 16-function port of bigballR/wbigballR wired to\n", "**stats.ncaa.org** โ€” schedules, rosters, box scores, play-by-play, lineups,\n", "possessions, on/off splits, and shot locations (`ncaa_wbb_team_schedule`,\n", "`ncaa_wbb_game_pbp`, `ncaa_wbb_lineups`, `ncaa_wbb_possessions`,\n", "`ncaa_wbb_on_off`, ...). The `(team, season) โ†’ stats.ncaa.org id` crosswalk\n", "ships with the package, so it works offline:" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sportsdataverse.wbb import ncaa_wbb_team_ids\n", "\n", "ids = ncaa_wbb_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.wbb import ncaa_wbb_game_pbp, ncaa_wbb_lineups\n", "\n", "pbp = ncaa_wbb_game_pbp(game_id) # one row per event\n", "lineups = ncaa_wbb_lineups(game_id) # five-player stints\n", "```" ] }, { "cell_type": "markdown", "id": "cc6a80bc", "metadata": {}, "source": [ "## ๐ŸŽ‰ Where to go next\n", "\n", "- Pass `return_as_pandas=True` to any wrapper for a pandas frame.\n", "- **Premium analytics**: [`espn_wbb_season_powerindex`](../wbb/reference/core.md#espn_wbb_season_powerindex), [`espn_wbb_game_probabilities`](../wbb/reference/core.md#espn_wbb_game_probabilities), and [`espn_wbb_rankings`](../wbb/reference/site.md#espn_wbb_rankings) are the deep cuts.\n", "- **Full reference**: the WBB pages โ€” [core](../wbb/reference/core.md), [site](../wbb/reference/site.md), [web](../wbb/reference/web.md), [additional](../wbb/reference/additional.md), and [loaders](../wbb/reference/loaders.md).\n", "- `dir(sdv.wbb)` shows the full 100+ endpoint surface (player gamelogs, splits, depth charts, transactions, recruits, and more).\n", "- Men's side? See the parallel [`06_mbb_intro.ipynb`](06_mbb_intro.ipynb).\n", "- R user? The same surface lives in [wehoop](https://wehoop.sportsdataverse.org).\n", "\n", "Now go find the next national champion! ๐Ÿ€๐Ÿ†" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ๐Ÿ“Š Bart Torvik (women's T-Rank)\n", "\n", "`bart_wbb_*` wraps barttorvik.com/ncaaw โ€” team ratings and game results with no key.\n" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sportsdataverse.wbb import bart_wbb_ratings\n", "\n", "safe(\"bart_wbb ratings\", lambda: bart_wbb_ratings(year=2025))\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }