{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# NFL\n",
    "\n",
    "Ten charts and tables from one season of nflverse play-by-play: team logos on a scatter, bars in team colors, a\n",
    "standings table, small multiples, a quarterback headshot chart, relocated franchises, an interactive plot, a field in\n",
    "team colors and a tier list. The data comes from the nflverse releases through `sportsdataverse.nfl`; sdvplot takes\n",
    "the abbreviations straight from it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import polars as pl\n",
    "import sportsdataverse.nfl as nfl\n",
    "\n",
    "import sdvplot\n",
    "\n",
    "SEASON = 2025\n",
    "CAPTION = f\"Data: nflverse via sportsdataverse-py | {SEASON} regular season\"\n",
    "\n",
    "pbp = nfl.load_nfl_pbp([SEASON])\n",
    "plays = pbp.filter(\n",
    "    pl.col(\"season_type\") == \"REG\",\n",
    "    pl.col(\"play_type\").is_in([\"pass\", \"run\"]),\n",
    "    pl.col(\"epa\").is_not_null(),\n",
    ")\n",
    "plays.height"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "## 1. Offense vs defense EPA per play\n",
    "\n",
    "The chart every NFL season ends with: each team's offensive EPA per play against the EPA per play its defense\n",
    "allowed, with the team's logo as the point. The defense axis is flipped so the good teams sit top right."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Scatter of the 32 NFL teams' 2025 offensive and defensive EPA per play, each point drawn as the team logo",
     "title": "NFL offense vs defense EPA"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "offense = plays.group_by(\"posteam\", maintain_order=True).agg(\n",
    "    off_epa=pl.col(\"epa\").mean(), off_sr=pl.col(\"success\").mean()\n",
    ")\n",
    "defense = plays.group_by(\"defteam\", maintain_order=True).agg(\n",
    "    def_epa=pl.col(\"epa\").mean(), def_sr=pl.col(\"success\").mean()\n",
    ")\n",
    "teams = offense.join(defense, left_on=\"posteam\", right_on=\"defteam\").rename({\"posteam\": \"team\"})\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.axvline(teams[\"off_epa\"].mean(), color=\"grey\", lw=0.8, ls=\"--\")\n",
    "ax.axhline(teams[\"def_epa\"].mean(), color=\"grey\", lw=0.8, ls=\"--\")\n",
    "ax.scatter(teams[\"off_epa\"], teams[\"def_epa\"], s=0)  # sets the axis limits; the logos are the marks\n",
    "ax.margins(0.08)\n",
    "sdvplot.add_logos(ax, teams[\"off_epa\"], teams[\"def_epa\"], teams[\"team\"], league=\"nfl\", season=SEASON, height=0.07)\n",
    "ax.invert_yaxis()\n",
    "for x, y, text in [(0.98, 0.98, \"good offense, good defense\"), (0.02, 0.02, \"bad offense, bad defense\")]:\n",
    "    ax.text(\n",
    "        x,\n",
    "        y,\n",
    "        text,\n",
    "        transform=ax.transAxes,\n",
    "        ha=\"right\" if x > 0.5 else \"left\",\n",
    "        va=\"top\" if y > 0.5 else \"bottom\",\n",
    "        color=\"grey\",\n",
    "        fontsize=9,\n",
    "    )\n",
    "ax.set(xlabel=\"Offense EPA per play\", ylabel=\"Defense EPA per play allowed (better is up)\")\n",
    "ax.set_title(f\"NFL offense vs defense, {SEASON}\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, CAPTION, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 2. A ranked bar chart with logos on the axis\n",
    "\n",
    "Offensive success rate, sorted, each bar in its team's primary color from `team_colors`, and `axis_logos` swapping\n",
    "the abbreviations under the bars for logos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from matplotlib.ticker import MultipleLocator, PercentFormatter\n",
    "\n",
    "ranked = teams.sort(\"off_sr\", descending=True)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 5))\n",
    "ax.bar(ranked[\"team\"], ranked[\"off_sr\"], color=sdvplot.team_colors(ranked[\"team\"], \"nfl\").to_list())\n",
    "ax.set_ylim(ranked[\"off_sr\"].min() - 0.02, ranked[\"off_sr\"].max() + 0.01)\n",
    "ax.yaxis.set_major_locator(MultipleLocator(0.04))\n",
    "ax.yaxis.set_major_formatter(PercentFormatter(1, decimals=0))\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "sdvplot.axis_logos(ax, \"x\", league=\"nfl\", season=SEASON, height=0.06)\n",
    "ax.set_title(f\"Offensive success rate, {SEASON}\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, CAPTION, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 3. Division standings table\n",
    "\n",
    "Records come from the schedule (`load_nfl_schedule`), divisions from `load_nfl_teams`. great_tables draws the table;\n",
    "`gt_sdv_logos` turns the abbreviation column into logos, `data_color` shades the point differential and\n",
    "`gt_theme_sdv` gives it the SportsDataverse look."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "from great_tables import GT\n",
    "\n",
    "from sdvplot.great_tables import gt_sdv_logos, gt_theme_sdv\n",
    "\n",
    "schedule = nfl.load_nfl_schedule([SEASON]).filter(pl.col(\"game_type\") == \"REG\")\n",
    "games = pl.concat(\n",
    "    [\n",
    "        schedule.select(\"week\", team=\"home_team\", pf=\"home_score\", pa=\"away_score\"),\n",
    "        schedule.select(\"week\", team=\"away_team\", pf=\"away_score\", pa=\"home_score\"),\n",
    "    ]\n",
    ")\n",
    "divisions = nfl.load_nfl_teams().select(team=\"team_abbr\", division=\"team_division\")\n",
    "names = sdvplot.teams(\"nfl\").select(\"team_id\", \"name\")\n",
    "\n",
    "records = games.group_by(\"team\", maintain_order=True).agg(\n",
    "    W=(pl.col(\"pf\") > pl.col(\"pa\")).sum(),\n",
    "    L=(pl.col(\"pf\") < pl.col(\"pa\")).sum(),\n",
    "    T=(pl.col(\"pf\") == pl.col(\"pa\")).sum(),\n",
    "    PF=pl.col(\"pf\").sum(),\n",
    "    PA=pl.col(\"pa\").sum(),\n",
    ")\n",
    "# resolve maps the schedule's abbreviations to sdvplot team ids, which carry the full names\n",
    "standings = (\n",
    "    records.with_columns(\n",
    "        team_id=sdvplot.resolve(records[\"team\"], \"nfl\"),\n",
    "        Diff=pl.col(\"PF\") - pl.col(\"PA\"),\n",
    "        pct=(pl.col(\"W\") + pl.col(\"T\") / 2) / (pl.col(\"W\") + pl.col(\"L\") + pl.col(\"T\")),\n",
    "    )\n",
    "    .join(names, on=\"team_id\")\n",
    "    .join(divisions, on=\"team\")\n",
    "    .sort([\"division\", \"pct\", \"Diff\"], descending=[False, True, True])\n",
    "    .select(\"division\", logo=\"team\", Team=\"name\", W=\"W\", L=\"L\", T=\"T\", PF=\"PF\", PA=\"PA\", Diff=\"Diff\")\n",
    ")\n",
    "limit = standings[\"Diff\"].abs().max()  # a color scale centered on zero\n",
    "\n",
    "(\n",
    "    GT(standings, groupname_col=\"division\")\n",
    "    .pipe(gt_sdv_logos, \"logo\", league=\"nfl\", season=SEASON, height=22)\n",
    "    .cols_label(logo=\"\")\n",
    "    .data_color(columns=\"Diff\", palette=[\"#b2182b\", \"#f7f7f7\", \"#1b7837\"], domain=[-limit, limit])\n",
    "    .tab_header(title=f\"{SEASON} NFL standings\", subtitle=\"Regular season, by division\")\n",
    "    .tab_source_note(CAPTION)\n",
    "    .pipe(gt_theme_sdv, density=\"compact\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 4. Small multiples by division with plotnine\n",
    "\n",
    "Each team's running point differential through the season, one panel per division. `scale_color_sdv` colors the\n",
    "lines by team and `geom_sdv_logos` puts each logo just past the end of its line. The logo layer gets its own data (the last\n",
    "week per team) with the `division` column, so plotnine draws each logo in its own panel."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from plotnine import aes, facet_wrap, geom_hline, geom_line, ggplot, labs, scale_x_continuous, theme, theme_minimal\n",
    "\n",
    "from sdvplot.plotnine import geom_sdv_logos, scale_color_sdv\n",
    "\n",
    "running = (\n",
    "    games.sort(\"week\")\n",
    "    .with_columns(diff=(pl.col(\"pf\") - pl.col(\"pa\")).cum_sum().over(\"team\"))\n",
    "    .join(divisions, on=\"team\")\n",
    ")\n",
    "ends = running.group_by(\"team\", maintain_order=True).last().with_columns(week=pl.col(\"week\") + 1.5)\n",
    "\n",
    "(\n",
    "    ggplot(running.to_pandas(), aes(\"week\", \"diff\", color=\"team\"))\n",
    "    + geom_hline(yintercept=0, color=\"grey\", size=0.3)\n",
    "    + geom_line(size=0.8)\n",
    "    + geom_sdv_logos(\n",
    "        aes(\"week\", \"diff\", team=\"team\"),\n",
    "        data=ends.to_pandas(),\n",
    "        league=\"nfl\",\n",
    "        season=SEASON,\n",
    "        height=0.13,\n",
    "        inherit_aes=False,\n",
    "    )\n",
    "    + scale_color_sdv(\"nfl\")\n",
    "    + scale_x_continuous(breaks=[1, 6, 12, 18], limits=(1, 20))\n",
    "    + facet_wrap(\"division\", ncol=4)\n",
    "    + labs(x=\"Week\", y=\"Point differential\", title=f\"Running point differential by division, {SEASON}\", caption=CAPTION)\n",
    "    + theme_minimal()\n",
    "    + theme(figure_size=(10, 6), legend_position=\"none\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "## 5. A win probability chart with a logo in the title\n",
    "\n",
    "nflverse's `home_wp` traced through Super Bowl LX. `title_image` sets the title with the winner's logo beside it.\n",
    "The fills use the teams' colors from `team_colors`; when two primaries are the same, one team switches to its\n",
    "secondary color."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sdvplot.matplotlib import title_image\n",
    "\n",
    "game = nfl.load_nfl_schedule([SEASON]).filter(pl.col(\"game_type\") == \"SB\").row(0, named=True)\n",
    "wp = pbp.filter(pl.col(\"game_id\") == game[\"game_id\"], pl.col(\"home_wp\").is_not_null()).select(\n",
    "    minute=(3600 - pl.col(\"game_seconds_remaining\")) / 60, away_wp=1 - pl.col(\"home_wp\")\n",
    ")\n",
    "away, home = game[\"away_team\"], game[\"home_team\"]\n",
    "away_color, home_color = sdvplot.team_colors([away, home], \"nfl\")\n",
    "if away_color == home_color:  # both teams' primary is the same navy: use the away team's second color\n",
    "    away_color = sdvplot.team_colors(away, \"nfl\", which=\"secondary\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 5))\n",
    "ax.fill_between(\n",
    "    wp[\"minute\"], 0.5, wp[\"away_wp\"], where=wp[\"away_wp\"] >= 0.5, color=away_color, alpha=0.8, interpolate=True\n",
    ")\n",
    "ax.fill_between(\n",
    "    wp[\"minute\"], 0.5, wp[\"away_wp\"], where=wp[\"away_wp\"] < 0.5, color=home_color, alpha=0.8, interpolate=True\n",
    ")\n",
    "ax.plot(wp[\"minute\"], wp[\"away_wp\"], color=\"black\", lw=0.8)\n",
    "ax.set(xlim=(0, 60), ylim=(0, 1), xticks=[0, 15, 30, 45, 60], xlabel=\"Minutes played\", ylabel=f\"{away} win probability\")\n",
    "ax.axhline(0.5, color=\"grey\", lw=0.6)\n",
    "winner = away if game[\"away_score\"] > game[\"home_score\"] else home\n",
    "title_image(\n",
    "    ax,\n",
    "    winner,\n",
    "    f\"Super Bowl LX: {away} {game['away_score']}, {home} {game['home_score']}\",\n",
    "    league=\"nfl\",\n",
    "    season=SEASON,\n",
    "    height=28,\n",
    "    loc=\"left\",\n",
    "    fontweight=\"bold\",\n",
    ")\n",
    "fig.text(0.99, 0.01, \"Data: nflverse via sportsdataverse-py\", ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "## 6. A quarterback leaderboard with headshots\n",
    "\n",
    "nflverse identifies players by gsis id (`passer_player_id`). `add_headshots` takes those ids with\n",
    "`id_system=\"gsis\"` and looks up each player's headshot through the nflverse player table; the bars take each\n",
    "quarterback's team color."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Bar chart of the top 16 NFL quarterbacks by 2025 EPA per dropback, bars in team colors, headshots at the bar ends",
     "title": "NFL quarterback headshots"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "qbs = (\n",
    "    pbp.filter(pl.col(\"season_type\") == \"REG\", pl.col(\"passer_player_id\").is_not_null(), pl.col(\"epa\").is_not_null())\n",
    "    .group_by(\"passer_player_id\", maintain_order=True)\n",
    "    .agg(\n",
    "        name=pl.col(\"passer_player_name\").first(),\n",
    "        team=pl.col(\"posteam\").last(),\n",
    "        plays=pl.len(),\n",
    "        epa=pl.col(\"epa\").mean(),\n",
    "    )\n",
    "    .filter(pl.col(\"plays\") >= 300)\n",
    "    .sort(\"epa\", descending=True)\n",
    "    .head(16)\n",
    "    .reverse()  # barh draws from the bottom up, so the leader ends on top\n",
    ")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "ax.barh(qbs[\"name\"], qbs[\"epa\"], height=0.7, color=sdvplot.team_colors(qbs[\"team\"], \"nfl\").to_list())\n",
    "sdvplot.add_headshots(\n",
    "    ax,\n",
    "    qbs[\"epa\"] + 0.012,\n",
    "    list(range(qbs.height)),\n",
    "    qbs[\"passer_player_id\"],\n",
    "    league=\"nfl\",\n",
    "    id_system=\"gsis\",\n",
    "    height=0.06,\n",
    ")\n",
    "ax.set_xlim(0, qbs[\"epa\"].max() + 0.03)\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "ax.set_xlabel(\"EPA per dropback\")\n",
    "ax.set_title(f\"Top 16 quarterbacks by EPA per dropback, {SEASON} (300+ dropbacks)\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, CAPTION, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "## 7. Relocated franchises and their eras\n",
    "\n",
    "The schedules use the abbreviation a team had that season: OAK until 2019, SD until 2016, STL until 2015. `resolve`\n",
    "with one season per row maps every era to the same franchise id, and `add_logos` with one season per point draws the\n",
    "mark the team wore that year."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "history = nfl.load_nfl_schedule(list(range(2012, SEASON + 1))).filter(pl.col(\"game_type\") == \"REG\")\n",
    "wins = (\n",
    "    pl.concat(\n",
    "        [\n",
    "            history.select(\"season\", team=\"home_team\", win=pl.col(\"result\") > 0),\n",
    "            history.select(\"season\", team=\"away_team\", win=pl.col(\"result\") < 0),\n",
    "        ]\n",
    "    )\n",
    "    .filter(pl.col(\"team\").is_in([\"OAK\", \"LV\", \"SD\", \"LAC\", \"STL\", \"LA\"]))\n",
    "    .group_by(\"season\", \"team\", maintain_order=True)\n",
    "    .agg(wins=pl.col(\"win\").sum())\n",
    "    .sort(\"season\")\n",
    ")\n",
    "wins = wins.with_columns(team_id=sdvplot.resolve(wins[\"team\"], \"nfl\", season=wins[\"season\"]))\n",
    "wins.group_by(\"team_id\", \"team\", maintain_order=True).agg(\n",
    "    first=pl.col(\"season\").min(), last=pl.col(\"season\").max()\n",
    ").sort(\"team_id\", \"first\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16",
   "metadata": {},
   "outputs": [],
   "source": [
    "moves = {\"13\": (2020, \"Las Vegas\"), \"24\": (2017, \"Los Angeles\"), \"14\": (2016, \"Los Angeles\")}\n",
    "fig, axes = plt.subplots(3, 1, figsize=(10, 6), sharex=True, sharey=True)\n",
    "for ax, (team_id, (moved, city)) in zip(axes, moves.items(), strict=True):\n",
    "    rows = wins.filter(pl.col(\"team_id\") == team_id)\n",
    "    ax.plot(rows[\"season\"], rows[\"wins\"], color=sdvplot.team_colors(team_id, \"nfl\"), lw=1.5)\n",
    "    ax.axvline(moved - 0.5, color=\"grey\", lw=0.8, ls=\"--\")\n",
    "    ax.text(moved - 0.6, 15, f\"moves to {city}\", fontsize=8, color=\"grey\", va=\"top\", ha=\"right\")\n",
    "    sdvplot.add_logos(ax, rows[\"season\"], rows[\"wins\"], rows[\"team\"], league=\"nfl\", season=rows[\"season\"], height=0.32)\n",
    "    ax.set_ylim(-1, 16)\n",
    "    ax.set_ylabel(\"Wins\")\n",
    "    ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "axes[0].set_title(\"Three relocated franchises, each season in that season's logo\", loc=\"left\", fontweight=\"bold\")\n",
    "axes[-1].set_xticks(range(2012, SEASON + 1, 2))\n",
    "fig.text(0.99, 0.01, \"Data: nflverse via sportsdataverse-py | regular season\", ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17",
   "metadata": {},
   "source": [
    "## 8. An interactive Plotly scatter\n",
    "\n",
    "Dropback EPA against rushing EPA, with hover text. `add_logos` works on a Plotly figure the same way: a transparent\n",
    "marker trace carries the hover, and the logos are layout images. The axis ranges are set first, with some\n",
    "room at the edges, so no logo is cut off."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18",
   "metadata": {},
   "outputs": [],
   "source": [
    "import plotly.graph_objects as go\n",
    "\n",
    "split = plays.group_by(\"posteam\", maintain_order=True).agg(\n",
    "    pass_epa=pl.col(\"epa\").filter(pl.col(\"play_type\") == \"pass\").mean(),\n",
    "    rush_epa=pl.col(\"epa\").filter(pl.col(\"play_type\") == \"run\").mean(),\n",
    "    pass_rate=(pl.col(\"play_type\") == \"pass\").mean(),\n",
    ")\n",
    "\n",
    "fig = go.Figure(\n",
    "    go.Scatter(\n",
    "        x=split[\"rush_epa\"],\n",
    "        y=split[\"pass_epa\"],\n",
    "        mode=\"markers\",\n",
    "        marker={\"size\": 30, \"opacity\": 0},\n",
    "        customdata=split.select(\"posteam\", \"pass_rate\").rows(),\n",
    "        hovertemplate=\"%{customdata[0]}<br>pass EPA %{y:.3f}<br>rush EPA %{x:.3f}\"\n",
    "        \"<br>pass rate %{customdata[1]:.0%}<extra></extra>\",\n",
    "    )\n",
    ")\n",
    "\n",
    "\n",
    "def padded(values, share=0.08):  # an axis range with room for the logos at the edges\n",
    "    low, high = values.min(), values.max()\n",
    "    return [low - share * (high - low), high + share * (high - low)]\n",
    "\n",
    "\n",
    "fig.update_layout(\n",
    "    xaxis_range=padded(split[\"rush_epa\"]),\n",
    "    yaxis_range=padded(split[\"pass_epa\"]),\n",
    "    title=f\"Passing vs rushing EPA per play, {SEASON}\",\n",
    "    xaxis_title=\"Rushing EPA per play\",\n",
    "    yaxis_title=\"Dropback EPA per play\",\n",
    "    template=\"plotly_white\",\n",
    "    width=800,\n",
    "    height=560,\n",
    ")\n",
    "sdvplot.add_logos(fig, split[\"rush_epa\"], split[\"pass_epa\"], split[\"posteam\"], league=\"nfl\", season=SEASON, height=0.08)\n",
    "fig"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19",
   "metadata": {},
   "source": [
    "## 9. A field in team colors\n",
    "\n",
    "`surface(\"nfl\", team)` draws an NFL field with sportypy, end zones in the team's colors. On top: every Seattle\n",
    "touchdown from scrimmage in the regular season, from the line of scrimmage to the end zone, placed by the side of the\n",
    "field the play went to."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "tds = pbp.filter(\n",
    "    pl.col(\"season_type\") == \"REG\",\n",
    "    pl.col(\"posteam\") == \"SEA\",\n",
    "    pl.col(\"td_team\") == \"SEA\",\n",
    "    pl.col(\"play_type\").is_in([\"pass\", \"run\"]),\n",
    ").with_columns(side=pl.coalesce(\"pass_location\", \"run_location\"))\n",
    "lane = {\"left\": 15.0, \"middle\": 0.0, \"right\": -15.0}\n",
    "rng = np.random.default_rng(1)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 5.5))\n",
    "sdvplot.surface(\"nfl\", \"SEA\", season=SEASON, ax=ax, center_logo=0.18, display_range=\"in_bounds_only\")\n",
    "for row in tds.iter_rows(named=True):\n",
    "    y = lane.get(row[\"side\"], 0.0) + rng.uniform(-6, 6)\n",
    "    color = \"#69be28\" if row[\"play_type\"] == \"pass\" else \"#ffffff\"\n",
    "    ax.annotate(\n",
    "        \"\",\n",
    "        xy=(53, y),\n",
    "        xytext=(50 - row[\"yardline_100\"], y),\n",
    "        arrowprops={\"arrowstyle\": \"->\", \"color\": color, \"lw\": 1.4},\n",
    "        zorder=20,\n",
    "    )\n",
    "ax.set_title(\n",
    "    f\"Seattle's {tds.height} touchdowns from scrimmage, {SEASON}: green = pass, white = run\",\n",
    "    loc=\"left\",\n",
    "    fontweight=\"bold\",\n",
    ")\n",
    "fig.text(0.99, 0.01, CAPTION, ha=\"right\", fontsize=8, color=\"grey\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21",
   "metadata": {},
   "source": [
    "## 10. Team tiers\n",
    "\n",
    "A tier list from net EPA per play (offense minus defense), drawn by `team_tiers` on sdvplotR's Tiermaker theme.\n",
    "`tier_no` and `team` are the only columns it needs; the order within each tier comes from the data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Tier list of the 32 NFL teams by 2025 net EPA per play, five rows of team logos on a dark background",
     "title": "NFL team tiers"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "from sdvplot.matplotlib import team_tiers\n",
    "\n",
    "sizes = [5, 7, 7, 7, 6]  # teams per tier, top to bottom\n",
    "tier_of_rank = [tier for tier, n in enumerate(sizes, start=1) for _ in range(n)]\n",
    "tiers = (\n",
    "    teams.with_columns(net=pl.col(\"off_epa\") - pl.col(\"def_epa\"))\n",
    "    .sort(\"net\", descending=True)\n",
    "    .with_columns(tier_no=pl.Series(tier_of_rank))\n",
    "    .select(\"tier_no\", \"team\")\n",
    ")\n",
    "\n",
    "fig = team_tiers(\n",
    "    tiers,\n",
    "    \"nfl\",\n",
    "    title=f\"NFL tiers by net EPA per play, {SEASON}\",\n",
    "    subtitle=\"offense EPA/play minus defense EPA/play allowed\",\n",
    "    caption=CAPTION,\n",
    "    tier_desc={1: \"Contenders\", 2: \"Good\", 3: \"Middle\", 4: \"Flawed\", 5: \"Rebuilding\"},\n",
    ")\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "sdvplot": {
   "description": "Ten NFL charts and tables from nflverse play-by-play: logos, team colors, headshots, eras, a field.",
   "label": "NFL",
   "position": 10
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
