Letting Them Off the Hook: When Pitchers Are Unable to Finish Batters and End Innings

As a pitcher, two-strike counts can be tricky. On one hand, you’re one pitch away from ending the at-bat. On the other, you still have to throw a small ball sixty feet, six inches past a very motivated man - a brutal task if you dwell at all on the mechanics of what you’re doing. In my experience, the best thing to do was simply try to block the situation out and work one pitch at a time.

Why have I been thinking about this? It’s the Nationals’ bullpen, of course. Despite my attempt to reverse-jinx them with my June blog post about how they led baseball in blown saves by a significant margin, they have continued to struggle. I’ve found it particularly painful when they’ve been one pitch away from ending an inning—with two outs and two strikes—only to lose the batter and give up runs later in that same inning.

It is in these scenarios that the pen’s lack of putaway stuff really hurts—as originally visualized in the June post, they have by far the lowest K/9 of all MLB bullpens.

HR/9 and K/9 for MLB bullpens

Visualization

To confirm my hypothesis, I spent a bit of time playing with MLB’s pitch-by-pitch data, looking for situations where teams were “one pitch away” from ending a half-inning but were unable to do so. With those situations identified, I then aggregated the number of runs that came around to score.

Runs allowed after failing to retire a batter with two outs and two strikes

Of course, this happens to the best of teams - pressure-packed pitching and clutch situational hitting is simply part of the game. But looking at the results, it is striking to see how much separation there is between MLB’s best and worst teams in this department - with teams like the Nats and Orioles allowing over 120 runs in this context. Failing to put away a single hitter does not just prolong an at-bat - it can open the door to a rally that can potentially change the game.

For the Nationals, the chart reinforces just how costly their struggles to finish batters and limit the damage have been - something I desperately hope they can improve while they still have a puncher’s chance at an NL Wild Card spot.

Code Reference

library(tidyverse)

# Download pitch-level Statcast data for the 2026 regular season
statcast_url <- paste0(
  "https://baseballsavant.mlb.com/statcast_search/csv?",
  "all=true&type=pitcher&game_date_gt=2026-03-25&game_date_lt=2026-08-02"
)

statcast <- read_csv(statcast_url, show_col_types = FALSE)

reach_events <- c(
  "single", "double", "triple", "home_run", "walk", "intent_walk",
  "hit_by_pitch", "field_error", "catcher_interf", "catcher_interference"
)

# Collapse the pitch rows into one record per plate appearance
plate_appearances <- statcast %>%
  filter(game_type == "R") %>%
  arrange(game_pk, at_bat_number, pitch_number) %>%
  group_by(game_pk, inning, inning_topbot, at_bat_number, home_team, away_team) %>%
  summarise(
    outs = first(outs_when_up),
    reached_two_strikes = any(strikes == 2, na.rm = TRUE),
    event = last(events[!is.na(events)]),
    fielding_team = if_else(first(inning_topbot) == "Top", first(home_team), first(away_team)),
    score_before = if_else(first(inning_topbot) == "Top", first(away_score), first(home_score)),
    score_after = if_else(first(inning_topbot) == "Top", last(post_away_score), last(post_home_score)),
    .groups = "drop"
  )

# Capture the batting team's score at the end of each half-inning
inning_finals <- plate_appearances %>%
  group_by(game_pk, inning, inning_topbot) %>%
  summarise(inning_final_score = last(score_after), .groups = "drop")

# Keep the first two-out, two-strike failure in each half-inning
team_damage <- plate_appearances %>%
  filter(outs == 2, reached_two_strikes, event %in% reach_events) %>%
  group_by(game_pk, inning, inning_topbot) %>%
  slice_min(at_bat_number, n = 1, with_ties = FALSE) %>%
  ungroup() %>%
  left_join(inning_finals, by = c("game_pk", "inning", "inning_topbot")) %>%
  # Count every run scored before the defense records the third out
  mutate(runs_allowed = pmax(inning_final_score - score_before, 0)) %>%
  summarise(
    innings_extended = n(),
    runs_allowed = sum(runs_allowed),
    .by = fielding_team
  ) %>%
  arrange(desc(runs_allowed))