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.