{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# MLB and MiLB\n",
    "\n",
    "Ten worked examples on the 2026 MLB regular season: standings and run differential from the MLB Stats API, Statcast\n",
    "leaderboards and batted balls from Baseball Savant, ESPN leaders with headshots, franchise eras, and one club's\n",
    "minor-league affiliates. Every dataset comes through [sportsdataverse-py](https://py.sportsdataverse.org/) and needs no\n",
    "API key."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import polars as pl\n",
    "import sportsdataverse.mlb as mlb\n",
    "\n",
    "import sdvplot\n",
    "\n",
    "SEASON = 2026  # the 2026 regular season is complete\n",
    "STATS_API = \"Data: MLB Stats API via sportsdataverse-py\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "The standings come from the Stats API with each team's division attached (`hydrate=\"division\"`); the teams endpoint\n",
    "adds the club abbreviation. Both sides key on the Stats API team id, so check the dtypes before the join."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3",
   "metadata": {},
   "outputs": [],
   "source": [
    "standings = mlb.parse_mlb_api_standings(mlb.mlb_standings(season=SEASON, hydrate=\"division\"))\n",
    "clubs = mlb.parse_mlb_api_teams(mlb.mlb_teams(season=SEASON)).select(pl.col(\"id\").alias(\"team_id\"), \"abbreviation\")\n",
    "assert standings.schema[\"team_id\"] == clubs.schema[\"team_id\"]\n",
    "\n",
    "standings = standings.join(clubs, on=\"team_id\").select(\n",
    "    \"team_id\",\n",
    "    \"abbreviation\",\n",
    "    \"team_name\",\n",
    "    division=\"standings_division_name\",\n",
    "    rank=pl.col(\"division_rank\").cast(pl.Int64),\n",
    "    w=\"wins\",\n",
    "    l=\"losses\",\n",
    "    pct=\"winning_percentage\",\n",
    "    gb=\"games_back\",\n",
    "    rs=\"runs_scored\",\n",
    "    ra=\"runs_allowed\",\n",
    "    diff=\"run_differential\",\n",
    "    strk=\"streak_streak_code\",\n",
    ")\n",
    "standings.sort(\"diff\", descending=True).head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4",
   "metadata": {},
   "source": [
    "## 1. Run differential in team colors\n",
    "\n",
    "`team_colors` takes the Stats API abbreviations as they come, and `axis_logos` swaps the x tick labels for logos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {},
   "outputs": [],
   "source": [
    "rd = standings.sort(\"diff\", descending=True)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 5.5))\n",
    "ax.bar(rd[\"abbreviation\"], rd[\"diff\"], color=sdvplot.team_colors(rd[\"abbreviation\"].to_list(), \"mlb\"))\n",
    "ax.axhline(0, color=\"#222222\", linewidth=0.8)\n",
    "ax.set_ylabel(\"Run differential\")\n",
    "ax.margins(x=0.01)\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "ax.set_title(f\"{SEASON} MLB run differential, regular season\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, STATS_API, ha=\"right\", fontsize=8, color=\"#666666\")\n",
    "sdvplot.axis_logos(ax, \"x\", league=\"mlb\", height=0.06)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 2. Division standings table\n",
    "\n",
    "A great_tables table grouped by division, with `gt_sdv_logos` turning the abbreviation column into logos and the\n",
    "Baseball Savant look from `gt_theme_savant`. The theme goes on first and the run-differential fill after it, so the\n",
    "theme's styling cannot replace the fill. The theme's row stripes are also switched off (extra keywords go to\n",
    "`tab_options`): in notebook output great_tables marks its CSS `!important`, so stripes would cover the fill on every\n",
    "other row."
   ]
  },
  {
   "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_savant\n",
    "\n",
    "table = standings.sort(\"division\", \"rank\").select(\n",
    "    \"division\", \"abbreviation\", \"team_name\", \"w\", \"l\", \"pct\", \"gb\", \"rs\", \"ra\", \"diff\", \"strk\"\n",
    ")\n",
    "gt = gt_theme_savant(\n",
    "    GT(table, groupname_col=\"division\")\n",
    "    .cols_label(\n",
    "        abbreviation=\"\",\n",
    "        team_name=\"Team\",\n",
    "        w=\"W\",\n",
    "        l=\"L\",\n",
    "        pct=\"Pct\",\n",
    "        gb=\"GB\",\n",
    "        rs=\"RS\",\n",
    "        ra=\"RA\",\n",
    "        diff=\"Diff\",\n",
    "        strk=\"Streak\",\n",
    "    )\n",
    "    .tab_header(title=f\"{SEASON} MLB standings\", subtitle=\"Final regular-season standings by division\")\n",
    "    .tab_source_note(STATS_API),\n",
    "    row_striping_include_table_body=False,\n",
    ")\n",
    "gt = gt_sdv_logos(gt, \"abbreviation\", league=\"mlb\", height=24)\n",
    "gt.data_color(columns=\"diff\", palette=[\"#c84630\", \"#ffffff\", \"#2a7ab9\"], domain=[-250, 250])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 3. Pythagorean wins: who beat their run differential\n",
    "\n",
    "Expected wins from runs scored and allowed (the 1.83 exponent) against actual wins. `add_logos` puts each logo at its\n",
    "point; set the axis limits first, since images do not move the autoscaling."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "pyth = standings.with_columns(\n",
    "    xw=(pl.col(\"rs\") ** 1.83 / (pl.col(\"rs\") ** 1.83 + pl.col(\"ra\") ** 1.83)) * (pl.col(\"w\") + pl.col(\"l\"))\n",
    ")\n",
    "lo, hi = 55, 108\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7, 6))\n",
    "ax.plot([lo, hi], [lo, hi], color=\"#999999\", linestyle=\"--\", linewidth=1)\n",
    "ax.set_xlim(lo, hi)\n",
    "ax.set_ylim(lo, hi)\n",
    "ax.set_xlabel(\"Expected wins (Pythagorean, exponent 1.83)\")\n",
    "ax.set_ylabel(\"Actual wins\")\n",
    "ax.text(lo + 2, hi - 3, \"Won more than\\ntheir runs suggest\", fontsize=9, color=\"#555555\", va=\"top\")\n",
    "ax.text(hi - 2, lo + 3, \"Won fewer\", fontsize=9, color=\"#555555\", ha=\"right\")\n",
    "ax.set_title(f\"{SEASON} MLB: actual vs expected wins\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, STATS_API, ha=\"right\", fontsize=8, color=\"#666666\")\n",
    "sdvplot.add_logos(ax, pyth[\"xw\"], pyth[\"w\"], pyth[\"abbreviation\"], league=\"mlb\", height=0.07)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "10",
   "metadata": {},
   "outputs": [],
   "source": [
    "pyth.select(\"abbreviation\", \"w\", xw=pl.col(\"xw\").round(1), luck=(pl.col(\"w\") - pl.col(\"xw\")).round(1)).sort(\n",
    "    \"luck\", descending=True\n",
    ").head(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11",
   "metadata": {},
   "source": [
    "## 4. Runs scored and allowed, one panel per division\n",
    "\n",
    "plotnine with `geom_sdv_logos`: the team aesthetic takes the abbreviation, and `facet_wrap` splits the league into its\n",
    "six divisions. The y axis is reversed so better run prevention sits higher, and the dashed lines mark the MLB average."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12",
   "metadata": {},
   "outputs": [],
   "source": [
    "from plotnine import (\n",
    "    aes,\n",
    "    facet_wrap,\n",
    "    geom_hline,\n",
    "    geom_vline,\n",
    "    ggplot,\n",
    "    labs,\n",
    "    scale_x_continuous,\n",
    "    scale_y_reverse,\n",
    "    theme,\n",
    "    theme_bw,\n",
    ")\n",
    "\n",
    "from sdvplot.plotnine import geom_sdv_logos\n",
    "\n",
    "per_game = standings.with_columns(\n",
    "    rs_g=pl.col(\"rs\") / (pl.col(\"w\") + pl.col(\"l\")), ra_g=pl.col(\"ra\") / (pl.col(\"w\") + pl.col(\"l\"))\n",
    ")\n",
    "(\n",
    "    ggplot(per_game.to_pandas(), aes(\"rs_g\", \"ra_g\", team=\"abbreviation\"))\n",
    "    + geom_vline(xintercept=per_game[\"rs_g\"].mean(), linetype=\"dashed\", color=\"#999999\")\n",
    "    + geom_hline(yintercept=per_game[\"ra_g\"].mean(), linetype=\"dashed\", color=\"#999999\")\n",
    "    + geom_sdv_logos(league=\"mlb\", height=0.15)\n",
    "    + scale_x_continuous(expand=(0.08, 0))\n",
    "    + scale_y_reverse(expand=(0.12, 0))\n",
    "    + facet_wrap(\"division\", ncol=3)\n",
    "    + labs(\n",
    "        x=\"Runs scored per game\",\n",
    "        y=\"Runs allowed per game (reversed)\",\n",
    "        title=f\"{SEASON} MLB run scoring and prevention by division\",\n",
    "        caption=STATS_API,\n",
    "    )\n",
    "    + theme_bw()\n",
    "    + theme(figure_size=(10, 6))\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13",
   "metadata": {},
   "source": [
    "## 5. Statcast: team wOBA against expected wOBA (interactive)\n",
    "\n",
    "Baseball Savant's expected-statistics leaderboard at the team level (`type=\"batter-team\"`). Its `team_id` column holds\n",
    "abbreviations, which `resolve` maps like any other. The Plotly adapter adds each logo as a layout image; hover a point\n",
    "for the numbers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "14",
   "metadata": {},
   "outputs": [],
   "source": [
    "import plotly.graph_objects as go\n",
    "\n",
    "xstats = mlb.mlb_statcast_leaderboard_expected_stats(type=\"batter-team\", year=SEASON)\n",
    "both = pl.concat([xstats[\"woba\"], xstats[\"est_woba\"]])\n",
    "lo, hi = both.min() - 0.005, both.max() + 0.005\n",
    "\n",
    "fig = go.Figure(\n",
    "    go.Scatter(\n",
    "        x=xstats[\"est_woba\"],\n",
    "        y=xstats[\"woba\"],\n",
    "        mode=\"markers\",\n",
    "        marker={\"opacity\": 0},\n",
    "        text=xstats[\"team\"],\n",
    "        hovertemplate=\"%{text}<br>xwOBA %{x:.3f}<br>wOBA %{y:.3f}<extra></extra>\",\n",
    "    )\n",
    ")\n",
    "fig.add_shape(type=\"line\", x0=lo, y0=lo, x1=hi, y1=hi, line={\"color\": \"#999999\", \"dash\": \"dash\"})\n",
    "fig = sdvplot.add_logos(fig, xstats[\"est_woba\"], xstats[\"woba\"], xstats[\"team_id\"], league=\"mlb\", height=0.07)\n",
    "fig.update_layout(\n",
    "    title=f\"{SEASON} team offense: wOBA vs expected wOBA<br><sup>Data: Baseball Savant via sportsdataverse-py</sup>\",\n",
    "    xaxis={\"title\": \"Expected wOBA (xwOBA)\", \"range\": [lo, hi]},\n",
    "    yaxis={\"title\": \"Actual wOBA\", \"range\": [lo, hi]},\n",
    "    width=760,\n",
    "    height=600,\n",
    "    template=\"plotly_white\",\n",
    ")\n",
    "fig"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15",
   "metadata": {},
   "source": [
    "Teams above the dashed line got more from their contact than its quality predicts.\n",
    "\n",
    "## 6. Home run leaders with headshots\n",
    "\n",
    "ESPN's leaders endpoint sorted by home runs (`season_type=2` is the regular season). ESPN athlete ids feed\n",
    "`add_headshots`, and ESPN team abbreviations feed `add_logos`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Horizontal bars of the ten 2026 MLB home run leaders, team logos at the base, headshots at the ends.",
     "title": "MLB home run leaders with headshots"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "raw = mlb.espn_mlb_leaders(season=SEASON, season_type=2, sort=\"batting.homeRuns:desc\", limit=10, return_parsed=False)\n",
    "labels = next(c[\"names\"] for c in raw[\"categories\"] if c[\"name\"] == \"batting\")\n",
    "rows = []\n",
    "for a in raw[\"athletes\"]:\n",
    "    batting = next(c for c in a[\"categories\"] if c[\"name\"] == \"batting\")\n",
    "    stats = dict(zip(labels, batting[\"values\"], strict=True))\n",
    "    rows.append(\n",
    "        {\n",
    "            \"espn_id\": a[\"athlete\"][\"id\"],\n",
    "            \"player\": a[\"athlete\"][\"displayName\"],\n",
    "            \"team\": a[\"athlete\"][\"teamShortName\"],\n",
    "            \"hr\": int(stats[\"homeRuns\"]),\n",
    "        }\n",
    "    )\n",
    "leaders = pl.DataFrame(rows).sort(\"hr\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "y = range(leaders.height)\n",
    "ax.barh(list(y), leaders[\"hr\"], color=sdvplot.team_colors(leaders[\"team\"].to_list(), \"mlb\"), height=0.7)\n",
    "ax.set_yticks(list(y), leaders[\"player\"].to_list())\n",
    "for i, hr in enumerate(leaders[\"hr\"]):\n",
    "    ax.text(hr - 1, i, str(hr), ha=\"right\", va=\"center\", color=\"white\", fontweight=\"bold\")\n",
    "ax.set_xlim(-5, leaders[\"hr\"].max() + 6)\n",
    "ax.set_xlabel(\"Home runs\")\n",
    "ax.spines[[\"top\", \"right\", \"left\"]].set_visible(False)\n",
    "ax.tick_params(axis=\"y\", length=0)\n",
    "ax.set_title(f\"{SEASON} MLB home run leaders\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, \"Data: ESPN via sportsdataverse-py\", ha=\"right\", fontsize=8, color=\"#666666\")\n",
    "sdvplot.add_logos(ax, [-2.5] * leaders.height, list(y), leaders[\"team\"], league=\"mlb\", height=0.06)\n",
    "sdvplot.add_headshots(ax, (leaders[\"hr\"] + 3).to_list(), list(y), leaders[\"espn_id\"], league=\"mlb\", height=0.1)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17",
   "metadata": {},
   "source": [
    "## 7. A home run spray chart on the field\n",
    "\n",
    "The Stats API's leaders endpoint gives the top of the home run list, and this takes its first entry; the `person.id` is\n",
    "the MLBAM id Statcast uses. Baseball Savant's search returns every one of his home runs with hit coordinates.\n",
    "`surface(\"mlb\")` draws the field, and the usual transform from baseballr's `mlbam_xy_transformation()` puts the\n",
    "coordinates in feet from home plate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Every 2026 regular-season home run by MLB's home run leader plotted on a baseball field, colored by distance, with his team's logo.",
     "title": "Home run spray chart on a baseball field"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "leader = mlb.mlb_stats_leaders(\"homeRuns\", season=SEASON, limit=1)[\"leagueLeaders\"][0][\"leaders\"][0]\n",
    "name, mlbam_id, club_id = leader[\"person\"][\"fullName\"], leader[\"person\"][\"id\"], leader[\"team\"][\"id\"]\n",
    "\n",
    "homers = (\n",
    "    mlb.mlb_statcast_search(\n",
    "        f\"{SEASON}-03-01\", f\"{SEASON}-10-01\", chunk_days=240, batters_lookup=mlbam_id, at_bat_result=\"home_run\"\n",
    "    )\n",
    "    .filter(pl.col(\"game_type\") == \"R\")\n",
    "    .with_columns(x=2.5 * (pl.col(\"hc_x\") - 125.42), y=2.5 * (198.27 - pl.col(\"hc_y\")))\n",
    ")\n",
    "\n",
    "ax = sdvplot.surface(\"mlb\")  # sportypy paints the whole figure as the field\n",
    "fig = ax.figure\n",
    "fig.set_size_inches(8, 6)\n",
    "ax.set_xlim(-330, 330)\n",
    "ax.set_ylim(-30, 480)\n",
    "dots = ax.scatter(\n",
    "    homers[\"x\"], homers[\"y\"], c=homers[\"hit_distance_sc\"], cmap=\"YlOrRd\", s=60, edgecolor=\"#222222\", zorder=30\n",
    ")\n",
    "bar = fig.colorbar(dots, ax=ax, shrink=0.6)\n",
    "bar.set_label(\"Distance (ft)\", color=\"white\")\n",
    "bar.ax.tick_params(colors=\"white\")\n",
    "ax.set_title(f\"{name}: {homers.height} home runs in {SEASON}\", loc=\"left\", fontweight=\"bold\", color=\"white\")\n",
    "fig.text(\n",
    "    0.98, 0.02, \"Data: Baseball Savant and MLB Stats API via sportsdataverse-py\", ha=\"right\", fontsize=8, color=\"white\"\n",
    ")\n",
    "sdvplot.add_logos(ax, [-265], [415], [club_id], league=\"mlb\", height=0.16)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19",
   "metadata": {},
   "source": [
    "The logo above takes the Stats API team id (`club_id`) directly: under `\"auto\"`, `resolve` tries the `mlbstats` id\n",
    "system after ESPN's.\n",
    "\n",
    "## 8. Franchise eras: renames and relocations\n",
    "\n",
    "The Stats API's team history records each franchise's name changes by season. The abbreviations change with the eras\n",
    "(PHA, KCA, OAK, ATH for the Athletics), and `resolve` reads each one in its seasons: KCA is the Athletics through 1967\n",
    "and the Royals from 1968."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20",
   "metadata": {},
   "outputs": [],
   "source": [
    "sdvplot.resolve([\"PHA\", \"KCA\", \"KCA\", \"OAK\", \"ATH\"], \"mlb\", season=[1950, 1960, 1990, 2000, 2025])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21",
   "metadata": {},
   "source": [
    "The Stats API team id never changes, so the chart resolves each franchise by its id and labels the row with its\n",
    "current abbreviation. Era boundaries are the seasons the Stats API gives."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22",
   "metadata": {
    "sdvplot_gallery": {
     "alt": "Timeline from 1950 of six MLB franchises' names and moves, one row per franchise with its current logo.",
     "title": "MLB franchise name eras"
    },
    "tags": [
     "gallery"
    ]
   },
   "outputs": [],
   "source": [
    "history = (\n",
    "    mlb.mlb_teams_history(team_ids=\"114,133,120,146,139,108\")  # CLE, ATH, WSH, MIA, TB, LAA today\n",
    "    .select(\"id\", \"season\", \"name\")\n",
    "    .sort(\"id\", \"season\")\n",
    ")\n",
    "# One row per name: an era runs from its first season to the season before the next name.\n",
    "eras = history.filter(pl.col(\"name\").ne_missing(pl.col(\"name\").shift().over(\"id\"))).with_columns(\n",
    "    end=(pl.col(\"season\").shift(-1).over(\"id\") - 1).fill_null(SEASON)\n",
    ")\n",
    "ids = (\n",
    "    eras.group_by(\"id\", maintain_order=True)\n",
    "    .agg(pl.col(\"season\").min())\n",
    "    .sort([\"season\", \"id\"], descending=[True, False])[\"id\"]\n",
    ")\n",
    "rows = pl.DataFrame({\"id\": ids, \"team_id\": sdvplot.resolve(ids.to_list(), \"mlb\")}).join(\n",
    "    sdvplot.teams(\"mlb\").select(\"team_id\", \"abbr\"), on=\"team_id\", how=\"left\", maintain_order=\"left\"\n",
    ")\n",
    "\n",
    "START = 1950\n",
    "fig, ax = plt.subplots(figsize=(10, 5))\n",
    "for k, (franchise, abbr) in enumerate(rows.select(\"id\", \"abbr\").iter_rows()):\n",
    "    color = sdvplot.team_colors(abbr, \"mlb\")\n",
    "    spans = eras.filter(pl.col(\"id\") == franchise).select(\"season\", \"end\", \"name\").rows()\n",
    "    for j, (start, end, name) in enumerate(spans):\n",
    "        lo, hi = max(start, START), end + 1\n",
    "        if hi <= lo:\n",
    "            continue\n",
    "        dark = j % 2 == 1\n",
    "        ax.barh(k, hi - lo, left=lo, height=0.72, color=color, alpha=0.9 if dark else 0.4, edgecolor=\"white\")\n",
    "        if j == len(spans) - 1:  # the current name goes to the right of the bar\n",
    "            ax.text(SEASON + 2, k, name, va=\"center\", fontsize=9, fontweight=\"bold\", clip_on=False)\n",
    "        elif hi - lo >= 0.6 * len(name):  # earlier names go inside their era when they fit\n",
    "            ax.text((lo + hi) / 2, k, name, ha=\"center\", va=\"center\", fontsize=7.5, color=\"white\" if dark else \"black\")\n",
    "ax.set_yticks(range(rows.height), rows[\"abbr\"].to_list())\n",
    "ax.set_xlim(START, SEASON + 1)\n",
    "ax.spines[[\"top\", \"right\", \"left\"]].set_visible(False)\n",
    "ax.tick_params(axis=\"y\", length=0)\n",
    "fig.subplots_adjust(right=0.8)\n",
    "ax.set_title(f\"Six MLB franchises' names since {START}\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, STATS_API, ha=\"right\", fontsize=8, color=\"#666666\")\n",
    "sdvplot.axis_logos(ax, \"y\", league=\"mlb\", height=0.1)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "23",
   "metadata": {},
   "source": [
    "## 9. One organization's minor-league affiliates\n",
    "\n",
    "`mlb_team_affiliates` lists a club's farm system. The affiliates carry Stats API ids, which the `milb` league\n",
    "(286 teams) resolves directly. MiLB teams have no official colors in the index: `color_source` is `\"fallback\"`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24",
   "metadata": {},
   "outputs": [],
   "source": [
    "ORG = \"PHI\"\n",
    "org_id = clubs.filter(pl.col(\"abbreviation\") == ORG)[\"team_id\"].item()\n",
    "levels = [\"Triple-A\", \"Double-A\", \"High-A\", \"Single-A\", \"Rookie\"]\n",
    "farm = (\n",
    "    mlb.mlb_team_affiliates(team_ids=org_id, season=SEASON)\n",
    "    .filter(pl.col(\"sport_name\").is_in(levels))\n",
    "    .select(pl.col(\"id\").cast(pl.Utf8), \"name\", level=\"sport_name\", circuit=\"league_name\")\n",
    "    .sort(pl.col(\"level\").replace_strict(levels, list(range(len(levels)))), \"name\")\n",
    ")\n",
    "farm.join(sdvplot.teams(\"milb\").select(pl.col(\"team_id\").alias(\"id\"), \"program\", \"color_source\"), on=\"id\", how=\"left\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25",
   "metadata": {},
   "source": [
    "The layout below is a plain matplotlib axes with logos placed by `add_logos`: the parent club from the `mlb` league,\n",
    "the affiliates from `milb`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26",
   "metadata": {},
   "outputs": [],
   "source": [
    "import textwrap\n",
    "\n",
    "n = farm.height\n",
    "fig, ax = plt.subplots(figsize=(10, 4.5))\n",
    "ax.set_xlim(-0.5, n - 0.5)\n",
    "ax.set_ylim(0, 1)\n",
    "ax.axis(\"off\")\n",
    "ax.plot([0, n - 1], [0.62, 0.62], color=\"#bbbbbb\", linewidth=1)\n",
    "ax.plot([(n - 1) / 2] * 2, [0.62, 0.69], color=\"#bbbbbb\", linewidth=1)\n",
    "for x, (name, level, circuit) in enumerate(farm.select(\"name\", \"level\", \"circuit\").iter_rows()):\n",
    "    ax.plot([x, x], [0.56, 0.62], color=\"#bbbbbb\", linewidth=1)\n",
    "    ax.text(x, 0.52, level, ha=\"center\", va=\"center\", fontsize=9, fontweight=\"bold\")\n",
    "    ax.text(x, 0.21, textwrap.fill(name, 18), ha=\"center\", va=\"top\", fontsize=8)\n",
    "    ax.text(x, 0.07, textwrap.fill(circuit, 16), ha=\"center\", va=\"top\", fontsize=7, color=\"#666666\")\n",
    "org_name = sdvplot.teams(\"mlb\").filter(pl.col(\"abbr\") == ORG)[\"name\"].item()\n",
    "ax.set_title(f\"{org_name} affiliates, {SEASON}\", loc=\"left\", fontweight=\"bold\")\n",
    "fig.text(0.99, 0.01, STATS_API, ha=\"right\", fontsize=8, color=\"#666666\")\n",
    "sdvplot.add_logos(ax, [(n - 1) / 2], [0.84], [ORG], league=\"mlb\", height=0.24)\n",
    "sdvplot.add_logos(ax, list(range(n)), [0.34] * n, farm[\"id\"], league=\"milb\", height=0.2)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27",
   "metadata": {},
   "source": [
    "## 10. A tier list by wins\n",
    "\n",
    "`team_tiers` from the plotnine adapter draws a tier list. Here the tiers are the five groups of six teams by wins, and\n",
    "each tier's label is its win range."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "28",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sdvplot.plotnine import team_tiers\n",
    "\n",
    "ranked = (\n",
    "    standings.sort(\"w\", descending=True)\n",
    "    .with_row_index(\"i\")\n",
    "    .with_columns(tier_no=pl.col(\"i\") // 6 + 1, tier_rank=pl.col(\"i\") % 6 + 1)\n",
    ")\n",
    "ranges = ranked.group_by(\"tier_no\", maintain_order=True).agg(lo=pl.col(\"w\").min(), hi=pl.col(\"w\").max()).sort(\"tier_no\")\n",
    "team_tiers(\n",
    "    ranked.select(\"tier_no\", \"tier_rank\", team=\"abbreviation\"),\n",
    "    \"mlb\",\n",
    "    title=f\"{SEASON} MLB tiers by wins\",\n",
    "    subtitle=\"Five tiers of six teams, by regular-season wins\",\n",
    "    caption=STATS_API,\n",
    "    tier_desc={t: f\"{lo}-{hi} wins\" for t, lo, hi in ranges.iter_rows()},\n",
    "    alpha=1,\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "sdvplot": {
   "description": "MLB and MiLB: run differential, division standings, Statcast, headshots, a spray chart on the field, franchise eras and a farm system.",
   "label": "MLB",
   "position": 30
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
