{ "cells": [ { "cell_type": "markdown", "id": "1cbb0cc0", "metadata": {}, "source": [ "# πŸ€ Women's basketball with `sportsdataverse-py`\n", "\n", "Welcome! In just a few lines of Python you're about to pull **WNBA** teams, rosters, schedules, play-by-play, season stats, standings and the draft β€” all as tidy [polars](https://pola.rs) DataFrames that are ready to model. πŸš€\n", "\n", "`sportsdataverse.wnba` leads with ESPN's rich public API (the `espn_wnba_*` family) and tops it off with `load_wnba_*` **parquet loaders** that hand you whole seasons in one shot. **No API key needed.** πŸŽ‰\n", "\n", "If you've used the R package [wehoop](https://wehoop.sportsdataverse.org), these names will feel right at home. Let's go hoop! πŸ€" ] }, { "cell_type": "markdown", "id": "fdc481cd", "metadata": {}, "source": [ "## 🧰 The toolbox\n", "\n", "Every accessor returns a tidy **polars** `DataFrame` by default β€” pass `return_as_pandas=True` for pandas, or `raw=True` (where supported) for the untouched ESPN JSON. Here's the whole kit (click any name for the full reference):\n", "\n", "| Function | What it gives you | Source |\n", "|---|---|---|\n", "| [`espn_wnba_teams`](../wnba/reference/additional.md#espn_wnba_teams) | One row per franchise (grab `team_id`s) | ⭐ ESPN |\n", "| [`espn_wnba_team_roster`](../wnba/reference/site.md#espn_wnba_team_roster) | A team's active roster for a season | ⭐ ESPN |\n", "| [`espn_wnba_schedule`](../wnba/reference/additional.md#espn_wnba_schedule) | Games + results for a date or date range | ⭐ ESPN |\n", "| [`espn_wnba_pbp`](../wnba/reference/additional.md#play-by-play-schedule--rosters) | Event-level play-by-play for one game | ⭐ ESPN |\n", "| [`espn_wnba_player_stats`](../wnba/reference/additional.md#espn_wnba_player_stats) | A player's season stat line (wide) | ⭐ ESPN |\n", "| [`espn_wnba_team_stats`](../wnba/reference/additional.md#espn_wnba_team_stats) | A team's season stats (Averages/Totals/Misc) | ⭐ ESPN |\n", "| [`espn_wnba_standings`](../wnba/reference/site.md#espn_wnba_standings) | League standings, one row per team | ⭐ ESPN |\n", "| [`espn_wnba_draft`](../wnba/reference/site.md#espn_wnba_draft) | Every draft pick for a season | ⭐ ESPN |\n", "| [`espn_wnba_game_officials`](../wnba/reference/additional.md#espn_wnba_game_officials) | The refs who worked a game | ⭐ ESPN |\n", "| [`load_wnba_schedule`](../wnba/reference/loaders.md#load_wnba_schedule) | Whole-season schedule (parquet release) | πŸ“¦ loader |\n", "| [`load_wnba_player_boxscore`](../wnba/reference/loaders.md#load_wnba_player_boxscore) | Whole-season player box scores | πŸ“¦ loader |\n", "| [`load_wnba_team_boxscore`](../wnba/reference/loaders.md#load_wnba_team_boxscore) | Whole-season team box scores | πŸ“¦ loader |\n", "| [`load_wnba_player_season_stats`](../wnba/reference/loaders.md#load_wnba_player_season_stats) | Season-aggregated player stats | πŸ“¦ loader |\n", "| [`load_wnba_pbp`](../wnba/reference/loaders.md#load_wnba_pbp) | Whole-season play-by-play | πŸ“¦ loader |\n", "| [`load_wnba_shots`](../wnba/reference/loaders.md#load_wnba_shots) | Shot-location data | πŸ“¦ loader |\n", "| [`load_wnba_standings`](../wnba/reference/loaders.md#load_wnba_standings) | Whole-season standings (long) | πŸ“¦ loader |\n", "| [`load_wnba_rosters`](../wnba/reference/loaders.md#load_wnba_rosters) | Whole-season rosters | πŸ“¦ loader |\n", "| [`load_wnba_draft`](../wnba/reference/loaders.md#load_wnba_draft) | Whole-season draft picks | πŸ“¦ loader |\n", "| [`most_recent_wnba_season`](../wnba/reference/additional.md#most_recent_wnba_season) | The latest season year | πŸ› οΈ helper |\n", "\n", "⭐ = the **premium ESPN live API** Β· πŸ“¦ = bulk parquet loaders Β· πŸ› οΈ = helpers." ] }, { "cell_type": "markdown", "id": "e05fd59b", "metadata": {}, "source": [ "## πŸ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "That's it β€” the ESPN endpoints are public, so there's nothing to configure. 😊" ] }, { "cell_type": "code", "execution_count": null, "id": "4d94e610", "metadata": {}, "outputs": [], "source": [ "import polars as pl\n", "import sportsdataverse as sdv\n", "import sportsdataverse.wnba as wnba\n", "\n", "SEASON = 2024 # a complete season, so every cell has data to show\n", "print('most recent WNBA season:', wnba.most_recent_wnba_season())" ] }, { "cell_type": "markdown", "id": "a9728119", "metadata": {}, "source": [ "ESPN's live endpoints are seasonal and occasionally rate-limited, so a tiny `safe()` helper runs each risky call defensively β€” you get the frame when the feed is up, and a friendly one-liner when it isn't (never a scary traceback). The `load_wnba_*` loaders read static parquet releases and are rock-solid, so we let those run bare. πŸ›Ÿ" ] }, { "cell_type": "code", "execution_count": null, "id": "147e1a16", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " \"\"\"Run a live call; print a one-liner instead of raising on failure.\"\"\"\n", " try:\n", " out = thunk()\n", " print(f'βœ… {label}')\n", " return out\n", " except (NoDataError, AssetFetchError) as e:\n", " print(f\"\\u23ed\\ufe0f {label}: {type(e).__name__}: {e}\")\n", " return None" ] }, { "cell_type": "markdown", "id": "ebbdd346", "metadata": {}, "source": [ "## 🏟️ Teams\n", "\n", "[`espn_wnba_teams`](../wnba/reference/additional.md#espn_wnba_teams) returns one row per franchise. The `team_id`, location, name and abbreviation are the keys you'll reuse to fetch rosters, schedules and stats." ] }, { "cell_type": "code", "execution_count": null, "id": "5418f71c", "metadata": {}, "outputs": [], "source": [ "teams = safe('WNBA teams', wnba.espn_wnba_teams)\n", "print('shape:', None if teams is None else teams.shape)\n", "(teams.select(['team_id', 'team_location', 'team_name',\n", " 'team_abbreviation', 'team_display_name']).head(15)\n", " if teams is not None else 'teams unavailable')" ] }, { "cell_type": "markdown", "id": "d61939ba", "metadata": {}, "source": [ "## πŸ‘₯ Team roster β€” Las Vegas Aces\n", "\n", "[`espn_wnba_team_roster`](../wnba/reference/site.md#espn_wnba_team_roster) lists active players for one team in a season. The back-to-back champion Aces are `team_id=17`. Player columns are unprefixed (`athlete_id`, `full_name`, `jersey`, `position_abbreviation`)." ] }, { "cell_type": "code", "execution_count": null, "id": "f93d755a", "metadata": {}, "outputs": [], "source": [ "aces = safe('Aces roster', lambda: wnba.espn_wnba_team_roster(team_id=17, season=SEASON))\n", "(aces.select(['athlete_id', 'full_name', 'jersey',\n", " 'position_abbreviation', 'display_height', 'age']).head(12)\n", " if aces is not None else 'roster unavailable')" ] }, { "cell_type": "markdown", "id": "46b8f23b", "metadata": {}, "source": [ "## πŸ“… Schedule\n", "\n", "[`espn_wnba_schedule`](../wnba/reference/additional.md#espn_wnba_schedule) takes `dates=YYYYMMDD` for a single day, or a `'YYYYMMDD-YYYYMMDD'` string for a range. Team-name columns are `home_display_name` / `away_display_name`, and `home_score` / `away_score` come back as **strings** β€” cast before doing arithmetic.\n", "\n", "The range below (Oct 16–20, 2024) is the back half of the 2024 WNBA Finals. Let's cast the scores and derive a winning margin to show a small polars transform." ] }, { "cell_type": "code", "execution_count": null, "id": "78c0f5c8", "metadata": {}, "outputs": [], "source": [ "finals = safe('2024 Finals schedule',\n", " lambda: wnba.espn_wnba_schedule(dates='20241016-20241020'))\n", "if finals is not None and finals.height:\n", " out = (finals\n", " .select(['id', 'home_display_name', 'away_display_name',\n", " 'home_score', 'away_score', 'status_type_description'])\n", " .with_columns([\n", " pl.col('home_score').cast(pl.Int64, strict=False).alias('home_pts'),\n", " pl.col('away_score').cast(pl.Int64, strict=False).alias('away_pts'),\n", " ])\n", " .with_columns((pl.col('home_pts') - pl.col('away_pts')).abs().alias('margin')))\n", "else:\n", " out = 'schedule unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "04c4db13", "metadata": {}, "source": [ "## 🎬 Play-by-play β€” 2024 Finals Game 5\n", "\n", "[`espn_wnba_pbp`](../wnba/reference/additional.md#play-by-play-schedule--rosters) returns a **dict** of component pieces (`plays`, `boxscore`, `header`, `winprobability`, …). The `plays` entry is a list of raw ESPN dicts; build a frame with `pl.DataFrame(..., infer_schema_length=None)`. Its columns use raw **dot-notation** (`period.number`, `clock.displayValue`, `scoringPlay`, `type.text`)." ] }, { "cell_type": "code", "execution_count": null, "id": "a8e15db7", "metadata": {}, "outputs": [], "source": [ "pbp = safe('Game 5 pbp', lambda: wnba.espn_wnba_pbp(game_id=401726992))\n", "print('dict keys:', list(pbp.keys())[:8] if pbp is not None else None)\n", "if pbp is not None and pbp.get('plays'):\n", " plays = pl.DataFrame(pbp['plays'], infer_schema_length=None)\n", " out = plays.select(['period.number', 'clock.displayValue',\n", " 'type.text', 'text', 'scoringPlay']).head(10)\n", "else:\n", " plays = None\n", " out = 'pbp unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "08fddcb3", "metadata": {}, "source": [ "Filter to scoring plays only to watch the lead change down the stretch." ] }, { "cell_type": "code", "execution_count": null, "id": "26261f85", "metadata": {}, "outputs": [], "source": [ "(plays\n", " .filter(pl.col('scoringPlay'))\n", " .select(['period.number', 'clock.displayValue', 'homeScore', 'awayScore', 'text'])\n", " .tail(8)\n", " if plays is not None else 'pbp unavailable')" ] }, { "cell_type": "markdown", "id": "f41ae207", "metadata": {}, "source": [ "## 🌟 Player season stats β€” Caitlin Clark\n", "\n", "[`espn_wnba_player_stats`](../wnba/reference/additional.md#espn_wnba_player_stats) returns a single **wide** row covering ESPN's `general` / `offensive` / `defensive` stat groups (averages and totals). The 2024 Rookie of the Year, Caitlin Clark, is `athlete_id=4433403`. Pass `total=True` for season totals instead of per-game averages." ] }, { "cell_type": "code", "execution_count": null, "id": "0fcd3ea6", "metadata": {}, "outputs": [], "source": [ "cc = safe('Caitlin Clark stats',\n", " lambda: wnba.espn_wnba_player_stats(athlete_id=4433403, season=SEASON))\n", "(cc.select(['full_name', 'team_abbreviation', 'general_games_played',\n", " 'offensive_avg_points', 'offensive_avg_assists',\n", " 'general_avg_rebounds', 'offensive_three_point_field_goal_pct'])\n", " if cc is not None else 'player stats unavailable')" ] }, { "cell_type": "markdown", "id": "31b4e954", "metadata": {}, "source": [ "## πŸ“Š Team season stats\n", "\n", "[`espn_wnba_team_stats`](../wnba/reference/additional.md#espn_wnba_team_stats) returns a **dict** keyed by category β€” `{'Averages', 'Totals', 'Misc'}`. Each value is a long frame of `stat_name` / `display_value` rows, so index into the dict rather than calling `.head()` on the return directly." ] }, { "cell_type": "code", "execution_count": null, "id": "b5e57150", "metadata": {}, "outputs": [], "source": [ "aces_stats = safe('Aces team stats',\n", " lambda: wnba.espn_wnba_team_stats(team_id=17, season=SEASON))\n", "print('categories:', list(aces_stats.keys()) if aces_stats is not None else None)\n", "(aces_stats['Averages'].select(['stat_name', 'abbreviation', 'display_value']).head(10)\n", " if aces_stats is not None else 'team stats unavailable')" ] }, { "cell_type": "markdown", "id": "bb74025d", "metadata": {}, "source": [ "## 🍳 Cookbook: common WNBA tasks\n", "\n", "Now for the fun part. These twelve recipes are the everyday tasks you'll reach for constantly β€” each blends a premium ESPN call (or a parquet loader) with a few polars expressions. They're all correct, runnable Python. The ESPN-backed recipes wear the `safe()` seatbelt; the loader-backed ones are rock-solid and run bare. πŸ§‘β€πŸ³" ] }, { "cell_type": "markdown", "id": "d6c26d0f", "metadata": {}, "source": [ "### Recipe 1 β€” Standings table πŸ†\n", "\n", "[`espn_wnba_standings`](../wnba/reference/site.md#espn_wnba_standings) gives one row per team with wins, losses, win percentage and point differential. Sort by win percentage to get the playoff picture." ] }, { "cell_type": "code", "execution_count": null, "id": "4930f2d3", "metadata": {}, "outputs": [], "source": [ "standings = safe('2024 standings', lambda: wnba.espn_wnba_standings(season=SEASON))\n", "(standings\n", " .select(['team_display_name', 'wins', 'losses', 'win_percent', 'point_differential'])\n", " .sort('win_percent', descending=True)\n", " .head(8)\n", " if standings is not None else 'standings unavailable')" ] }, { "cell_type": "markdown", "id": "9a87ef8b", "metadata": {}, "source": [ "### Recipe 2 β€” Draft board πŸŽ“\n", "\n", "[`espn_wnba_draft`](../wnba/reference/site.md#espn_wnba_draft) lists every pick for a season. The 2024 draft headlined with Caitlin Clark going first overall to the Indiana Fever." ] }, { "cell_type": "code", "execution_count": null, "id": "4cd694bf", "metadata": {}, "outputs": [], "source": [ "draft = safe('2024 draft', lambda: wnba.espn_wnba_draft(season=SEASON))\n", "(draft.select(['overall_pick', 'team_display_name', 'athlete_display_name',\n", " 'athlete_position_abbreviation', 'school_name']).head(10)\n", " if draft is not None else 'draft unavailable')" ] }, { "cell_type": "markdown", "id": "552acf3c", "metadata": {}, "source": [ "### Recipe 3 β€” Top 10 scorers of the season πŸ“ˆ\n", "\n", "[`load_wnba_player_boxscore`](../wnba/reference/loaders.md#load_wnba_player_boxscore) reads a whole season's player box scores from a parquet release (no per-game API calls). Drop did-not-play rows, then aggregate points and assists per player with polars. Loaders are reliable, so this one runs bare." ] }, { "cell_type": "code", "execution_count": null, "id": "2bda21b0", "metadata": {}, "outputs": [], "source": [ "box = wnba.load_wnba_player_boxscore(seasons=[SEASON])\n", "top_scorers = (\n", " box\n", " .filter(~pl.col('did_not_play'))\n", " .group_by(['athlete_display_name', 'team_abbreviation'])\n", " .agg([\n", " pl.len().alias('games'),\n", " pl.col('points').sum().alias('total_points'),\n", " pl.col('points').mean().round(1).alias('ppg'),\n", " pl.col('assists').mean().round(1).alias('apg'),\n", " ])\n", " .filter(pl.col('games') >= 20)\n", " .sort('ppg', descending=True)\n", " .head(10)\n", ")\n", "top_scorers" ] }, { "cell_type": "markdown", "id": "d7a41149", "metadata": {}, "source": [ "### Recipe 4 β€” Who worked the whistle? πŸ‘€\n", "\n", "[`espn_wnba_game_officials`](../wnba/reference/additional.md#espn_wnba_game_officials) returns the referees assigned to a game β€” handy for officiating studies. Pair a `game_id` from the schedule with this call." ] }, { "cell_type": "code", "execution_count": null, "id": "1f91f216", "metadata": {}, "outputs": [], "source": [ "refs = safe('Game 5 officials',\n", " lambda: wnba.espn_wnba_game_officials(game_id=401726992, season=SEASON))\n", "if refs is not None and refs.height:\n", " keep = [c for c in ['full_name', 'display_name', 'position', 'order'] if c in refs.columns]\n", " out = refs.select(keep) if keep else refs.head()\n", "else:\n", " out = 'officials unavailable'\n", "out" ] }, { "cell_type": "markdown", "id": "c7cbb206", "metadata": {}, "source": [ "### Recipe 5 β€” Best net rating in the league βš–οΈ\n", "\n", "[`load_wnba_team_boxscore`](../wnba/reference/loaders.md#load_wnba_team_boxscore) carries each team's score **and** its opponent's score per game. Average points for minus points against gives a quick-and-dirty net rating β€” the single best one-number summary of who's good. We require 20+ games to drop the All-Star exhibition noise." ] }, { "cell_type": "code", "execution_count": null, "id": "cd821c77", "metadata": {}, "outputs": [], "source": [ "team_box = wnba.load_wnba_team_boxscore(seasons=[SEASON])\n", "net_rating = (\n", " team_box\n", " .group_by(['team_abbreviation', 'team_display_name'])\n", " .agg([\n", " pl.len().alias('games'),\n", " pl.col('team_score').mean().round(1).alias('pts_for'),\n", " pl.col('opponent_team_score').mean().round(1).alias('pts_against'),\n", " ])\n", " .filter(pl.col('games') >= 20)\n", " .with_columns((pl.col('pts_for') - pl.col('pts_against')).round(1).alias('net'))\n", " .sort('net', descending=True)\n", ")\n", "net_rating" ] }, { "cell_type": "markdown", "id": "6ed1e27a", "metadata": {}, "source": [ "### Recipe 6 β€” Double-double machines πŸ…\n", "\n", "Count games where a player hit double digits in two of the five box-score categories (points, rebounds, assists, steals, blocks) β€” the classic double-double, plus triple-doubles for free. All from the player box-score loader and a little polars boolean arithmetic." ] }, { "cell_type": "code", "execution_count": null, "id": "f50037d7", "metadata": {}, "outputs": [], "source": [ "cats = ['points', 'rebounds', 'assists', 'steals', 'blocks']\n", "double_doubles = (\n", " box\n", " .filter(~pl.col('did_not_play'))\n", " .with_columns(\n", " sum((pl.col(c) >= 10).cast(pl.Int8) for c in cats).alias('cats10')\n", " )\n", " .with_columns([\n", " (pl.col('cats10') >= 2).alias('is_dd'),\n", " (pl.col('cats10') >= 3).alias('is_td'),\n", " ])\n", " .group_by(['athlete_display_name', 'team_abbreviation'])\n", " .agg([\n", " pl.col('is_dd').sum().alias('double_doubles'),\n", " pl.col('is_td').sum().alias('triple_doubles'),\n", " ])\n", " .sort(['double_doubles', 'triple_doubles'], descending=True)\n", " .head(10)\n", ")\n", "double_doubles" ] }, { "cell_type": "markdown", "id": "ac45a17a", "metadata": {}, "source": [ "### Recipe 7 β€” Most efficient high-volume scorers 🎯\n", "\n", "Raw points reward volume; **true shooting %** rewards efficiency. TS% = points / (2 Γ— (FGA + 0.44 Γ— FTA)). Aggregate the makes/attempts from the box-score loader, keep players with real workloads, and you've got the league's most efficient buckets." ] }, { "cell_type": "code", "execution_count": null, "id": "0b2562e2", "metadata": {}, "outputs": [], "source": [ "true_shooting = (\n", " box\n", " .filter(~pl.col('did_not_play'))\n", " .group_by(['athlete_display_name', 'team_abbreviation'])\n", " .agg([\n", " pl.len().alias('games'),\n", " pl.col('points').sum().alias('pts'),\n", " pl.col('field_goals_attempted').sum().alias('fga'),\n", " pl.col('free_throws_attempted').sum().alias('fta'),\n", " ])\n", " .filter((pl.col('games') >= 20) & (pl.col('pts') >= 300))\n", " .with_columns(\n", " (pl.col('pts') / (2 * (pl.col('fga') + 0.44 * pl.col('fta'))) * 100)\n", " .round(1).alias('ts_pct')\n", " )\n", " .sort('ts_pct', descending=True)\n", " .head(10)\n", ")\n", "true_shooting" ] }, { "cell_type": "markdown", "id": "5d676b04", "metadata": {}, "source": [ "### Recipe 8 β€” Where do the threes come from? 🎯\n", "\n", "[`load_wnba_shots`](../wnba/reference/loaders.md#load_wnba_shots) is event-level shot data with a `score_value` (the point value of the attempt). Tally made vs. attempted threes per team to see who lives behind the arc β€” and who actually makes them." ] }, { "cell_type": "code", "execution_count": null, "id": "e58e17eb", "metadata": {}, "outputs": [], "source": [ "shots = wnba.load_wnba_shots(seasons=[SEASON])\n", "threes = (\n", " shots\n", " .filter(pl.col('score_value') == 3)\n", " .group_by('team_id')\n", " .agg([\n", " pl.len().alias('three_pt_attempts'),\n", " pl.col('scoring_play').sum().alias('three_pt_makes'),\n", " ])\n", " .with_columns(\n", " (pl.col('three_pt_makes') / pl.col('three_pt_attempts') * 100)\n", " .round(1).alias('three_pt_pct')\n", " )\n", " .sort('three_pt_attempts', descending=True)\n", ")\n", "# attach readable team abbreviations from the team box score\n", "team_names = team_box.select(['team_id', 'team_abbreviation']).unique()\n", "threes.join(team_names, on='team_id', how='left').select(\n", " ['team_abbreviation', 'three_pt_attempts', 'three_pt_makes', 'three_pt_pct']\n", ").head(12)" ] }, { "cell_type": "markdown", "id": "95d216aa", "metadata": {}, "source": [ "### Recipe 9 β€” Head-to-head series βš”οΈ\n", "\n", "Want every meeting between two clubs? Filter the team box-score loader on team + opponent abbreviations and you get the full season series β€” scores, dates and who won. Here's New York vs. Minnesota, the eventual 2024 Finals matchup." ] }, { "cell_type": "code", "execution_count": null, "id": "16613f52", "metadata": {}, "outputs": [], "source": [ "head_to_head = (\n", " team_box\n", " .filter(\n", " (pl.col('team_abbreviation') == 'NY')\n", " & (pl.col('opponent_team_abbreviation') == 'MIN')\n", " )\n", " .select(['game_date', 'team_score', 'opponent_team_score', 'team_winner'])\n", " .sort('game_date')\n", " .with_columns(\n", " pl.when(pl.col('team_winner')).then(pl.lit('NY'))\n", " .otherwise(pl.lit('MIN')).alias('winner')\n", " )\n", ")\n", "print('NY series record vs MIN:',\n", " head_to_head['team_winner'].sum(), '-',\n", " head_to_head.height - head_to_head['team_winner'].sum())\n", "head_to_head" ] }, { "cell_type": "markdown", "id": "dc7c1f3a", "metadata": {}, "source": [ "### Recipe 10 β€” Rolling form: hot and cold streaks πŸ”₯\n", "\n", "A team's last-5 record tells you who's surging into the playoffs. Sort one team's games by date, then a `rolling_sum` over the win flag gives a running 5-game window β€” polars makes the time-series slice a one-liner." ] }, { "cell_type": "code", "execution_count": null, "id": "59ec51e6", "metadata": {}, "outputs": [], "source": [ "form = (\n", " team_box\n", " .filter(pl.col('team_abbreviation') == 'NY')\n", " .sort('game_date')\n", " .with_columns(pl.col('team_winner').cast(pl.Int8).alias('won'))\n", " .with_columns(\n", " pl.col('won').rolling_sum(window_size=5).alias('wins_last5')\n", " )\n", " .select(['game_date', 'opponent_team_abbreviation', 'team_score',\n", " 'opponent_team_score', 'won', 'wins_last5'])\n", " .tail(12)\n", ")\n", "form" ] }, { "cell_type": "markdown", "id": "f20fbc21", "metadata": {}, "source": [ "### Recipe 11 β€” Roster construction by position πŸ‘₯\n", "\n", "[`load_wnba_rosters`](../wnba/reference/loaders.md#load_wnba_rosters) hands you every team's full roster. Pivot guards / forwards / centers per team to see how each front office balances its lineup β€” a clean join-free `pivot`." ] }, { "cell_type": "code", "execution_count": null, "id": "8020b796", "metadata": {}, "outputs": [], "source": [ "rosters = wnba.load_wnba_rosters(seasons=[SEASON])\n", "position_mix = (\n", " rosters\n", " .group_by(['team_abbreviation', 'position_abbreviation'])\n", " .agg(pl.len().alias('n'))\n", " .pivot(values='n', index='team_abbreviation', on='position_abbreviation')\n", " .fill_null(0)\n", " .sort('team_abbreviation')\n", ")\n", "position_mix" ] }, { "cell_type": "markdown", "id": "4fa49f7c", "metadata": {}, "source": [ "### Recipe 12 β€” Season scoring leaders, the pre-aggregated way πŸ“\n", "\n", "Don't want to roll up box scores yourself? [`load_wnba_player_season_stats`](../wnba/reference/loaders.md#load_wnba_player_season_stats) ships ESPN's own season aggregates in **long** format (`category` / `stat_name` / `value`). Filter to the `averages` category and the `avgPoints` stat for an instant scoring leaderboard β€” a great cross-check against Recipe 3." ] }, { "cell_type": "code", "execution_count": null, "id": "a6367c87", "metadata": {}, "outputs": [], "source": [ "season_stats = wnba.load_wnba_player_season_stats(seasons=[SEASON])\n", "scoring_leaders = (\n", " season_stats\n", " .filter(\n", " (pl.col('category') == 'averages')\n", " & (pl.col('stat_name') == 'avgPoints')\n", " )\n", " .select(['athlete_display_name', 'team_display_name',\n", " 'athlete_position_abbreviation', 'value'])\n", " .rename({'value': 'ppg'})\n", " .sort('ppg', descending=True)\n", " .head(10)\n", ")\n", "scoring_leaders" ] }, { "cell_type": "markdown", "id": "37e02416", "metadata": {}, "source": [ "## πŸ“¦ Bulk loaders (`load_wnba_*`)\n", "\n", "The `load_wnba_*` family reads pre-built **parquet releases** (whole seasons at once) instead of calling the live API per game β€” perfect for season-long analysis. They return polars by default (`return_as_pandas=True` for pandas). A few favourites:\n", "\n", "| Loader | Whole-season… |\n", "|---|---|\n", "| [`load_wnba_schedule`](../wnba/reference/loaders.md#load_wnba_schedule) | schedule + results |\n", "| [`load_wnba_player_boxscore`](../wnba/reference/loaders.md#load_wnba_player_boxscore) | player box scores |\n", "| [`load_wnba_team_boxscore`](../wnba/reference/loaders.md#load_wnba_team_boxscore) | team box scores |\n", "| [`load_wnba_player_season_stats`](../wnba/reference/loaders.md#load_wnba_player_season_stats) | season-aggregated player stats |\n", "| [`load_wnba_pbp`](../wnba/reference/loaders.md#load_wnba_pbp) | play-by-play |\n", "| [`load_wnba_shots`](../wnba/reference/loaders.md#load_wnba_shots) | shot locations |\n", "\n", "Pass a list of seasons to combine several years in one frame." ] }, { "cell_type": "markdown", "id": "d7bd9f5b", "source": "## stats.wnba.com surface (`wnba_stats_*`)\n\n`sportsdataverse.wnba` also ships **95 wrappers** for the official `stats.wnba.com` API (WNBA `LeagueID=10`) β€” the capture-confirmed live, non-deprecated endpoints. Every function is named `wnba_stats_`. The module uses the same `curl_cffi` browser-TLS transport as the NBA stats family β€” live calls require `pip install \"sportsdataverse[all]\"` (or `pip install curl_cffi`). WNBA is served by **both** `stats.wnba.com` and `stats.nba.com` (`LeagueID=10`).\n\nWrappers return a tidy **polars DataFrame** by default via the generic `parse_wnba_stats_result_sets` parser (a re-export alias of `parse_nba_stats_result_sets`, which also flattens the shot-location endpoints' grouped headers and unrolls the `scoreboardv3` game feed). Pass `return_as_pandas=True` for pandas.", "metadata": {} }, { "cell_type": "code", "id": "56953b76", "source": "from sportsdataverse.wnba import wnba_stats\n\n# WNBA player dashboard (stats.wnba.com)\n# Live calls require: pip install \"sportsdataverse[all]\" (curl_cffi transport)\nwdf = safe('wnba_stats_leaguedashplayerstats',\n lambda: wnba_stats.wnba_stats_leaguedashplayerstats())\n\nprint('shape:', wdf.shape if wdf is not None else None)\nwdf", "metadata": {}, "execution_count": null, "outputs": [] }, { "cell_type": "code", "execution_count": null, "id": "60053841", "metadata": {}, "outputs": [], "source": [ "sched_2024 = wnba.load_wnba_schedule(seasons=[SEASON])\n", "print('schedule rows:', sched_2024.shape)\n", "box_2024 = wnba.load_wnba_player_boxscore(seasons=[SEASON])\n", "box_2024.select(['game_id', 'game_date', 'athlete_display_name',\n", " 'team_abbreviation', 'minutes', 'points',\n", " 'rebounds', 'assists']).head()" ] }, { "cell_type": "markdown", "id": "c6b81878", "metadata": {}, "source": [ "## πŸŽ‰ Where to next\n", "\n", "- Pass `return_as_pandas=True` for a pandas frame, or `raw=True` (where supported) for the untouched ESPN JSON.\n", "- Full reference: the **WNBA** section in the sidebar β€” [ESPN extras](../wnba/reference/additional.md), [site API](../wnba/reference/site.md), [core API](../wnba/reference/core.md) and [loaders](../wnba/reference/loaders.md).\n", "- R user? The same surface lives in [wehoop](https://wehoop.sportsdataverse.org).\n", "- Want a deeper stats API? [nba_api](https://github.com/swar/nba_api) also covers the WNBA.\n", "\n", "Now go chart some buckets! πŸ€" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## πŸ¦“ Game officials\n", "\n", "Who worked a given game, from the ESPN summary feed.\n" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sportsdataverse.wnba import wnba_game_officials\n", "\n", "safe(\"wnba game officials\", lambda: wnba_game_officials(game_id=1022400100))\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }