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.

Subscribe to Singletons Going Steady

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe