{ "cells": [ { "cell_type": "markdown", "id": "e797c1a0", "metadata": {}, "source": [ "# 🏈 The NFL with `sportsdataverse-py`\n", "\n", "Welcome to gridiron data! πŸŽ‰ In a handful of lines you're about to pull\n", "**standings, rosters, weekly injury reports, NextGen Stats tracking\n", "leaderboards, and full play-by-play** β€” straight from the source.\n", "\n", "`sportsdataverse.nfl` leads with the **premium [`api.nfl.com`](https://api.nfl.com)\n", "native endpoints** (`nfl_standings`, `nfl_rosters`, `nfl_injuries`, …) and the\n", "**[NextGen Stats](https://nextgenstats.nfl.com) tracking API** (`nfl_ngs_*`),\n", "backed by the battle-tested **[nflverse](https://nflverse.nflverse.com) release\n", "loaders** (`load_nfl_pbp`, `load_nfl_player_stats`, …). ESPN\n", "(`espn_nfl_*`) rides shotgun as a quick, no-auth secondary path.\n", "\n", "Every accessor hands you a tidy **polars** `DataFrame` by default β€” pass\n", "`return_as_pandas=True` for pandas. If you've used the R packages\n", "[nflfastR](https://www.nflfastr.com) / [nflreadr](https://nflreadr.nflverse.com),\n", "or the Python [nflreadpy](https://github.com/nflverse/nflreadpy), you're already\n", "home: the `load_*` names line up. Let's hike it! 🏈" ] }, { "cell_type": "markdown", "id": "63daf854", "metadata": {}, "source": [ "## 🧰 The toolbox\n", "\n", "Three data families, one module. The 🟒 **premium** rows lead with native\n", "`api.nfl.com` / NextGen Stats endpoints; the πŸ“¦ rows read versioned nflverse\n", "release parquets; ESPN is the πŸ”΅ quick secondary path. Click any name for the\n", "full reference.\n", "\n", "| Function | What it gives you | Source |\n", "|---|---|---|\n", "| [`nfl_standings`](../nfl/reference/nfl_api.md#nfl_standings) | Team standings for a season/week β€” one row per team | 🟒 premium (NFL.com) |\n", "| [`nfl_rosters`](../nfl/reference/nfl_api.md#nfl_rosters) | Season rosters, one row per team (players nested) | 🟒 premium (NFL.com) |\n", "| [`nfl_injuries`](../nfl/reference/nfl_api.md#nfl_injuries) | Weekly injury report, one row per player | 🟒 premium (NFL.com) |\n", "| [`nfl_weeks`](../nfl/reference/nfl_api.md#nfl_weeks) | The week calendar (bye weeks, date ranges) | 🟒 premium (NFL.com) |\n", "| [`nfl_weekly_game_details`](../nfl/reference/nfl_api.md#nfl_weekly_game_details) | Rich per-game details for a week (drive charts, standings) | 🟒 premium (NFL.com) |\n", "| [`nfl_game_summaries`](../nfl/reference/nfl_api.md#nfl_game_summaries) | Live game state, one row per game | 🟒 premium (NFL.com) |\n", "| [`nfl_team`](../nfl/reference/nfl_api.md#nfl_team) | Single-team detail by `team_id` | 🟒 premium (NFL.com) |\n", "| [`nfl_ngs_statboard`](../nfl/reference/additional.md#nfl_ngs_statboard) | NextGen Stats season leaderboard (passing/rushing/receiving) | 🟒 premium (NextGen Stats) |\n", "| [`nfl_ngs_leaders`](../nfl/reference/additional.md#nfl_ngs_leaders) | NextGen top-N highlight boards (speed, YAC over expected, …) | 🟒 premium (NextGen Stats) |\n", "| [`nfl_ngs_league_schedule`](../nfl/reference/additional.md#nfl_ngs_league_schedule) | NextGen schedule β€” source of NGS `gameId`s | 🟒 premium (NextGen Stats) |\n", "| [`nfl_ngs_gamecenter_overview`](../nfl/reference/additional.md#nfl_ngs_gamecenter_overview) | Per-game NextGen player splits (passers/rushers/…) | 🟒 premium (NextGen Stats) |\n", "| [`load_nfl_pbp`](../nfl/reference/loaders.md#load_nfl_pbp) | Full nflfastR play-by-play (370+ columns) | πŸ“¦ nflverse release |\n", "| [`load_nfl_player_stats`](../nfl/reference/additional.md#load_nfl_player_stats) | Weekly player box-score stats | πŸ“¦ nflverse release |\n", "| [`load_nfl_nextgen_stats`](../nfl/reference/additional.md#load_nfl_nextgen_stats) | NextGen Stats back to 2016 (release parquet) | πŸ“¦ nflverse release |\n", "| [`load_nfl_rosters`](../nfl/reference/loaders.md#load_nfl_rosters) | Season rosters with IDs & bios | πŸ“¦ nflverse release |\n", "| [`espn_nfl_schedule`](../nfl/reference/additional.md#espn_nfl_schedule) Β· [`espn_nfl_scoreboard`](../nfl/reference/site.md#espn_nfl_scoreboard) | ESPN scoreboard/schedule (no auth) | πŸ”΅ ESPN (secondary) |\n", "| [`load_nfl_snap_counts`](../nfl/reference/loaders.md#load_nfl_snap_counts) | Weekly snap counts & snap-share % per player | πŸ“¦ nflverse release |\n", "| [`load_nfl_depth_charts`](../nfl/reference/loaders.md#load_nfl_depth_charts) | Weekly depth charts, one row per slotted player | πŸ“¦ nflverse release |\n", "| [`load_nfl_schedule`](../nfl/reference/additional.md#load_nfl_schedule) | Game results + lines, one row per game | πŸ“¦ nflverse release |\n", "| [`load_nfl_draft_picks`](../nfl/reference/additional.md#load_nfl_draft_picks) | Every draft pick + career value, one row per pick | πŸ“¦ nflverse release |\n", "| `get_current_nfl_season` Β· `most_recent_nfl_season` | Season helpers | 🟒 helper |\n" ] }, { "cell_type": "markdown", "id": "e20780bc", "metadata": {}, "source": [ "## πŸ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "**No API key needed** β€” the native `api.nfl.com` wrappers mint a fresh\n", "anonymous token for you, and the NextGen Stats client warms its own browser\n", "cookies. The nflverse loaders just read public release parquets. 😊" ] }, { "cell_type": "code", "execution_count": null, "id": "e42d3324", "metadata": {}, "outputs": [], "source": [ "import polars as pl\n", "import sportsdataverse.nfl as nfl\n", "\n", "pl.Config.set_tbl_cols(8) # keep wide frames readable in the notebook\n" ] }, { "cell_type": "markdown", "id": "ca22338d", "metadata": {}, "source": [ "Native NFL.com / NextGen endpoints and ESPN are *live* services β€” great\n", "in-season, occasionally grumpy in the offseason or behind a flaky network. A\n", "tiny `safe()` helper runs each live call defensively: you get the frame when\n", "the feed is up, and a friendly one-liner when it isn't (never a scary\n", "traceback). πŸ›Ÿ The `load_*` release parquets are reliable, so we call those\n", "directly." ] }, { "cell_type": "code", "execution_count": null, "id": "5f74c3dc", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " \"\"\"Run a live call; return its result, or print a one-liner and return None.\"\"\"\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", "# 2024 is a complete season with full data everywhere β€” a safe default to demo.\n", "SEASON = 2024\n" ] }, { "cell_type": "markdown", "id": "75b7d375", "metadata": {}, "source": [ "## 🟒 Premium first: NFL.com native standings\n", "\n", "The headliner. [`nfl_standings`](../nfl/reference/nfl_api.md#nfl_standings)\n", "returns **one row per team** with conference/division records, streaks,\n", "clinch flags, point differentials β€” the works. Pass `season`, `season_type`\n", "(`\"REG\"`/`\"POST\"`/`\"PRE\"` β€” *strings*, not ESPN's numeric codes) and `week`." ] }, { "cell_type": "code", "execution_count": null, "id": "a66c25f9", "metadata": {}, "outputs": [], "source": [ "standings = safe(\n", " \"NFL.com standings\",\n", " lambda: nfl.nfl_standings(season=SEASON, season_type=\"REG\", week=18),\n", ")\n", "standings.shape if standings is not None else \"standings unavailable\"\n" ] }, { "cell_type": "code", "execution_count": null, "id": "82d0bfc1", "metadata": {}, "outputs": [], "source": [ "cols = [\n", " \"team_full_name\", \"conference_rank\", \"division_rank\",\n", " \"overall_wins\", \"overall_losses\", \"overall_ties\",\n", " \"division_wins\", \"division_losses\",\n", "]\n", "(standings.select([c for c in cols if c in standings.columns])\n", " .sort(\"conference_rank\")\n", " .head(10)\n", " if standings is not None else \"standings unavailable\")\n" ] }, { "cell_type": "markdown", "id": "97fcfb18", "metadata": {}, "source": [ "## πŸ‘₯ Rosters & the week calendar\n", "\n", "[`nfl_rosters`](../nfl/reference/nfl_api.md#nfl_rosters) gives one row per\n", "team for a season, with the player list nested under `persons` (great for a\n", "team directory). [`nfl_weeks`](../nfl/reference/nfl_api.md#nfl_weeks) is the\n", "season's week calendar β€” handy for finding bye weeks and date ranges before\n", "you loop over a slate.\n", "\n", "| Function | One row per | Key columns |\n", "|---|---|---|\n", "| [`nfl_rosters`](../nfl/reference/nfl_api.md#nfl_rosters) | team | `team_abbreviation`, `team_conference_abbr`, `persons` |\n", "| [`nfl_weeks`](../nfl/reference/nfl_api.md#nfl_weeks) | week | `week`, `week_type`, `bye_teams`, `date_begin` |" ] }, { "cell_type": "code", "execution_count": null, "id": "6510e4f1", "metadata": {}, "outputs": [], "source": [ "rosters = safe(\"NFL.com rosters\", lambda: nfl.nfl_rosters(season=SEASON))\n", "cols = [\"team_abbreviation\", \"team_full_name\", \"team_conference_abbr\", \"team_division_full_name\"]\n", "(rosters.select([c for c in cols if c in rosters.columns]).head(8)\n", " if rosters is not None else \"rosters unavailable\")\n" ] }, { "cell_type": "code", "execution_count": null, "id": "3873040a", "metadata": {}, "outputs": [], "source": [ "weeks = safe(\"NFL.com weeks\", lambda: nfl.nfl_weeks(season=SEASON, season_type=\"REG\"))\n", "cols = [\"season\", \"week\", \"week_type\", \"date_begin\", \"date_end\", \"bye_teams\"]\n", "(weeks.select([c for c in cols if c in weeks.columns]).head(8)\n", " if weeks is not None else \"weeks unavailable\")\n" ] }, { "cell_type": "markdown", "id": "e282b026", "metadata": {}, "source": [ "## πŸ₯ The weekly injury report\n", "\n", "[`nfl_injuries`](../nfl/reference/nfl_api.md#nfl_injuries) is the official\n", "weekly injury report β€” **one row per listed player** with their\n", "`injury_status` (Out / Doubtful / Questionable), practice participation, and\n", "team. This is the premium native feed, not a scrape." ] }, { "cell_type": "code", "execution_count": null, "id": "5ea15081", "metadata": {}, "outputs": [], "source": [ "inj = safe(\n", " \"NFL.com injuries\",\n", " lambda: nfl.nfl_injuries(season=SEASON, season_type=\"REG\", week=1),\n", ")\n", "cols = [\n", " \"team_full_name\", \"person_display_name\", \"position\",\n", " \"injuries\", \"injury_status\", \"practice_status\",\n", "]\n", "(inj.select([c for c in cols if c in inj.columns]).head(10)\n", " if inj is not None else \"injuries unavailable\")\n" ] }, { "cell_type": "markdown", "id": "baf3544e", "metadata": {}, "source": [ "## πŸ“‹ Per-game details for a week\n", "\n", "Need the full slate with drive charts, broadcast info and embedded standings?\n", "[`nfl_weekly_game_details`](../nfl/reference/nfl_api.md#nfl_weekly_game_details)\n", "returns **one row per game** for a week (toggle the heavy blocks with the\n", "`include_*` flags). For live in-game state (clock, down & distance, red-zone\n", "flags), reach for\n", "[`nfl_game_summaries`](../nfl/reference/nfl_api.md#nfl_game_summaries)." ] }, { "cell_type": "code", "execution_count": null, "id": "2b1e1ce4", "metadata": {}, "outputs": [], "source": [ "wgd = safe(\n", " \"NFL.com weekly game details\",\n", " lambda: nfl.nfl_weekly_game_details(season=SEASON, season_type=\"REG\", week=1),\n", ")\n", "cols = [\"week\", \"date\", \"game_type\", \"away_team_full_name\", \"home_team_full_name\", \"status\"]\n", "(wgd.select([c for c in cols if c in wgd.columns]).head(8)\n", " if wgd is not None else \"weekly game details unavailable\")\n" ] }, { "cell_type": "markdown", "id": "44dd2e2e", "metadata": {}, "source": [ "## ⚑ NextGen Stats: the tracking layer\n", "\n", "This is where it gets *fun*. The NFL's **NextGen Stats** API exposes\n", "player-tracking metrics you won't find in a box score β€” time to throw,\n", "completion percentage over expectation (CPOE), separation, ball-carrier\n", "top speed. All token-free.\n", "\n", "[`nfl_ngs_statboard`](../nfl/reference/additional.md#nfl_ngs_statboard) is the\n", "season leaderboard. Ask for `stat_type` `\"passing\"`, `\"rushing\"`, or\n", "`\"receiving\"`." ] }, { "cell_type": "code", "execution_count": null, "id": "e4251cf6", "metadata": {}, "outputs": [], "source": [ "qb = safe(\n", " \"NGS passing statboard\",\n", " lambda: nfl.nfl_ngs_statboard(stat_type=\"passing\", season=SEASON, season_type=\"REG\"),\n", ")\n", "cols = [\n", " \"playerName\", \"passerRating\", \"completionPercentageAboveExpectation\",\n", " \"avgTimeToThrow\", \"aggressiveness\", \"passYards\", \"passTouchdowns\",\n", "]\n", "(qb.select([c for c in cols if c in qb.columns])\n", " .sort(\"passerRating\", descending=True)\n", " .head(10)\n", " if qb is not None and \"passerRating\" in qb.columns else \"NGS statboard unavailable\")\n" ] }, { "cell_type": "markdown", "id": "ef2f183e", "metadata": {}, "source": [ "And [`nfl_ngs_leaders`](../nfl/reference/additional.md#nfl_ngs_leaders)\n", "serves the highlight-reel top-N boards β€” each row is the *play* that earned\n", "the leader their spot. Categories include `\"speed\"` (fastest ball carriers),\n", "`\"yac_season\"` (yards-after-catch over expected), `\"completion_season\"`\n", "(most-improbable completions) and more." ] }, { "cell_type": "code", "execution_count": null, "id": "502756dc", "metadata": {}, "outputs": [], "source": [ "fast = safe(\n", " \"NGS fastest ball carriers\",\n", " lambda: nfl.nfl_ngs_leaders(category=\"speed\", season=SEASON, season_type=\"REG\"),\n", ")\n", "cols = [\"leader_playerName\", \"leader_teamAbbr\", \"leader_maxSpeed\", \"leader_yards\", \"play_playDescription\"]\n", "(fast.select([c for c in cols if c in fast.columns]).head(8)\n", " if fast is not None else \"NGS leaders unavailable\")\n" ] }, { "cell_type": "markdown", "id": "7308d14f", "metadata": {}, "source": [ "## πŸ“¦ nflverse loaders: the bulk-data workhorses\n", "\n", "For full-season modelling you want the **nflverse release parquets** β€” the\n", "exact same assets that power nflfastR / nflreadr / nflreadpy. These are\n", "versioned, cached releases (very reliable), so we call them directly.\n", "\n", "| Function | Rows | Highlights |\n", "|---|---|---|\n", "| [`load_nfl_pbp`](../nfl/reference/loaders.md#load_nfl_pbp) | ~49k/season | EPA, WP, air yards, 370+ columns |\n", "| [`load_nfl_player_stats`](../nfl/reference/additional.md#load_nfl_player_stats) | weekly | passing/rushing/receiving box lines |\n", "| [`load_nfl_nextgen_stats`](../nfl/reference/additional.md#load_nfl_nextgen_stats) | weekly | NGS back to 2016 |\n", "| [`load_nfl_rosters`](../nfl/reference/loaders.md#load_nfl_rosters) | per player | IDs, bios, draft info |" ] }, { "cell_type": "code", "execution_count": null, "id": "8cbe86c1", "metadata": {}, "outputs": [], "source": [ "pbp = nfl.load_nfl_pbp([SEASON])\n", "pbp.shape\n" ] }, { "cell_type": "code", "execution_count": null, "id": "e2f8286f", "metadata": {}, "outputs": [], "source": [ "(pbp\n", " .filter(pl.col(\"play_type\").is_not_null())\n", " .select([\"game_id\", \"qtr\", \"down\", \"ydstogo\", \"posteam\", \"play_type\", \"yards_gained\", \"epa\", \"desc\"])\n", " .head(8))\n" ] }, { "cell_type": "code", "execution_count": null, "id": "25b6a355", "metadata": {}, "outputs": [], "source": [ "ngs_release = nfl.load_nfl_nextgen_stats([SEASON], stat_type=\"passing\")\n", "(ngs_release\n", " .filter(pl.col(\"week\") == 0) # week 0 == season totals in this release\n", " .select([\"player_display_name\", \"team_abbr\", \"attempts\", \"pass_yards\",\n", " \"completion_percentage_above_expectation\", \"passer_rating\"])\n", " .sort(\"passer_rating\", descending=True)\n", " .head(8))\n" ] }, { "cell_type": "markdown", "id": "8d110c48", "metadata": {}, "source": [ "## πŸ”΅ Secondary path: ESPN (quick & no-auth)\n", "\n", "When you just want a fast scoreboard without minting a token, ESPN is right\n", "there. [`espn_nfl_schedule`](../nfl/reference/additional.md#espn_nfl_schedule)\n", "returns a tidy schedule frame; pass `dates=YYYYMMDD` for a single day. (There's\n", "also a raw [`espn_nfl_scoreboard`](../nfl/reference/site.md#espn_nfl_scoreboard)\n", "if you want the unparsed JSON.)" ] }, { "cell_type": "code", "execution_count": null, "id": "79dded79", "metadata": {}, "outputs": [], "source": [ "espn_sched = safe(\"ESPN schedule\", lambda: nfl.espn_nfl_schedule(dates=20240908))\n", "cols = [\"id\", \"away_display_name\", \"home_display_name\", \"away_score\", \"home_score\", \"status_type_description\"]\n", "(espn_sched.select([c for c in cols if c in espn_sched.columns]).head(8)\n", " if espn_sched is not None else \"ESPN schedule unavailable\")\n" ] }, { "cell_type": "markdown", "id": "452ec0a4", "metadata": {}, "source": [ "## 🍳 Cookbook: common NFL tasks\n", "\n", "A full dozen recipes you'll reach for constantly β€” leaderboards, splits,\n", "team-level efficiency, snap-share workhorses, play-by-play slices, a\n", "schedule scan, and a quick hop into pandas. Each one leans on the\n", "**premium** native/NextGen feeds or the rock-solid **nflverse** release\n", "parquets, and every live call is wrapped so a network blip never breaks\n", "your run. Recipes 1–4 use the live NFL.com / NextGen endpoints; 5–12 build\n", "on the cached release parquets, so they run anywhere, anytime. 🏈" ] }, { "cell_type": "markdown", "id": "8cba6790", "metadata": {}, "source": [ "### Recipe 1 β€” This week's \"Out\" list πŸš‘\n", "\n", "Filter the official injury report down to players ruled **Out** β€” exactly what\n", "you'd check before setting a lineup." ] }, { "cell_type": "code", "execution_count": null, "id": "91704815", "metadata": {}, "outputs": [], "source": [ "rep = safe(\n", " \"injury report\",\n", " lambda: nfl.nfl_injuries(season=SEASON, season_type=\"REG\", week=1),\n", ")\n", "if rep is not None and rep.height and \"injury_status\" in rep.columns:\n", " out = (\n", " rep.filter(pl.col(\"injury_status\").str.to_lowercase() == \"out\")\n", " .select([c for c in [\"team_full_name\", \"person_display_name\", \"position\", \"injuries\"]\n", " if c in rep.columns])\n", " .head(15)\n", " )\n", "else:\n", " out = \"injury report unavailable\"\n", "out\n" ] }, { "cell_type": "markdown", "id": "245ff153", "metadata": {}, "source": [ "### Recipe 2 β€” CPOE leaderboard from NextGen Stats 🎯\n", "\n", "Who's beating expectation as a passer? Rank qualified QBs by **completion\n", "percentage above expectation** straight off the NextGen statboard." ] }, { "cell_type": "code", "execution_count": null, "id": "b68a2dc7", "metadata": {}, "outputs": [], "source": [ "board = safe(\n", " \"NGS passing board\",\n", " lambda: nfl.nfl_ngs_statboard(stat_type=\"passing\", season=SEASON, season_type=\"REG\"),\n", ")\n", "if board is not None and board.height and \"completionPercentageAboveExpectation\" in board.columns:\n", " cpoe = (\n", " board.filter(pl.col(\"attempts\") >= 200)\n", " .select([\"playerName\", \"attempts\", \"completionPercentage\",\n", " \"completionPercentageAboveExpectation\", \"passerRating\"])\n", " .sort(\"completionPercentageAboveExpectation\", descending=True)\n", " .head(10)\n", " )\n", "else:\n", " cpoe = \"NGS board unavailable\"\n", "cpoe\n" ] }, { "cell_type": "markdown", "id": "85b21b55", "metadata": {}, "source": [ "### Recipe 3 β€” Standings β†’ division winners πŸ†\n", "\n", "Take the premium standings and pull the team that tops each division. One\n", "group-by and you've got your playoff-seeding cheat sheet." ] }, { "cell_type": "code", "execution_count": null, "id": "e5630417", "metadata": {}, "outputs": [], "source": [ "st = safe(\n", " \"standings\",\n", " lambda: nfl.nfl_standings(season=SEASON, season_type=\"REG\", week=18),\n", ")\n", "if st is not None and st.height and {\"division_rank\", \"team_full_name\"}.issubset(st.columns):\n", " div_col = next((c for c in [\"team_division_full_name\", \"division_full_name\", \"division\"] if c in st.columns), None)\n", " keep = [c for c in [div_col, \"team_full_name\", \"overall_wins\", \"overall_losses\"] if c]\n", " winners = (\n", " st.filter(pl.col(\"division_rank\") == 1)\n", " .select(keep)\n", " .sort(div_col) if div_col else st.filter(pl.col(\"division_rank\") == 1).select(keep)\n", " )\n", "else:\n", " winners = \"standings unavailable\"\n", "winners\n" ] }, { "cell_type": "markdown", "id": "34102fe7", "metadata": {}, "source": [ "### Recipe 4 β€” A game's NextGen passer splits πŸ”¬\n", "\n", "Grab an NGS `gameId` from the schedule, then pull\n", "[`nfl_ngs_gamecenter_overview`](../nfl/reference/additional.md#nfl_ngs_gamecenter_overview)\n", "to see each side's primary passer with tracking-derived splits." ] }, { "cell_type": "code", "execution_count": null, "id": "5022e3a0", "metadata": {}, "outputs": [], "source": [ "sched = safe(\n", " \"NGS schedule\",\n", " lambda: nfl.nfl_ngs_league_schedule(season=SEASON, season_type=\"REG\", week=1),\n", ")\n", "if sched is not None and sched.height and \"gameId\" in sched.columns:\n", " gid = sched[\"gameId\"][0]\n", " ov = safe(f\"NGS gamecenter {gid}\",\n", " lambda: nfl.nfl_ngs_gamecenter_overview(game_id=gid, group=\"passers\"))\n", " if ov is not None and ov.height:\n", " out = ov.select([c for c in [\"side\", \"teamAbbr\", \"playerName\", \"position\",\n", " \"completions\", \"attempts\", \"passYards\", \"touchdowns\"]\n", " if c in ov.columns])\n", " else:\n", " out = \"gamecenter unavailable\"\n", "else:\n", " out = \"NGS schedule unavailable\"\n", "out\n" ] }, { "cell_type": "markdown", "id": "29b1a607", "metadata": {}, "source": [ "### Recipe 5 β€” Season rushing leaders πŸƒ\n", "\n", "Roll the weekly box scores in [`load_nfl_player_stats`](../nfl/reference/additional.md#load_nfl_player_stats) up to season totals and crown the ground-game kings (β‰₯150 carries)." ] }, { "cell_type": "code", "execution_count": null, "id": "5e219a8a", "metadata": {}, "outputs": [], "source": [ "ps = nfl.load_nfl_player_stats()\n", "rush_cols = {\"season\", \"season_type\", \"carries\", \"rushing_yards\"}\n", "if rush_cols.issubset(ps.columns):\n", " rush_lb = (\n", " ps.filter((pl.col(\"season\") == SEASON) & (pl.col(\"season_type\") == \"REG\"))\n", " .group_by([\"player_display_name\", \"recent_team\"])\n", " .agg(\n", " pl.col(\"carries\").sum().alias(\"carries\"),\n", " pl.col(\"rushing_yards\").sum().alias(\"rush_yds\"),\n", " pl.col(\"rushing_tds\").sum().alias(\"rush_td\"),\n", " pl.col(\"rushing_epa\").sum().round(1).alias(\"rush_epa\"),\n", " )\n", " .filter(pl.col(\"carries\") >= 150)\n", " .sort(\"rush_yds\", descending=True)\n", " .head(10)\n", " )\n", "else:\n", " rush_lb = \"player_stats schema changed β€” rushing columns missing\"\n", "rush_lb" ] }, { "cell_type": "markdown", "id": "16289eab", "metadata": {}, "source": [ "### Recipe 6 β€” The most efficient offenses (EPA/play) πŸ“ˆ\n", "\n", "Expected points added is the modeller's favourite efficiency yardstick. Average `epa` over every run/pass in [`load_nfl_pbp`](../nfl/reference/loaders.md#load_nfl_pbp) to rank offenses." ] }, { "cell_type": "code", "execution_count": null, "id": "9d7d1ce9", "metadata": {}, "outputs": [], "source": [ "pbp = nfl.load_nfl_pbp([SEASON])\n", "if {\"epa\", \"posteam\", \"play_type\"}.issubset(pbp.columns):\n", " epa_off = (\n", " pbp.filter(pl.col(\"play_type\").is_in([\"run\", \"pass\"]))\n", " .group_by(\"posteam\")\n", " .agg(\n", " pl.col(\"epa\").mean().round(3).alias(\"epa_per_play\"),\n", " pl.len().alias(\"plays\"),\n", " )\n", " .filter(pl.col(\"posteam\").is_not_null())\n", " .sort(\"epa_per_play\", descending=True)\n", " .head(10)\n", " )\n", "else:\n", " epa_off = \"pbp schema changed β€” epa/posteam columns missing\"\n", "epa_off" ] }, { "cell_type": "markdown", "id": "376e7b40", "metadata": {}, "source": [ "### Recipe 7 β€” Third-down conversion kings πŸ”‘\n", "\n", "Move-the-chains efficiency: keep only 3rd-down run/pass snaps and divide conversions by attempts per offense β€” a classic play-by-play split." ] }, { "cell_type": "code", "execution_count": null, "id": "ce22b6fb", "metadata": {}, "outputs": [], "source": [ "if {\"down\", \"third_down_converted\", \"posteam\"}.issubset(pbp.columns):\n", " third = (\n", " pbp.filter((pl.col(\"down\") == 3) & (pl.col(\"play_type\").is_in([\"run\", \"pass\"])))\n", " .group_by(\"posteam\")\n", " .agg(\n", " pl.col(\"third_down_converted\").sum().alias(\"conversions\"),\n", " pl.len().alias(\"attempts\"),\n", " )\n", " .filter(pl.col(\"posteam\").is_not_null())\n", " .with_columns((pl.col(\"conversions\") / pl.col(\"attempts\") * 100).round(1).alias(\"conv_pct\"))\n", " .sort(\"conv_pct\", descending=True)\n", " .head(10)\n", " )\n", "else:\n", " third = \"pbp schema changed β€” third-down columns missing\"\n", "third" ] }, { "cell_type": "markdown", "id": "d4cac034", "metadata": {}, "source": [ "### Recipe 8 β€” Red-zone touchdown efficiency 🎯\n", "\n", "Filter the play-by-play to snaps inside the opponent's 20 (`yardline_100 ≀ 20`) and see which offenses actually punch it in instead of settling for three." ] }, { "cell_type": "code", "execution_count": null, "id": "5bb244f5", "metadata": {}, "outputs": [], "source": [ "if {\"yardline_100\", \"touchdown\", \"posteam\"}.issubset(pbp.columns):\n", " redzone = (\n", " pbp.filter((pl.col(\"yardline_100\") <= 20) & (pl.col(\"play_type\").is_in([\"run\", \"pass\"])))\n", " .group_by(\"posteam\")\n", " .agg(\n", " pl.col(\"touchdown\").sum().alias(\"rz_tds\"),\n", " pl.len().alias(\"rz_plays\"),\n", " )\n", " .filter((pl.col(\"posteam\").is_not_null()) & (pl.col(\"rz_plays\") >= 80))\n", " .with_columns((pl.col(\"rz_tds\") / pl.col(\"rz_plays\") * 100).round(1).alias(\"td_pct\"))\n", " .sort(\"td_pct\", descending=True)\n", " .head(10)\n", " )\n", "else:\n", " redzone = \"pbp schema changed β€” red-zone columns missing\"\n", "redzone" ] }, { "cell_type": "markdown", "id": "c0652aac", "metadata": {}, "source": [ "### Recipe 9 β€” Snap-share workhorse running backs 🐴\n", "\n", "[`load_nfl_snap_counts`](../nfl/reference/loaders.md#load_nfl_snap_counts) carries `offense_pct` per game β€” average it to find the backs their teams simply would not take off the field." ] }, { "cell_type": "code", "execution_count": null, "id": "2339555d", "metadata": {}, "outputs": [], "source": [ "snaps = nfl.load_nfl_snap_counts([SEASON])\n", "if {\"position\", \"offense_pct\", \"player\"}.issubset(snaps.columns):\n", " workhorses = (\n", " snaps.filter(pl.col(\"position\") == \"RB\")\n", " .group_by([\"player\", \"team\"])\n", " .agg(\n", " (pl.col(\"offense_pct\").mean() * 100).round(1).alias(\"avg_snap_pct\"),\n", " pl.len().alias(\"games\"),\n", " )\n", " .filter(pl.col(\"games\") >= 10)\n", " .sort(\"avg_snap_pct\", descending=True)\n", " .head(10)\n", " )\n", "else:\n", " workhorses = \"snap_counts schema changed β€” offense_pct/position missing\"\n", "workhorses" ] }, { "cell_type": "markdown", "id": "91a82b95", "metadata": {}, "source": [ "### Recipe 10 β€” Receiving leaders, then a hop into pandas 🐼\n", "\n", "Aggregate the receiving box lines, `.to_pandas()`, and add derived columns (yards-per-catch, catch rate) with familiar pandas syntax β€” the polarsβ†’pandas handoff is one method call." ] }, { "cell_type": "code", "execution_count": null, "id": "a23d419b", "metadata": {}, "outputs": [], "source": [ "rec_cols = {\"receptions\", \"receiving_yards\", \"targets\", \"season\", \"season_type\"}\n", "if rec_cols.issubset(ps.columns):\n", " rec = (\n", " ps.filter((pl.col(\"season\") == SEASON) & (pl.col(\"season_type\") == \"REG\"))\n", " .group_by([\"player_display_name\", \"recent_team\"])\n", " .agg(\n", " pl.col(\"receptions\").sum().alias(\"rec\"),\n", " pl.col(\"receiving_yards\").sum().alias(\"rec_yds\"),\n", " pl.col(\"targets\").sum().alias(\"tgt\"),\n", " )\n", " .filter(pl.col(\"rec\") >= 70)\n", " )\n", " pdf = rec.to_pandas() # <-- polars -> pandas in one call\n", " pdf[\"yards_per_rec\"] = (pdf[\"rec_yds\"] / pdf[\"rec\"]).round(1)\n", " pdf[\"catch_rate\"] = (pdf[\"rec\"] / pdf[\"tgt\"] * 100).round(1)\n", " rec_out = pdf.sort_values(\"rec_yds\", ascending=False).head(10)[\n", " [\"player_display_name\", \"recent_team\", \"rec\", \"rec_yds\", \"yards_per_rec\", \"catch_rate\"]\n", " ].reset_index(drop=True)\n", "else:\n", " rec_out = \"player_stats schema changed β€” receiving columns missing\"\n", "rec_out" ] }, { "cell_type": "markdown", "id": "cfeed902", "metadata": {}, "source": [ "### Recipe 11 β€” Who gets open? NextGen separation πŸ›°οΈ\n", "\n", "The receiving statboard exposes a tracking-only metric box scores can't: **average separation** at the catch point. Rank qualified targets (β‰₯80) to find the route-runners defenders can't shadow." ] }, { "cell_type": "code", "execution_count": null, "id": "c74ee118", "metadata": {}, "outputs": [], "source": [ "sepboard = safe(\n", " \"NGS receiving statboard\",\n", " lambda: nfl.nfl_ngs_statboard(stat_type=\"receiving\", season=SEASON, season_type=\"REG\"),\n", ")\n", "if sepboard is not None and sepboard.height and \"avgSeparation\" in sepboard.columns:\n", " sep = (\n", " sepboard.filter(pl.col(\"targets\") >= 80)\n", " .select([c for c in [\n", " \"player_displayName\", \"player_position\", \"avgSeparation\",\n", " \"avgYACAboveExpectation\", \"catchPercentage\", \"yards\",\n", " ] if c in sepboard.columns])\n", " .sort(\"avgSeparation\", descending=True)\n", " .head(10)\n", " )\n", "else:\n", " sep = \"NGS receiving statboard unavailable\"\n", "sep" ] }, { "cell_type": "markdown", "id": "899bd2e8", "metadata": {}, "source": [ "### Recipe 12 β€” The nail-biters: closest games of the season 😬\n", "\n", "[`load_nfl_schedule`](../nfl/reference/additional.md#load_nfl_schedule) carries the final `result` (home margin). Take its absolute value and sort ascending to surface the one-score thrillers β€” built-in betting lines ride along too." ] }, { "cell_type": "code", "execution_count": null, "id": "90c099e2", "metadata": {}, "outputs": [], "source": [ "sched = nfl.load_nfl_schedule([SEASON])\n", "if {\"result\", \"game_type\", \"home_team\", \"away_team\"}.issubset(sched.columns):\n", " scored = (\n", " sched.filter((pl.col(\"game_type\") == \"REG\") & pl.col(\"result\").is_not_null())\n", " .with_columns(pl.col(\"result\").abs().alias(\"margin\"))\n", " )\n", " want = [\n", " \"week\", \"away_team\", \"away_score\", \"home_team\", \"home_score\",\n", " \"margin\", \"spread_line\", \"total_line\",\n", " ]\n", " nailbiters = (\n", " scored.select([c for c in want if c in scored.columns])\n", " .sort(\"margin\")\n", " .head(10)\n", " )\n", "else:\n", " nailbiters = \"schedule schema changed β€” result/team columns missing\"\n", "nailbiters" ] }, { "cell_type": "markdown", "id": "faff0adb", "metadata": {}, "source": [ "## πŸ—“οΈ Season helpers\n", "\n", "Handy when you want \"the current/most-recent season\" instead of hard-coding a\n", "year. `get_current_nfl_season()` / `get_current_nfl_week()` track the live\n", "calendar; `most_recent_nfl_season()` gives the latest season with data." ] }, { "cell_type": "code", "execution_count": null, "id": "1ebcaad3", "metadata": {}, "outputs": [], "source": [ "{\n", " \"current_season\": nfl.get_current_nfl_season(),\n", " \"current_week\": nfl.get_current_nfl_week(),\n", " \"most_recent_season\": nfl.most_recent_nfl_season(),\n", "}\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## πŸ† Season standings + playoff seeding β€” `nfl_season_standings`\n", "\n", "a faithful port of the nflseedR v2 standings engine. It\n", "computes division ranks, conference seeds, and the full tiebreaker cascade\n", "straight from a games frame β€” the schedule loader output works as-is:" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "from sportsdataverse.nfl import load_nfl_schedule, nfl_season_standings\n", "\n", "standings = nfl_season_standings(load_nfl_schedule(seasons=[2024]))\n", "print(\"2024 standings:\", standings.shape)\n", "standings.select([\"team\", \"division\", \"games\", \"wins\", \"losses\", \"true_wins\"]).head(8)" ] }, { "cell_type": "markdown", "id": "a04c8650", "metadata": {}, "source": [ "## πŸŽ‰ Where to next\n", "\n", "- **Premium native API** β€” the full `nfl_*` endpoint set:\n", " [`docs/docs/nfl/reference/nfl_api.md`](../nfl/reference/nfl_api.md)\n", "- **NextGen Stats & loaders** β€” `nfl_ngs_*` and `load_nfl_*`:\n", " [`docs/docs/nfl/reference/additional.md`](../nfl/reference/additional.md)\n", " and [`docs/docs/nfl/reference/loaders.md`](../nfl/reference/loaders.md)\n", "- **ESPN secondary path** β€” every `espn_nfl_*` wrapper:\n", " [`docs/docs/nfl/reference/site.md`](../nfl/reference/site.md)\n", "- Pass `return_as_pandas=True` for a pandas frame, or `return_parsed=False`\n", " (native API) for raw JSON.\n", "- R user? The same data lives in [nflfastR](https://www.nflfastr.com) /\n", " [nflreadr](https://nflreadr.nflverse.com); Python parity is\n", " [nflreadpy](https://github.com/nflverse/nflreadpy).\n", "\n", "Now go build something great β€” may your EPA be ever positive! πŸ“ˆπŸˆ" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 🏟️ NFL Pro, PFF and the Shield adapter\n", "\n", "Three NFL-only sources beyond ESPN and nflverse: **NFL Pro** (`pro.nfl.com` Next\n", "Gen Stats, no key), the **PFF Developer API** (`api.pff.com`, needs\n", "`SDV_PY_PFF_API_KEY`), and the **Shield** play-by-play parser that feeds\n", "`NFLPlayProcess` with `source=\"shield\"`.\n" ] }, { "cell_type": "code", "metadata": {}, "execution_count": null, "outputs": [], "source": [ "import os\n", "\n", "from sportsdataverse.nfl.nflpro import nfl_pro_players_offense_passing_season\n", "\n", "ngs = safe(\"nfl_pro passing (season)\", lambda: nfl_pro_players_offense_passing_season(season=2024))\n", "if ngs is not None:\n", " print(ngs.columns[:8])\n", "\n", "# PFF needs a key; the cell is a no-op without one rather than a traceback.\n", "if os.environ.get(\"SDV_PY_PFF_API_KEY\") or os.environ.get(\"PFF_API_KEY\"):\n", " from sportsdataverse.nfl.pff_api import pff_api_facet_defense_coverage\n", "\n", " safe(\"pff_api defense coverage\", lambda: pff_api_facet_defense_coverage(season=2024))\n", "else:\n", " print(\"no PFF key set - skipping pff_api_* (set SDV_PY_PFF_API_KEY)\")\n", "\n", "# The Shield parser is what source=\"shield\" routes through.\n", "from sportsdataverse.nfl import shield_pbp\n", "\n", "print(\"shield_pbp:\", [n for n in dir(shield_pbp) 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 }