Using Polars to find pitcher velocity drops
While looking at a fantasy baseball trade involving Freddy Peralta, I caught this X post about a potential drop in his velocity against the Chicago White Sox on April 29, 2025:

I used Polars to explore by loading Baseball Savant data the following morning, and building a concise pipeline that evaluates pitcher appearances for velocity gains and losses.
Velocity fluctuations aren’t just numbers—they’re windows into a pitcher’s form, fatigue levels, and even injury risks. A sudden drop in release speed might signal arm stiffness, while a surprising uptick could reflect a pitcher pushing harder or refining their mechanics. In this post, we’ll walk through a compact pipeline that compares each pitcher’s most recent average velocity for each pitch type against a rolling three-game baseline. Along the way, you’ll see how concise code can surface these critical insights. We'll use Polars for this for its concision.
We begin by restricting to 2025 regular-season data and ordering by pitcher, date, then pitch type—ensuring subsequent window functions operate on a true chronological sequence.
I am using data from Baseball Savant, which can be exported from their search. In this case, I am using all of 2025 to date (May 1, 2025).
import polars as pl
df = pl.read_parquet("2025-mlb.parquet")
df = df.filter(game_type="R") # regular season only
df = df.sort("pitcher", "game_date", "pitch_type")
Next, we group every pitch of the same type thrown by a pitcher per game date and compute its mean release_speed
(velocity at time of release). It’s our measure of that outing’s velocity for each pitch type.
df = df.group_by(
["pitcher", "game_date", "pitch_type"],
maintain_order=True
).agg(
pl.col("release_speed").mean().alias("avg_release_speed")
)
By shifting the current game’s value out of the window, we calculate the rolling average of release_speed
from the pitcher's three most recent previous appearances—requiring at least two prior games to qualify. This baseline reflects a pitcher’s recent “normal” velocity.
df = df.with_columns(
pl.col("avg_release_speed")
.shift(1)
.rolling_mean(window_size=3, min_samples=2)
.over(["pitcher", "pitch_type"])
.alias("avg_last_3_speeds")
)
Here we isolate each pitcher’s latest outing and compute the difference between that outing’s speed and their recent baseline. Positive deltas can highlight mechanical gains or extra effort; negative deltas can prompt a closer look at fatigue or injury.
df = (
df.group_by("pitcher", "pitch_type", maintain_order=True)
.agg(
pl.col("avg_release_speed").last().alias("avg_release_speed"),
pl.col("avg_last_3_speeds").last().alias("avg_last_3_speeds"),
pl.col("game_date").last().alias("most_recent_game_date"),
)
.with_columns(
(pl.col("avg_release_speed") - pl.col("avg_last_3_speeds")).alias("delta")
)
)
Dropping pitchers without sufficient history keeps our comparisons robust. Sorting by delta
surfaces the most pronounced velocity changes—both improvements and declines.
df = df.drop_nulls()
df = df.sort("delta")
Remove incomplete pitcher data, sort
Here's how Freddy Peralta’s recent velocity compares to his three previous starts:
┌────────────┬────────────┬──────┬───────────┬───────────┐
│ game_date ┆ pitch_type ┆ velo ┆ prev_velo ┆ delta │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ str ┆ f64 ┆ f64 ┆ f64 │
╞════════════╪════════════╪══════╪═══════════╪═══════════╡
│ 2025-04-29 ┆ FF ┆ 93.0 ┆ 95.1 ┆ -2.1 │
│ 2025-04-29 ┆ SL ┆ 82.2 ┆ 84.3 ┆ -2.1 │
│ 2025-04-29 ┆ CH ┆ 87.7 ┆ 89.5 ┆ -1.8 │
│ 2025-04-29 ┆ CU ┆ 79.3 ┆ 80.7 ┆ -1.5 │
└────────────┴────────────┴──────┴───────────┴───────────┘
Uh oh, Peralta's numbers show a noticeable drop across all pitches
Why Velocity Deltas Matter
- Performance Monitoring: A rising fastball can correlate with effectiveness, while an uncharacteristic dip may precede reduced strikeout rates.
- Health Alerts: Subtle, persistent drops in velocity often precede arm soreness or injury. Tracking these trends can inform workload management.
- Mechanics Insights: Sudden increases might indicate a change in delivery or arm slot—useful for coaching adjustments.
Administrative Notes
- Few-pitch outings could skew the baseline, so consider weighted means
- Statcast measurements can vary by ballpark and tracking conditions, so you may consider adjustments or calibration.
This Polars pipeline elegantly demonstrates how a few clear, simple steps can flag the pitchers whose velocity is shifting most dramatically, offering coaches, analysts, and medical staff actionable data.