Skip to main content

savant-extras

Baseball Savant leaderboard data — complements pybaseball.

pybaseball is great but many Baseball Savant leaderboards are missing or limited. savant-extras fills that gap with 17 leaderboards covering batting, pitching, catching, baserunning, fielding, and park factors — all as simple one-line function calls returning DataFrames.

Installation

pip install savant-extras

Quick Start

from savant_extras import (
    bat_tracking, pitch_tempo, arm_strength,
    pitch_movement, swing_take, catcher_throwing,
)

# Bat tracking with custom date range (2024+)
df = bat_tracking("2024-04-01", "2024-04-30")

# Pitcher pitch tempo
df = pitch_tempo(2024)

# Outfielder arm strength
df = arm_strength(2024, position="Outfielder")

# Slider movement
df = pitch_movement(2024, pitch_type="SL")

# Batter plate discipline
df = swing_take(2024)

# Catcher pop time & CS rate
df = catcher_throwing(2024)

All Functions

Every leaderboard function returns a pd.DataFrame. Most have a _range() variant for multi-season queries (adds a year column).

Batting

Function Data from Description
bat_tracking(start_date, end_date) 2024+ Bat speed, attack angle, swing tilt (custom date range)
bat_tracking_monthly(year) 2024+ Monthly bat tracking (Apr–Oct)
bat_tracking_splits(year) 2024+ First-half / second-half splits
batted_ball(year) — GB/FB/LD rates, pull/oppo splits
home_runs(year, player_type="batter", category="adj_xhr") — HRs hit (batter) or allowed (pitcher), xHR, no-doubters
swing_take(year) — Run values by zone (heart/shadow/chase/waste). Returned no rows through 0.5.0 — fixed in 0.6.0
year_to_year(year, player_type="batter", stat="xwoba") — A stat (xwOBA by default) across seasons

ABS challenges & minor leagues (new in 0.6.0)

Function Data from Description
abs_challenges(year, level="mlb", challenge_type="batter") MLB 2026+ / AAA 2025+ ABS challenge results vs. expected overturn rate, with run values and the MLBAM player_id (the site CSV export has neither); challenge_type is batter, pitcher or catcher
abs_challenges_range(start, end, ...) same One request per season, concatenated
statcast_minors(start_date, end_date, level="AAA") AAA 2023+ Pitch-level Statcast for Triple-A (ball flight and arm_angle; no bat tracking in the minors)
from savant_extras import abs_challenges, statcast_minors

# Who gains the most from challenging, a year before and after MLB adopted ABS
aaa = abs_challenges(2025, level="aaa")
mlb = abs_challenges(2026)

# One week of Triple-A pitches
pitches = statcast_minors("2025-06-01", "2025-06-07")

Pitching

Function Data from Description
pitch_tempo(year) 2010+ Pace metrics (median seconds, hot/warm/cold)
pitch_movement(year, pitch_type="FF") — Horizontal/vertical break for one pitch type
pitcher_arm_angle(year) — Release point angles and positions
running_game(year) — Pitcher running game (pickoffs, CS above avg)
timer_infractions(year) 2023+ Pitch clock violations

Catching

Function Data from Description
catcher_blocking(year) — Blocks above average, PB/WP prevention
catcher_throwing(year) — Pop time, exchange time, CS rate, arm strength
catcher_stance(year) — One-knee vs traditional: framing, blocking, throwing

Baserunning & Fielding

Function Data from Description
arm_strength(year, position="") 2020+ Fielder throw speed; position keeps fielders measured there
baserunning(year) — Total baserunning run value (XB + SB)
basestealing(year) — Stolen base run value, lead distances

Park Factors

Function Data from Description
park_factors(season) 2015+ Ballpark run factors per club, 1-year and 3-year windows (Baseball Savant)
park_factors_range(start, end) 2015+ Multi-season park factors concatenated
park_factors_fangraphs(season) 2015+ FanGraphs Guts! park factors - the only source here for pf_5yr and pf_fip
park_factors_fangraphs_range(start, end) 2015+ Multi-season FanGraphs park factors

Columns returned: season, team, venue_id, venue_name, n_pa_1yr, n_pa_3yr, pf_1yr, pf_3yr, pf_3yr_years, pf_hr, pf_1b, pf_2b, pf_3b, pf_so, pf_bb, pf_obp, pf_hits, pf_woba, pf_wobacon, pf_xwobacon, pf_bacon, pf_xbacon, pf_hardhit, pf_wobatto. All factors: 100 = neutral, >100 = hitter-friendly, <100 = pitcher-friendly. Every column except pf_1yr and n_pa_1yr comes from the 3-year window; pf_3yr_years records which window that was.

All 30 clubs appear in every season, and every factor column is float64 in every season. Two kinds of NaN come from the 3-year view (measured 2026-09-23 over 2015-2026, 360 rows):

  • 8 rows have no 3-year window at all, because the park has no three-year history: 2017 ATL, 2018 ATL, 2020 TEX, 2020 TOR, 2021 TEX, 2025 OAK, 2025 TB, 2026 OAK. Their pf_3yr* columns are NaN while pf_1yr is still filled.
  • pf_xwobacon, pf_xbacon and pf_hardhit are NaN for all 30 clubs in 2015 and 2016 (60 further rows), because those 3-year windows reach back before Statcast measured batted balls. Those three columns have 68 NaN, not 8.

Source changed in 0.5.0. Through 0.4.4 park_factors() scraped FanGraphs Guts!, in a package whose every other function reads Baseball Savant. It now reads Savant too. The FanGraphs table is still here as park_factors_fangraphs(), which is the only way to get pf_5yr and pf_fip.

Columns kept their names but changed their meaning. pf_hr, pf_1b, pf_2b, pf_3b, pf_so, pf_bb and the runs factor come from Savant's index now, and the two scales disagree: COL 2024 is pf_hr 109 here and 131 on the FanGraphs scale. Pin savant-extras<0.5 or call park_factors_fangraphs() if you need the old numbers.

The old implementation sent a Chrome User-Agent, and that is what FanGraphs challenges: measured 2026-09-23 from a residential line, a Chrome/140 UA gets 403 + cf-mitigated: challenge while curl, no UA, and this package's own UA all get 200. park_factors_fangraphs() says who it is instead. Datacenter addresses are blocked separately - pybaseball, which sends the honest default UA, still got 403 for all 12 seasons on a GitHub runner (2026-09-21), so expect the FanGraphs functions to work from a home connection and not from a cloud runner.

from savant_extras import park_factors, park_factors_range

# Single season
df = park_factors(2024)
print(df[df["team"] == "COL"][["team", "pf_3yr", "pf_1yr", "pf_hr"]])
#   team  pf_3yr  pf_1yr  pf_hr
# 7  COL   125.0   121.0  109.0

# Multi-season (e.g. for model training)
df = park_factors_range(2020, 2025)
print(df["season"].nunique())  # 6

Common Parameters

Parameter Type Description
player_type str "batter" or "pitcher" (where applicable)
min_* int or str Minimum qualifier. Pass an int (e.g. min_pa=100) or "q" to apply the MLB standard qualifier automatically.
position str Position filter (arm_strength): "", "RF", "SS", etc.
pitch_type str Pitch type filter (pitch_movement): "FF", "SL", etc.

Multi-Season Queries

Most functions have a _range(start_year, end_year) variant:

from savant_extras import pitch_tempo_range, arm_strength_range

# Compare pitch tempo pre/post pitch clock
df = pitch_tempo_range(2022, 2024)

# 5 years of arm strength
df = arm_strength_range(2020, 2024)

Demo App

MLB Bat Tracking Dashboard — built with savant-extras (source)

Why savant-extras?

Leaderboard pybaseball savant-extras
Bat tracking (date range) Full season only Custom date ranges
Pitch tempo Not supported ✅
Arm strength Not supported ✅
Batted ball profile Not supported ✅
Home runs Not supported ✅
Pitch movement statcast_pitcher_pitch_movement ✅
Swing & take statcast_batter_run_value / statcast_pitcher_run_value ✅
Year-to-year changes Not supported ✅
Pitcher arm angle Not supported ✅
Running game (pitcher) Not supported ✅
Catcher blocking Not supported ✅
Catcher throwing Pop time only (statcast_catcher_poptime) ✅
Catcher stance Not supported ✅
Baserunning run value Not supported ✅
Basestealing run value Not supported ✅
Timer infractions Not supported ✅
Park factors (Statcast) Not supported ✅
ABS challenges (MLB + AAA) Not supported ✅
Minor-league Statcast (AAA) Not supported (statcast() has no minors switch) ✅

Known Issues

  • Before 0.6.0, twelve functions returned the wrong season or ignored a filter - Savant renamed their query parameters and, when it does not recognise one, silently serves the current season. baserunning, basestealing, catcher_blocking, catcher_throwing, running_game, pitcher_arm_angle and timer_infractions returned the same table for every year; catcher_stance returned league summary rows; pitch_movement always returned four-seamers; home_runs, year_to_year and arm_strength ignored their filters. Re-fetch anything saved with those versions. 0.6.0 sends the parameters Savant reads and raises ValueError when the season it returns is not the one asked for. See the CHANGELOG for the table.
  • swing_take() returned no rows through 0.5.0. This was a bug in this package, not an upstream outage as this section used to say: it sent type=batter, and the leaderboard's parameter is group=Batter / group=Pitcher (case-sensitive). Fixed in 0.6.0.
  • Empty answers now warn. Savant replies to a request it cannot serve with HTTP 200 and either an HTML page or a CSV header with no rows. Every function used to turn that into an empty DataFrame silently, which is how the swing_take bug went unnoticed. They now emit savant_extras.EmptySavantResponse (a UserWarning) with the URL; filter it with warnings.simplefilter("ignore", EmptySavantResponse) if an empty season is expected.

Cloud Environment Notes

Every function in this package except the two *_fangraphs ones reads Baseball Savant, which answers cloud runners (Kaggle, Google Colab, GitHub Actions) the same way it answers a laptop. Before 0.5.0 park_factors() read FanGraphs and returned 403 from those environments; that is no longer the case.

park_factors_fangraphs() still depends on FanGraphs, which blocks datacenter addresses — measured on a GitHub runner 2026-09-21, where pybaseball got 403 for all 12 seasons despite sending the honest default User-Agent. Run it from a home connection and save the result:

from savant_extras import park_factors_fangraphs_range
df = park_factors_fangraphs_range(2024, 2025)
df.to_csv("park_factors_fg.csv", index=False)

If a fetch fails for every season, both *_range functions return an empty DataFrame with a warning rather than raising, and a partial failure warns with the seasons it skipped.

In Kaggle notebooks, pybaseball is not pre-installed. Install both explicitly:

!pip install savant-extras pybaseball

License

MIT

Metadata

Release files for savant-extras 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for savant-extras 0.6.0
File Size Uploaded
savant_extras-0.6.0.tar.gz 52.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for savant-extras 0.6.0
File Interpreter ABI Platform
savant_extras-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 95.6 kB

Release files / savant_extras-0.6.0.tar.gz

Download URL savant_extras-0.6.0.tar.gz
Size 52.2 kB
Tags Source
SHA-256 checksum
How to use checksums
788f21b634b8371925158cf760e3b6b30df3b4487ebe9a58dc9a3ba90846c0f8
BLAKE2b-256 checksum
How to use checksums
9d9c9c966e9351909f968a64b5004dba297d50ba12c1200dedfec8c436a5f704
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release files / savant_extras-0.6.0-py3-none-any.whl

Download URL savant_extras-0.6.0-py3-none-any.whl
Size 43.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7d7ba9bd7bc452e76759ffc335a549f22ae5722a941fa7657e10b3f2118e4baf
BLAKE2b-256 checksum
How to use checksums
a13de10c36c9b091f765bd61aaddc33dd900838836d4341784234355dade480d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

0.6.1

2 release files

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page