{ "cells": [ { "cell_type": "markdown", "id": "ec04e780", "metadata": {}, "source": [ "# โšพ Baseball with `sportsdataverse-py`\n", "\n", "Welcome to the ballpark! ๐ŸŸ๏ธ In just a few lines of Python you're about to\n", "pull **official MLB data** โ€” schedules, standings, rosters, box scores,\n", "play-by-play โ€” straight from the league's own **MLB Stats API**, plus\n", "**pitch-level Statcast** tracking from [Baseball Savant](https://baseballsavant.mlb.com/).\n", "Every premium call hands you back a tidy **polars** DataFrame (or raw JSON\n", "when you want it), ready to model. ๐Ÿš€\n", "\n", "If you've used the R package [baseballr](https://billpetti.github.io/baseballr/),\n", "or Python's [pybaseball](https://github.com/jldbc/pybaseball), the data shapes\n", "will feel right at home. Let's play ball! โšพ" ] }, { "cell_type": "markdown", "id": "df3ab13b", "metadata": {}, "source": [ "## ๐Ÿงฐ The toolbox\n", "\n", "We **lead with the premium sources** โ€” the **MLB Stats API** (`mlb_*`,\n", "backed by `statsapi.mlb.com`) and the **comprehensive Statcast** surface\n", "(`mlb_statcast_*`, from Baseball\n", "Savant). ESPN (`espn_mlb_*`) is a handy secondary path. Click any name for the\n", "full reference:\n", "\n", "| Function | What it gives you | Source |\n", "|---|---|---|\n", "| [`mlb_schedule`](../mlb/reference/additional.md#mlb_schedule) ยท [`parse_mlb_api_schedule`](../mlb/reference/additional.md#mlb_schedule) | Games for a date / range โ€” one row per game (with `game_pk`) | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_teams`](../mlb/reference/additional.md#mlb_teams) ยท [`parse_mlb_api_teams`](../mlb/reference/additional.md#mlb_teams) | Every club โ€” one row per team | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_standings`](../mlb/reference/additional.md#mlb_standings) ยท [`parse_mlb_api_standings`](../mlb/reference/additional.md#mlb_standings) | Division standings โ€” wins, losses, run diff | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_team_roster`](../mlb/reference/mlb_api.md#mlb_team_roster) | A team's roster โ€” one row per player | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_person`](../mlb/reference/mlb_api.md#mlb_person) | A player's bio (one tidy row) | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_person_stats`](../mlb/reference/additional.md#mlb_person_stats) ยท [`parse_mlb_api_person_stats`](../mlb/reference/additional.md#mlb_person_stats) | A player's season stat splits | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_boxscore`](../mlb/reference/mlb_api.md#mlb_boxscore) | Full game box score | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_play_by_play`](../mlb/reference/mlb_api.md#mlb_play_by_play) | Plate-appearance-level play-by-play | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_stats_leaders`](../mlb/reference/additional.md#mlb_stats_leaders) | League leaders for any stat (HR, AVG, ERA, โ€ฆ) | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_win_probability`](../mlb/reference/mlb_api.md#mlb_win_probability) | Per-play win probability + WPA for a game | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_awards`](../mlb/reference/mlb_api.md#mlb_awards) ยท [`mlb_award_recipients`](../mlb/reference/mlb_api.md#mlb_award_recipients) | Award catalog + season winners (MVP, Cy Young, โ€ฆ) | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_draft`](../mlb/reference/mlb_api.md#mlb_draft) | Amateur draft board โ€” one row per pick | ๐ŸŸข **MLB Stats API** |\n", "| [`mlb_statcast_search`](../mlb/reference/additional.md#mlb_statcast_search) | Every pitch matching a filter โ€” ~110 cols/pitch; auto date-chunks past the 25k cap; friendly filters (`batters_lookup`, `pitch_type`, `at_bat_result`, โ€ฆ) | ๐Ÿ”ต **Statcast** |\n", "| [`mlb_statcast_search_minors`](../mlb/reference/additional.md#mlb_statcast_search_minors) ยท [`mlb_statcast_search_wbc`](../mlb/reference/additional.md#mlb_statcast_search_wbc) | Same pitch search for MiLB and the World Baseball Classic | ๐Ÿ”ต **Statcast** |\n", "| `mlb_statcast_leaderboard_*` (37 of them) โ€” e.g. [`โ€ฆ_sprint_speed`](../mlb/reference/mlb_statcast.md#mlb_statcast_leaderboard_sprint_speed), [`โ€ฆ_expected_stats`](../mlb/reference/mlb_statcast.md#mlb_statcast_leaderboard_expected_stats), [`โ€ฆ_bat_tracking`](../mlb/reference/mlb_statcast.md#mlb_statcast_leaderboard_bat_tracking), [`โ€ฆ_outs_above_average`](../mlb/reference/mlb_statcast.md#mlb_statcast_leaderboard_outs_above_average) | Every Savant leaderboard: expected stats, sprint speed, bat tracking, pitch arsenals/movement/tempo, OAA, arm strength, catcher framing/blocking/throwing, baserunning, park factors, โ€ฆ | ๐Ÿ”ต **Statcast** |\n", "| [`mlb_statcast_gamefeed`](../mlb/reference/mlb_statcast.md#mlb_statcast_gamefeed) | Savant single-game feed โ€” one tidy row per pitch | ๐Ÿ”ต **Statcast** |\n", "| [`mlb_statcast_player`](../mlb/reference/additional.md#mlb_statcast_player) | A player's Savant page metrics | ๐Ÿ”ต **Statcast** |\n", "| [`espn_mlb_teams`](../mlb/reference/additional.md#espn_mlb_teams) ยท [`espn_mlb_schedule`](../mlb/reference/additional.md#espn_mlb_schedule) | ESPN teams / schedule (wide frames) | โšช ESPN |\n", "| [`most_recent_mlb_season`](../mlb/reference/additional.md#most_recent_mlb_season) | Current season helper | โšช helper |" ] }, { "cell_type": "markdown", "id": "6293de37", "metadata": {}, "source": [ "## ๐Ÿ”Œ Setup\n", "\n", "```sh\n", "pip install sportsdataverse\n", "```\n", "\n", "**No API key needed** for any of the premium MLB endpoints โ€” the MLB Stats API\n", "and Baseball Savant are both public. ๐ŸŽ‰" ] }, { "cell_type": "code", "execution_count": null, "id": "8598479f", "metadata": {}, "outputs": [], "source": [ "import polars as pl\n", "import sportsdataverse.mlb as mlb\n", "\n", "pl.Config.set_tbl_rows(12)\n", "print(\"most recent MLB season:\", mlb.most_recent_mlb_season())" ] }, { "cell_type": "markdown", "id": "1b910a15", "metadata": {}, "source": [ "The MLB Stats API and Savant are public and reliable, but they're still\n", "**live network calls** โ€” a date with no games, an offseason day, or a blip can\n", "make a call come back empty. So we use a tiny `safe()` helper: you get the\n", "frame when the feed is up, and a friendly one-liner when it isn't (never a\n", "scary traceback). ๐Ÿ›Ÿ\n", "\n", "We also pick a stable **completed-season** date for our examples so the page\n", "renders the same in June as in October." ] }, { "cell_type": "code", "execution_count": null, "id": "8cd9a6b5", "metadata": {}, "outputs": [], "source": [ "from sportsdataverse.errors import AssetFetchError, NoDataError\n", "\n", "def safe(label, thunk):\n", " \"\"\"Run a live call defensively: return its result, or print a one-liner.\"\"\"\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", "# A known completed regular-season slate โ€” stable for the docs build.\n", "SAMPLE_SEASON = 2024\n", "SAMPLE_DATE = \"2024-07-01\" # YYYY-MM-DD for the Stats API\n", "JUDGE_ID = 592450 # Aaron Judge, NYY โ€” our running example player\n", "YANKEES_ID = 147 # New York Yankees team_id" ] }, { "cell_type": "markdown", "id": "121814fa", "metadata": {}, "source": [ "## ๐Ÿ“… The schedule (MLB Stats API)\n", "\n", "[`mlb_schedule`](../mlb/reference/additional.md#mlb_schedule) returns the\n", "raw JSON `dict`; its partner\n", "[`parse_mlb_api_schedule`](../mlb/reference/additional.md#mlb_schedule)\n", "flattens it to **one row per game**. The most important column is `game_pk` โ€”\n", "that's the id you feed to the box score and play-by-play endpoints. Pass a\n", "single `date=`, or a `start_date`/`end_date` range, `team_id`, or `season`." ] }, { "cell_type": "code", "execution_count": null, "id": "af00c2e2", "metadata": {}, "outputs": [], "source": [ "schedule = safe(\n", " \"schedule\",\n", " lambda: mlb.parse_mlb_api_schedule(mlb.mlb_schedule(date=SAMPLE_DATE)),\n", ")\n", "cols = [\"game_pk\", \"status_detailed_state\",\n", " \"teams_away_team_name\", \"teams_away_score\",\n", " \"teams_home_team_name\", \"teams_home_score\"]\n", "(schedule.select([c for c in cols if c in schedule.columns]).head()\n", " if schedule is not None else \"schedule unavailable right now\")" ] }, { "cell_type": "markdown", "id": "908a65b3", "metadata": {}, "source": [ "## ๐Ÿ† Standings (MLB Stats API)\n", "\n", "[`mlb_standings`](../mlb/reference/additional.md#mlb_standings) covers\n", "both leagues by default (`league_id=\"103,104\"`).\n", "[`parse_mlb_api_standings`](../mlb/reference/additional.md#mlb_standings)\n", "returns one row per team with wins/losses, division rank, and winning\n", "percentage." ] }, { "cell_type": "code", "execution_count": null, "id": "24e8ec69", "metadata": {}, "outputs": [], "source": [ "standings = safe(\n", " \"standings\",\n", " lambda: mlb.parse_mlb_api_standings(mlb.mlb_standings(season=SAMPLE_SEASON)),\n", ")\n", "keep = [\"team_name\", \"standings_division_name\", \"wins\", \"losses\",\n", " \"winning_percentage\", \"division_rank\"]\n", "(standings.select([c for c in keep if c in standings.columns])\n", " .sort(\"wins\", descending=True).head(10)\n", " if standings is not None else \"standings unavailable right now\")" ] }, { "cell_type": "markdown", "id": "2e49f63d", "metadata": {}, "source": [ "## ๐Ÿงข Teams & rosters (MLB Stats API)\n", "\n", "[`mlb_teams`](../mlb/reference/additional.md#mlb_teams) +\n", "[`parse_mlb_api_teams`](../mlb/reference/additional.md#mlb_teams) lists every\n", "club โ€” grab a `team_id` here.\n", "[`mlb_team_roster`](../mlb/reference/mlb_api.md#mlb_team_roster) then\n", "returns a tidy frame directly (one row per player)." ] }, { "cell_type": "code", "execution_count": null, "id": "98a4af35", "metadata": {}, "outputs": [], "source": [ "teams = safe(\n", " \"teams\",\n", " lambda: mlb.parse_mlb_api_teams(mlb.mlb_teams(season=SAMPLE_SEASON)),\n", ")\n", "(teams.select([\"id\", \"name\", \"abbreviation\", \"location_name\", \"team_name\"]).head()\n", " if teams is not None else \"teams unavailable right now\")" ] }, { "cell_type": "code", "execution_count": null, "id": "d1ab6002", "metadata": {}, "outputs": [], "source": [ "roster = safe(\n", " \"Yankees roster\",\n", " lambda: mlb.mlb_team_roster(team_id=YANKEES_ID, season=SAMPLE_SEASON),\n", ")\n", "rcols = [\"jersey_number\", \"person_id\", \"person_full_name\",\n", " \"position_abbreviation\", \"status_description\"]\n", "(roster.select([c for c in rcols if c in roster.columns]).head()\n", " if roster is not None else \"roster unavailable right now\")" ] }, { "cell_type": "markdown", "id": "b19238c5", "metadata": {}, "source": [ "## ๐Ÿง Player bio & season stats (MLB Stats API)\n", "\n", "[`mlb_person`](../mlb/reference/mlb_api.md#mlb_person) returns a one-row\n", "bio frame. [`mlb_person_stats`](../mlb/reference/additional.md#mlb_person_stats)\n", "returns the raw stat-split `dict`;\n", "[`parse_mlb_api_person_stats`](../mlb/reference/additional.md#mlb_person_stats)\n", "flattens it. Our running example is Aaron Judge (`person_id=592450`)." ] }, { "cell_type": "code", "execution_count": null, "id": "b6fe0e96", "metadata": {}, "outputs": [], "source": [ "bio = safe(\"Judge bio\", lambda: mlb.mlb_person(person_id=JUDGE_ID))\n", "bcols = [\"id\", \"full_name\", \"primary_number\", \"birth_date\",\n", " \"height\", \"weight\", \"mlb_debut_date\"]\n", "(bio.select([c for c in bcols if c in bio.columns])\n", " if bio is not None else \"bio unavailable right now\")" ] }, { "cell_type": "code", "execution_count": null, "id": "8b6e5ebd", "metadata": {}, "outputs": [], "source": [ "hitting = safe(\n", " \"Judge 2024 hitting\",\n", " lambda: mlb.parse_mlb_api_person_stats(\n", " mlb.mlb_person_stats(person_id=JUDGE_ID, stats=\"season\",\n", " group=\"hitting\", season=SAMPLE_SEASON)\n", " ),\n", ")\n", "scols = [\"season\", \"stat_games_played\", \"stat_home_runs\", \"stat_rbi\",\n", " \"stat_avg\", \"stat_obp\", \"stat_slg\", \"stat_ops\"]\n", "(hitting.select([c for c in scols if c in hitting.columns])\n", " if hitting is not None else \"stats unavailable right now\")" ] }, { "cell_type": "markdown", "id": "1234ae12", "metadata": {}, "source": [ "## ๐ŸŽฏ Pitch-level Statcast (Baseball Savant)\n", "\n", "Now the fun part โ€” **every single pitch**.\n", "[`mlb_statcast_search`](../mlb/reference/additional.md#mlb_statcast_search) pulls each\n", "pitch matching your filter, with 100+ columns (velocity, spin, launch angle,\n", "expected stats). Keep windows **small** (one player, one game, or a 1โ€“2 day\n", "slice) โ€” a full season is millions of pitches. Here's every pitch Aaron Judge\n", "saw over a two-day window." ] }, { "cell_type": "code", "execution_count": null, "id": "e606e9e4", "metadata": {}, "outputs": [], "source": [ "pitches = safe(\n", " \"Judge pitches (2-day)\",\n", " lambda: mlb.mlb_statcast_search(start_dt=\"2024-07-01\", end_dt=\"2024-07-02\",\n", " batters_lookup=JUDGE_ID),\n", ")\n", "if pitches is not None and pitches.height:\n", " print(\"shape:\", pitches.shape)\n", " out = pitches.select([\"game_date\", \"player_name\", \"pitch_type\", \"release_speed\",\n", " \"launch_speed\", \"launch_angle\", \"events\", \"description\"]).head()\n", "else:\n", " out = \"no pitches in that window right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "6dd34fe6", "metadata": {}, "source": [ "## ๐Ÿณ Cookbook: common baseball tasks\n", "\n", "A handful of recipes you'll reach for constantly โ€” every one leads with a\n", "**premium** source." ] }, { "cell_type": "markdown", "id": "4fb350cc", "metadata": {}, "source": [ "### Recipe 1 โ€” A team's schedule + where they sit in the standings ๐Ÿ“‹\n", "\n", "Pull one club's slate with `mlb_schedule(team_id=...)`, then find their row\n", "in the standings. Two premium calls, one tidy snapshot." ] }, { "cell_type": "code", "execution_count": null, "id": "78c30abe", "metadata": {}, "outputs": [], "source": [ "yanks_sched = safe(\n", " \"Yankees July schedule\",\n", " lambda: mlb.parse_mlb_api_schedule(\n", " mlb.mlb_schedule(team_id=YANKEES_ID,\n", " start_date=\"2024-07-01\", end_date=\"2024-07-07\")\n", " ),\n", ")\n", "sched_cols = [\"game_pk\", \"official_date\", \"teams_away_team_name\",\n", " \"teams_home_team_name\", \"teams_away_score\", \"teams_home_score\"]\n", "if yanks_sched is not None and yanks_sched.height:\n", " games = yanks_sched.select([c for c in sched_cols if c in yanks_sched.columns])\n", "else:\n", " games = \"schedule unavailable right now\"\n", "\n", "if standings is not None and \"team_name\" in standings.columns:\n", " rank = (standings.filter(pl.col(\"team_name\").str.contains(\"Yankees\"))\n", " .select([c for c in [\"team_name\", \"wins\", \"losses\", \"division_rank\"]\n", " if c in standings.columns]))\n", "else:\n", " rank = \"standings unavailable\"\n", "print(rank)\n", "games" ] }, { "cell_type": "markdown", "id": "ffef7d35", "metadata": {}, "source": [ "### Recipe 2 โ€” A Statcast leaderboard ๐Ÿƒ\n", "\n", "The `mlb_statcast_leaderboard_*` family wraps Savant's *pre-aggregated* season\n", "leaderboards โ€” fast, because the heavy lifting happens server-side. Here's the\n", "2024 **sprint speed** leaderboard, fastest first." ] }, { "cell_type": "code", "execution_count": null, "id": "b5c93d5e", "metadata": {}, "outputs": [], "source": [ "sprint = safe(\n", " \"sprint speed leaderboard\",\n", " lambda: mlb.mlb_statcast_leaderboard_sprint_speed(year=SAMPLE_SEASON, min_opp=10),\n", ")\n", "spcols = [\"last_name, first_name\", \"team\", \"position\", \"competitive_runs\", \"sprint_speed\"]\n", "(sprint.select([c for c in spcols if c in sprint.columns])\n", " .sort(\"sprint_speed\", descending=True).head(10)\n", " if sprint is not None and sprint.height else \"leaderboard unavailable right now\")" ] }, { "cell_type": "markdown", "id": "aa7dad17", "metadata": {}, "source": [ "### Recipe 3 โ€” Box score for one game ๐Ÿ“Š\n", "\n", "Take a `game_pk` from any schedule and pull the full box score with\n", "[`mlb_boxscore`](../mlb/reference/mlb_api.md#mlb_boxscore). Asking for\n", "`return_parsed=False` gives the raw `dict`, which carries per-team batting and\n", "pitching lines under `teams.home` / `teams.away`." ] }, { "cell_type": "code", "execution_count": null, "id": "a23ab0aa", "metadata": {}, "outputs": [], "source": [ "def team_line(game_pk):\n", " box = mlb.mlb_boxscore(game_pk=game_pk, return_parsed=False)\n", " rows = []\n", " for side in (\"away\", \"home\"):\n", " t = box[\"teams\"][side]\n", " bat = t[\"teamStats\"][\"batting\"]\n", " rows.append({\"side\": side, \"team\": t[\"team\"][\"name\"],\n", " \"runs\": bat[\"runs\"], \"hits\": bat[\"hits\"],\n", " \"home_runs\": bat[\"homeRuns\"], \"rbi\": bat[\"rbi\"], \"avg\": bat[\"avg\"]})\n", " return pl.DataFrame(rows)\n", "\n", "# Use a game_pk from the schedule we pulled, or fall back to a known game.\n", "gid = int(schedule[\"game_pk\"][0]) if (schedule is not None and schedule.height) else 744914\n", "box_df = safe(f\"boxscore {gid}\", lambda: team_line(gid))\n", "out = box_df if box_df is not None else \"boxscore unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "0178b094", "metadata": {}, "source": [ "### Recipe 4 โ€” Plate-appearance play-by-play + outcome mix โšพ\n", "\n", "[`mlb_play_by_play`](../mlb/reference/mlb_api.md#mlb_play_by_play)\n", "returns a `dict` with an `allPlays` list โ€” one entry per plate appearance.\n", "Flatten it with `pl.json_normalize` (dot-notation columns), then tally the\n", "plate-appearance outcomes." ] }, { "cell_type": "code", "execution_count": null, "id": "0b14e295", "metadata": {}, "outputs": [], "source": [ "def pbp_frame(game_pk):\n", " raw = mlb.mlb_play_by_play(game_pk=game_pk, return_parsed=False)\n", " return pl.json_normalize(raw[\"allPlays\"], separator=\".\", max_level=2)\n", "\n", "plays = safe(f\"play-by-play {gid}\", lambda: pbp_frame(gid))\n", "if plays is not None and plays.height:\n", " pcols = [\"about.inning\", \"about.halfInning\", \"matchup.batter.fullName\",\n", " \"matchup.pitcher.fullName\", \"result.event\"]\n", " out = plays.select([c for c in pcols if c in plays.columns]).head()\n", "else:\n", " out = \"play-by-play unavailable right now\"\n", "out" ] }, { "cell_type": "code", "execution_count": null, "id": "63030065", "metadata": {}, "outputs": [], "source": [ "# Outcome mix for the game โ€” the shape of every plate appearance.\n", "if plays is not None and plays.height and \"result.event\" in plays.columns:\n", " out = (plays.group_by(\"result.event\")\n", " .agg(pl.len().alias(\"count\"))\n", " .sort(\"count\", descending=True).head(10))\n", "else:\n", " out = \"no play-by-play to summarize right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "b30cbdcc", "metadata": {}, "source": [ "### Recipe 5 โ€” League leaders for any stat ๐Ÿฅ‡\n", "\n", "[`mlb_stats_leaders`](../mlb/reference/additional.md#mlb_stats_leaders)\n", "gives you the league leaderboard for **any** category โ€” `homeRuns`, `avg`,\n", "`era`, `strikeouts`, you name it. The leaders come back nested under each\n", "category, so we flatten the top-N into a tidy frame. Here's the 2024 home-run\n", "race." ] }, { "cell_type": "code", "execution_count": null, "id": "ae0025dc", "metadata": {}, "outputs": [], "source": [ "def hr_leaders(season, category=\"homeRuns\", group=\"hitting\", n=10):\n", " raw = mlb.mlb_stats_leaders(leader_categories=category, season=season,\n", " stat_group=group, limit=n)\n", " leaders = raw[\"leagueLeaders\"][0][\"leaders\"]\n", " rows = [{\"rank\": l[\"rank\"], \"player\": l[\"person\"][\"fullName\"],\n", " \"team\": l.get(\"team\", {}).get(\"name\"), \"value\": l[\"value\"]}\n", " for l in leaders]\n", " return pl.DataFrame(rows)\n", "\n", "leaders = safe(\"2024 HR leaders\",\n", " lambda: hr_leaders(SAMPLE_SEASON, \"homeRuns\", \"hitting\", 10))\n", "leaders if leaders is not None else \"leaders unavailable right now\"" ] }, { "cell_type": "markdown", "id": "88147c0f", "metadata": {}, "source": [ "### Recipe 6 โ€” Who's beating their expected stats? ๐ŸŽฒ\n", "\n", "Statcast's expected stats ask *what should have happened* given each ball's\n", "exit velocity and launch angle.\n", "[`mlb_statcast_leaderboard_expected_stats`](../mlb/reference/mlb_statcast.md#mlb_statcast_leaderboard_expected_stats)\n", "hands you `ba`/`est_ba`, `slg`/`est_slg`, `woba`/`est_woba` side by side โ€”\n", "sort by the diff to find the luckiest (and unluckiest) hitters." ] }, { "cell_type": "code", "execution_count": null, "id": "e82b4cd2", "metadata": {}, "outputs": [], "source": [ "xstats = safe(\n", " \"expected stats\",\n", " lambda: mlb.mlb_statcast_leaderboard_expected_stats(\n", " year=SAMPLE_SEASON, type=\"batter\", min=\"q\"),\n", ")\n", "if xstats is not None and xstats.height and \"est_woba_minus_woba_diff\" in xstats.columns:\n", " cols = [\"last_name, first_name\", \"pa\", \"woba\", \"est_woba\",\n", " \"est_woba_minus_woba_diff\"]\n", " # Most negative diff = outperforming their expected wOBA the most.\n", " out = (xstats.select([c for c in cols if c in xstats.columns])\n", " .sort(\"est_woba_minus_woba_diff\").head(10))\n", "else:\n", " out = \"expected-stats leaderboard unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "032a67c9", "metadata": {}, "source": [ "### Recipe 7 โ€” The fastest bats in baseball ๐Ÿ’จ\n", "\n", "Bat tracking is one of Statcast's newest toys.\n", "[`mlb_statcast_leaderboard_bat_tracking`](../mlb/reference/mlb_statcast.md#mlb_statcast_leaderboard_bat_tracking)\n", "returns average bat speed, swing length, and \"hard-swing rate\" per hitter โ€”\n", "sort by `avg_bat_speed` to see who's swinging the hardest." ] }, { "cell_type": "code", "execution_count": null, "id": "6221eef1", "metadata": {}, "outputs": [], "source": [ "bats = safe(\n", " \"bat tracking\",\n", " lambda: mlb.mlb_statcast_leaderboard_bat_tracking(year=SAMPLE_SEASON, type=\"batter\"),\n", ")\n", "if bats is not None and bats.height and \"avg_bat_speed\" in bats.columns:\n", " cols = [\"name\", \"swings_competitive\", \"avg_bat_speed\",\n", " \"hard_swing_rate\", \"swing_length\"]\n", " out = (bats.select([c for c in cols if c in bats.columns])\n", " .sort(\"avg_bat_speed\", descending=True).head(10))\n", "else:\n", " out = \"bat-tracking leaderboard unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "f457df31", "metadata": {}, "source": [ "### Recipe 8 โ€” The best gloves: Outs Above Average ๐Ÿงค\n", "\n", "Offense is easy to measure; defense is hard. Statcast's\n", "[`mlb_statcast_leaderboard_outs_above_average`](../mlb/reference/mlb_statcast.md#mlb_statcast_leaderboard_outs_above_average)\n", "credits fielders for the plays they make *relative to expectation*. Sort by\n", "`outs_above_average` to find the season's best defenders." ] }, { "cell_type": "code", "execution_count": null, "id": "40a041d6", "metadata": {}, "outputs": [], "source": [ "oaa = safe(\n", " \"outs above average\",\n", " lambda: mlb.mlb_statcast_leaderboard_outs_above_average(year=SAMPLE_SEASON),\n", ")\n", "if oaa is not None and oaa.height and \"outs_above_average\" in oaa.columns:\n", " cols = [\"last_name, first_name\", \"display_team_name\",\n", " \"primary_pos_formatted\", \"outs_above_average\",\n", " \"fielding_runs_prevented\"]\n", " out = (oaa.select([c for c in cols if c in oaa.columns])\n", " .sort(\"outs_above_average\", descending=True).head(10))\n", "else:\n", " out = \"OAA leaderboard unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "b67a6b8d", "metadata": {}, "source": [ "### Recipe 9 โ€” Find the X: the hardest-hit homers ๐Ÿš€\n", "\n", "`mlb_statcast_search` isn't just for one player โ€” point its filters at an outcome.\n", "Pass `at_bat_result=\"home_run\"` over a short window to pull **every homer**,\n", "then sort by `launch_speed` to find the ones that were absolutely crushed.\n", "(Keep the window small โ€” a couple of days at a time.)" ] }, { "cell_type": "code", "execution_count": null, "id": "289001a1", "metadata": {}, "outputs": [], "source": [ "homers = safe(\n", " \"home runs (2-day)\",\n", " lambda: mlb.mlb_statcast_search(start_dt=\"2024-07-01\", end_dt=\"2024-07-02\",\n", " at_bat_result=\"home_run\"),\n", ")\n", "if homers is not None and homers.height and \"launch_speed\" in homers.columns:\n", " print(\"homers in window:\", homers.height)\n", " cols = [\"game_date\", \"player_name\", \"launch_speed\",\n", " \"launch_angle\", \"hit_distance_sc\"]\n", " out = (homers.select([c for c in cols if c in homers.columns])\n", " .sort(\"launch_speed\", descending=True).head(10))\n", "else:\n", " out = \"no homers in that window right now\"\n", "out" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Recipe 13 โ€” Every pitch of a single game (Savant gamefeed) ๐ŸŽฎ\n", "\n", "[`mlb_statcast_gamefeed`](../mlb/reference/mlb_statcast.md#mlb_statcast_gamefeed)\n", "pulls Baseball Savant's rich single-game feed and tidies it to **one row per\n", "pitch** โ€” pitch type, velocity, plate location, and the batted-ball result โ€”\n", "across both teams. Feed it any `game_pk` from a schedule." ], "id": "gf01md" }, { "cell_type": "code", "metadata": {}, "source": [ "gf = safe(\n", " f\"gamefeed {gid}\",\n", " lambda: mlb.mlb_statcast_gamefeed(game_pk=gid),\n", ")\n", "if gf is not None and gf.height:\n", " print(\"pitches tracked:\", gf.height)\n", " gcols = [\"inning\", \"half_inning\", \"batter_name\", \"pitcher_name\",\n", " \"pitch_type\", \"start_speed\", \"launch_speed\", \"events\"]\n", " out = gf.select([c for c in gcols if c in gf.columns]).head()\n", "else:\n", " out = \"gamefeed unavailable right now\"\n", "out" ], "execution_count": null, "outputs": [], "id": "gf01code" }, { "cell_type": "markdown", "id": "11714150", "metadata": {}, "source": [ "### Recipe 10 โ€” The biggest swings of a game (WPA) ๐Ÿ“ˆ\n", "\n", "[`mlb_win_probability`](../mlb/reference/mlb_api.md#mlb_win_probability)\n", "returns every play with the live win-probability before and after, plus\n", "**Win Probability Added** (`homeTeamWinProbabilityAdded`). Sort by its absolute\n", "value to surface the most pivotal moments of the game." ] }, { "cell_type": "code", "execution_count": null, "id": "789c1d32", "metadata": {}, "outputs": [], "source": [ "def wpa_swings(game_pk, n=8):\n", " plays = mlb.mlb_win_probability(game_pk=game_pk, return_parsed=False)\n", " df = pl.json_normalize(plays, separator=\".\", max_level=2)\n", " keep = [\"about.inning\", \"about.halfInning\", \"result.event\",\n", " \"result.description\", \"homeTeamWinProbabilityAdded\"]\n", " df = df.select([c for c in keep if c in df.columns])\n", " if \"homeTeamWinProbabilityAdded\" in df.columns:\n", " df = (df.with_columns(\n", " pl.col(\"homeTeamWinProbabilityAdded\").abs().alias(\"wpa_abs\"))\n", " .sort(\"wpa_abs\", descending=True).drop(\"wpa_abs\").head(n))\n", " return df\n", "\n", "# Reuse the game_pk we pulled earlier (falls back to a known game).\n", "wpa = safe(f\"WPA swings {gid}\", lambda: wpa_swings(gid))\n", "wpa if wpa is not None else \"win-probability unavailable right now\"" ] }, { "cell_type": "markdown", "id": "174a7512", "metadata": {}, "source": [ "### Recipe 11 โ€” Season award winners (MVP, Cy Young) ๐Ÿ…\n", "\n", "[`mlb_awards`](../mlb/reference/mlb_api.md#mlb_awards) is the catalog of\n", "every award id; [`mlb_award_recipients`](../mlb/reference/mlb_api.md#mlb_award_recipients)\n", "names the season's winner for one id. We grab the four marquee awards โ€” AL/NL\n", "MVP and AL/NL Cy Young โ€” and stack them into one tidy board." ] }, { "cell_type": "code", "execution_count": null, "id": "5dc75b2a", "metadata": {}, "outputs": [], "source": [ "def award_board(season, award_ids):\n", " frames = []\n", " for label, aid in award_ids.items():\n", " df = mlb.mlb_award_recipients(award_id=aid, season=season)\n", " if df is not None and df.height:\n", " name_col = (\"player_name_first_last\" if \"player_name_first_last\"\n", " in df.columns else \"name\")\n", " frames.append(df.select([\n", " pl.lit(label).alias(\"award\"),\n", " pl.col(\"season\"),\n", " pl.col(name_col).alias(\"winner\"),\n", " ]))\n", " return pl.concat(frames, how=\"vertical\") if frames else pl.DataFrame()\n", "\n", "AWARDS = {\"AL MVP\": \"ALMVP\", \"NL MVP\": \"NLMVP\",\n", " \"AL Cy Young\": \"ALCY\", \"NL Cy Young\": \"NLCY\"}\n", "board = safe(\"2024 award winners\", lambda: award_board(SAMPLE_SEASON, AWARDS))\n", "board if (board is not None and board.height) else \"awards unavailable right now\"" ] }, { "cell_type": "markdown", "id": "4fc2193f", "metadata": {}, "source": [ "### Recipe 12 โ€” The first-round draft board ๐ŸŽ“\n", "\n", "[`mlb_draft`](../mlb/reference/mlb_api.md#mlb_draft) returns the amateur\n", "draft, organized into rounds of picks. Pass `round_=1` and flatten the picks\n", "into one row per selection โ€” who went where, and from which school." ] }, { "cell_type": "code", "execution_count": null, "id": "914cfba3", "metadata": {}, "outputs": [], "source": [ "def draft_board(year, round_=1):\n", " raw = mlb.mlb_draft(year=year, round_=round_, return_parsed=False)\n", " picks = raw[\"drafts\"][\"rounds\"][0][\"picks\"]\n", " rows = [{\n", " \"pick\": p.get(\"pickNumber\"),\n", " \"player\": p.get(\"person\", {}).get(\"fullName\"),\n", " \"team\": p.get(\"team\", {}).get(\"name\"),\n", " \"school\": p.get(\"school\", {}).get(\"name\"),\n", " } for p in picks]\n", " return pl.DataFrame(rows)\n", "\n", "draft = safe(\"2024 first round\", lambda: draft_board(2024, round_=1))\n", "draft.head(12) if (draft is not None and draft.height) else \"draft unavailable right now\"" ] }, { "cell_type": "markdown", "id": "89ac9a51", "metadata": {}, "source": [ "## ๐Ÿ“… A whole season's schedule via ESPN\n", "\n", "Want *every* game in a season without looping over dates? The bulk\n", "`load_mlb_*` release-parquet loaders are still being wired up (they raise a\n", "friendly `NotImplementedError` for now), and they point you to the working\n", "path: [`espn_mlb_schedule`](../mlb/reference/additional.md#espn_mlb_schedule)\n", "with `dates=` pulls the full slate as one wide frame. Scores come\n", "back as **strings** โ€” cast before doing arithmetic." ] }, { "cell_type": "code", "execution_count": null, "id": "acb05488", "metadata": {}, "outputs": [], "source": [ "season_sched = safe(\n", " \"ESPN 2024 season schedule\",\n", " lambda: mlb.espn_mlb_schedule(dates=2024),\n", ")\n", "if season_sched is not None and season_sched.height:\n", " print(\"games:\", season_sched.height)\n", " scols = [\"game_id\", \"away_display_name\", \"away_score\",\n", " \"home_display_name\", \"home_score\", \"status_type_completed\"]\n", " out = season_sched.select([c for c in scols if c in season_sched.columns]).head()\n", "else:\n", " out = \"ESPN schedule unavailable right now\"\n", "out" ] }, { "cell_type": "markdown", "id": "affc9883", "metadata": {}, "source": [ "## โšช Secondary path: ESPN teams (`espn_mlb_*`)\n", "\n", "[`espn_mlb_teams`](../mlb/reference/additional.md#espn_mlb_teams) returns one\n", "wide polars frame โ€” handy as a cross-check, or when you want ESPN's display\n", "names and ids alongside the MLB Stats API ones." ] }, { "cell_type": "code", "execution_count": null, "id": "036ef791", "metadata": {}, "outputs": [], "source": [ "espn_teams = safe(\"ESPN teams\", lambda: mlb.espn_mlb_teams())\n", "ecols = [\"team_id\", \"team_location\", \"team_name\", \"team_abbreviation\", \"team_display_name\"]\n", "(espn_teams.select([c for c in ecols if c in espn_teams.columns]).head()\n", " if espn_teams is not None else \"ESPN teams unavailable right now\")" ] }, { "cell_type": "markdown", "id": "b5741248", "metadata": {}, "source": [ "## ๐ŸŽ‰ Where to next\n", "\n", "- Everything returns **polars** by default โ€” pass `return_as_pandas=True` for a\n", " pandas frame, or `return_parsed=False` on the `mlb_*` wrappers for raw JSON.\n", "- Full reference: the **MLB** pages in the sidebar โ€”\n", " [MLB Stats API + Statcast helpers](../mlb/reference/additional.md),\n", " [the full MLB Stats API surface](../mlb/reference/mlb_api.md),\n", " and the ESPN [core](../mlb/reference/core.md) / [site](../mlb/reference/site.md) / [web](../mlb/reference/web.md) endpoints.\n", "- R user? The same data lives in [baseballr](https://billpetti.github.io/baseballr/).\n", "- Compare conventions with the other league intros (`04_nba_intro.ipynb`,\n", " `07_nhl_intro.ipynb`) or the cross-sport `01_quickstart.ipynb`.\n", "\n", "Now go find the next 60-homer season. โšพ๐Ÿ”ฅ" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }