Tiger Hitter Consistency

statistics
r
baseball
Author

Mark Jurries II

Published

August 5, 2025

One of my favorite things at Fangraphs are their rolling stat charts. Want to know the ups and downs of Pete Crow-Armstrong’s year? Just load up the 15-day rolling wOBA chart and you’ve got your answer. The only drawback - and I’m sure I’m in a very niche audience for this - is that I can’t see an entire team at once.

Thankfully, we can get game log data using the baseballr package. I wound up calcing wOBA* by using a moving sum of its components (singles, doubles, triples, HR, walks, hit by pitch, and plate appearances), so it may vary slightly from what Fangraphs shows, but it’s close enough.

*This is a nice primer to wOBA. It’s been my preferred offensive stat for years now, but the method could apply to batting average, on-base percentage, slugging, etc.

I chose to go with a 7 game moving average. This means there’s a bit more noise, but I think this lines up more with how most fans would think about recent performance. We’ll let the average cut across off days and injured list stints, this way a player won’t see a change simply because they didn’t play on a given day and their 7 day average becomes 6 days. Finally, this is filtered on all batters with at least 100 PA, and ends on 8/4/2025.

Show the code
library(hrbrthemes)
library(ggrepel)
library(gt)
library(gtExtras)
library(tidyverse)
library(zoo)

det_2025_long <- read_csv('det_2025.csv')

det_2025_date <- det_2025_long %>%
  select(Date) %>%
  distinct()

det_2025_players <- det_2025_long %>%
  select(playerid, PlayerName) %>%
  distinct()

det_2025_long_munged <-  det_2025_long %>%
  select(PlayerName, playerid, Date, PA, `1B`, `3B`, `2B`, HR, BB, HBP, SB, CS) %>%
  arrange(playerid, Date) %>%
  group_by(playerid, PlayerName) %>%
  mutate(rolling_PA = rollmean(PA, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_1B = rollmean(`1B`, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_2B = rollmean(`2B`, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_3B = rollmean(`3B`, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_HR = rollmean(HR, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_BB = rollmean(BB, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_HBP = rollmean(HBP, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_SB = rollmean(SB, k = 7, fill = NA, align = "right", na.rm = TRUE),
         rolling_CS = rollmean(CS, k = 7, fill = NA, align = "right", na.rm = TRUE)) %>%
  mutate(rolling_wOBA = (
    (rolling_1B * .884) + (rolling_2B * 1.256) + (rolling_3B * 1.256) + 
      (rolling_HR * 2.047) + (rolling_BB * .692) + (rolling_HBP * .723)
  ) / rolling_PA
  )


det_2025_date %>%
  cross_join(det_2025_players) %>%
  left_join(det_2025_long_munged) %>%
  ggplot(aes(x = Date, y = rolling_wOBA, group = PlayerName))+
  geom_line()+
  facet_wrap(PlayerName ~ .)+
  theme_ipsum()

The first thing we notices is that everybody has ups and downs. Javier Baez had two monstrous stretches followed by some… not monstrous stretches, which we kind of knew anyway.

It’d be nice to have something besides the eyeball test to let us know who’s consistent. Here, we can borrow from Exploring Baseball Data with R and calculate the average and standard deviation of our seven day streaks. We’ll plot these against each other, adding a line for league average wOBA (.314) so we have a reference point for the game overall. Lower standard deviations mean more consistency, while higher mean streakier performances.

Show the code
det_2025_long_munged %>%
  group_by(playerid, PlayerName) %>%
  summarise(mean_roll = mean(rolling_wOBA, na.rm = TRUE),
            sd_roll = sd(rolling_wOBA, na.rm = TRUE)) %>%
  arrange(desc(sd_roll)) %>%
  ggplot(aes(x = sd_roll, y = mean_roll, label = PlayerName))+
  geom_point()+
  geom_text_repel()+
  geom_hline(yintercept = .314, linetype = 'dashed')+
  theme_ipsum()+
  xlab('7-Day wOBA Standard Deviation')+
  ylab('7-Day wOBA Mean')+
  annotate("text", x = 0.07, y = 0.37, 
           label= "More Consistent", 
           fontface = "bold",
           size = 6,
           color = '#0C2340')+
  annotate("text", x = 0.125, y = 0.37, 
           label= "Less Consistent", 
           size = 6,
           fontface = "bold",
           color = '#FA4616')

Confession time: If Gleyber Torres hadn’t come in as the most consistent hitter in this lineup, I’d have questioned the entire methodology. But he and Tork are good and consistent, while Riley Greene is good but streaky. Kerry Carpenter had injuries, so in his case a high SD is good since it means he’s improving. Javy being streaky should surprise no one, that’s always been a part of his game.

Whether or not consistency is inherently better is another question. Greene is less consistent that Torkleson, but is having a more productive year overall. Meanwhile, Parker Meadows was consistent, but also bad*. Frankly, I’m fine with lumpy performances as long as they all get hot in the postseason.

*Though we still have high hopes for his future.

Bonus table for your edification.

Show the code
det_2025_long_munged %>%
  group_by(PlayerName) %>%
  summarise(PA = sum(PA),
            mean_roll = mean(rolling_wOBA, na.rm = TRUE),
            sd_roll = sd(rolling_wOBA, na.rm = TRUE)) %>%
  arrange(sd_roll) %>%
  gt() %>%
  gt_theme_espn() %>%
  fmt_number(columns = c('mean_roll', 'sd_roll'), decimals = 3) %>%
  cols_label(PlayerName = 'Player',
             mean_roll = 'Mean',
             sd_roll = 'SD') %>%
  tab_header(title = 'Detroit Tigers 7 Day wOBA',
             subtitle = 'Data through 8/4/2025, min 100 PA')
Detroit Tigers 7 Day wOBA
Data through 8/4/2025, min 100 PA
Player PA Mean SD
Gleyber Torres 430 0.348 0.066
Parker Meadows 137 0.229 0.066
Spencer Torkelson 452 0.342 0.077
Dillon Dingler 312 0.315 0.081
Andy Ibanez 120 0.316 0.092
Zach McKinstry 379 0.336 0.096
Wenceel Perez 198 0.320 0.103
Colt Keith 343 0.319 0.107
Justyn-Henry Malloy 118 0.270 0.111
Trey Sweeney 258 0.249 0.112
Riley Greene 464 0.356 0.121
Javier Baez 328 0.303 0.124
Kerry Carpenter 301 0.342 0.129