Wins, Losses, and Saves

Beginner 10 min read 0 views Nov 26, 2025

Wins, Losses, and Saves: Understanding Baseball's Traditional Pitching Statistics

Wins, losses, and saves have been baseball's primary pitching statistics for over a century. While they remain prominent in record books and popular discourse, modern analytics has revealed significant limitations in their ability to evaluate pitcher performance. This comprehensive guide examines how these statistics work, their historical context, and why advanced metrics have largely superseded them in serious analysis.

Core Definitions

Wins (W)

A pitcher is credited with a win when they meet the following conditions:

  • Starting pitcher: Must pitch at least 5.0 innings
  • Leading when exiting: Team must be ahead when the starter leaves and never relinquish the lead
  • Winning pitcher: Official scorer determines which pitcher was most effective if starter doesn't qualify
  • Team victory required: Team must win the game

Losses (L)

A pitcher receives a loss when:

  • Pitcher of record: The pitcher on the mound when the opposing team takes the lead
  • Lead never regained: Team never ties or goes ahead after that point
  • Team loses game: Final score must be a loss

Saves (SV)

A relief pitcher earns a save by finishing a winning game under specific conditions (at least one must apply):

  • Narrow lead: Enters with a lead of three runs or fewer and pitches at least one inning
  • Tying run situation: Enters with the potential tying run on base, at bat, or on deck
  • Extended relief: Pitches effectively for at least three innings regardless of score

Holds (HLD)

An unofficial statistic created in 1986 to recognize effective middle relievers:

  • Enters in save situation: Meets save criteria when entering
  • Maintains lead: Records at least one out without surrendering the lead
  • Doesn't finish: Exits with team still ahead but doesn't record the final out

Blown Saves (BS)

A blown save occurs when:

  • Save situation squandered: Pitcher enters in a save situation but allows the tying or go-ahead run
  • Lead relinquished: Team's advantage is lost on that pitcher's watch
  • Can still get win: Pitcher can blow a save but earn the win if team retakes lead

The Win Allocation Rules: A Detailed Examination

The Five-Inning Rule for Starters

Established in the 1950s, the five-inning minimum ensures starters pitch a "significant" portion of the game. However, this arbitrary threshold creates perverse outcomes:

ScenarioInningsResultOutcome
Starter A dominates4.2 IP, 0 ERTeam wins 2-0No win (reliever gets W)
Starter B struggles5.0 IP, 7 ERTeam wins 10-8Gets the win
Starter C perfect4.0 IP, 0 H, 0 BBTeam wins 1-0No win

Official Scorer Discretion

When the starter doesn't qualify, the official scorer determines the winning pitcher based on "effectiveness"—a subjective judgment that can vary between scorers and ballparks.

Save Situation Rules in Detail

The Three-Run Rule

Why three runs? The threshold was designed to reflect situations where the closer could give up a game-tying three-run homer. This creates significant strategy around save opportunities:

Lead SizeSave Available?Managerial Incentive
1-3 runsYesUse best reliever
4+ runsNo (unless 3+ IP)Save closer for tomorrow

The One-Inning Minimum

With leads of 1-3 runs, pitchers must complete at least one inning. This prevents closers from entering with two outs in the ninth and earning a save for one out.

The Extended Relief Exception

The three-inning provision allows saves in blowouts if a reliever pitches extended innings. Rarely invoked in the modern game due to specialized bullpen roles.

Historical Evolution: From Complete Games to Bullpen Specialization

The Cy Young Era (1890s-1910s)

Pitchers routinely completed 30-40 games per season. Cy Young won 511 games in his career, a record that will never be approached:

  • Complete games were standard: Young completed 749 of 815 starts (91.9%)
  • No bullpen specialization: Relief pitchers were typically failed starters
  • Wins reflected workload: More starts and complete games meant more win opportunities

The Integration Era (1950s-1960s)

The save statistic was created in 1969 to recognize closers like Hoyt Wilhelm and Roy Face who pitched exclusively in relief:

  • Relief pitching gained respect: Teams began valuing specialists
  • First save leaders: Early leaders like Stu Miller and Ted Abernathy saved 20-30 games
  • Starting pitcher usage declined: Complete games dropped from 40% to 25%

The Closer Revolution (1980s-1990s)

Tony LaRussa and Dennis Eckersley popularized the modern closer role:

  • One-inning closers: Eckersley averaged just 1.1 IP per appearance in 1990 (51 saves)
  • Ninth-inning specialists: Best reliever reserved exclusively for save situations
  • Setup man emergence: Seventh and eighth innings got their own specialists
  • Save totals exploded: 40+ save seasons became common

The Modern Bullpen Era (2010s-Present)

Analytics revealed inefficiencies in traditional closer usage:

  • High-leverage approach: Using best relievers in highest-leverage situations (not always the ninth)
  • Committee closers: Teams rotating save opportunities based on matchups
  • Opener strategy: Reliever starts, "starter" pitches innings 2-5
  • Starter workload decline: Complete games now under 2% of starts

Why Wins Are Unreliable for Pitcher Evaluation

1. Team Quality Dominance

A pitcher's win total correlates far more with team offense and defense than individual performance:

FactorCorrelation with WinsCorrelation with ERA
Team runs scored0.650.12
Team defensive efficiency0.480.55
Pitcher strikeout rate0.31-0.67
Pitcher walk rate-0.220.58

2. Run Support Variance

Pitchers on the same team can receive wildly different run support:

Example: 2019 Atlanta Braves
Max Fried: 5.8 runs/game support, 17-6 record, 4.02 ERA
Mike Soroka: 4.9 runs/game support, 13-4 record, 2.68 ERA
Julio Teheran: 4.1 runs/game support, 10-11 record, 3.81 ERA

3. The Five-Inning Threshold Problem

Pitchers who consistently go 4.2 innings with dominant performances get no wins, while mediocre pitchers who reach 5.0 innings benefit.

4. Bullpen Dependency

Starters depend on relievers to preserve their wins. A great start can become a no-decision if the bullpen blows the lead.

Why Saves Are Problematic

1. Arbitrary Thresholds

The three-run cutoff creates perverse incentives:

  • 4-run lead: Closer sits, even in high-leverage situations
  • 3-run lead: Closer pitches, even in low-leverage situations
  • Managers optimize for saves: Not necessarily for win probability

2. Not All Saves Are Equal

Compare these save situations:

SituationDifficultySave Earned?
9th inning, 3-run lead, bases emptyLowYes
9th inning, 1-run lead, bases loaded, 0 outsExtremeYes
8th inning, 1-run lead, bases loaded (gets 2 outs, leaves)Very highNo (Hold)

3. Ninth-Inning Bias

The highest-leverage situation often occurs before the ninth inning, but closers rarely pitch in those spots.

Modern Analytical Alternatives

WAR (Wins Above Replacement)

Measures total value a pitcher provides compared to a replacement-level player:

  • Context-neutral: Adjusts for park factors, league, and era
  • Team-independent: Isolates individual performance
  • Comprehensive: Accounts for innings, quality, and role
WAR ValuePlayer Quality
8+ WARMVP candidate
5-8 WARAll-Star
2-5 WARSolid regular
0-2 WARRole player
<0 WARBelow replacement

ERA- (ERA Minus)

Park and league-adjusted ERA where 100 is average and lower is better:

  • 70 ERA-: 30% better than average (elite)
  • 100 ERA-: League average
  • 130 ERA-: 30% worse than average

WPA (Win Probability Added)

Measures how much a pitcher increases or decreases their team's win probability:

  • Context-dependent: Values high-leverage situations more
  • Clutch indicator: Shows performance in critical moments
  • Season totals: Elite relievers: +3.0 WPA, Elite starters: +5.0 WPA

Championship Win Probability (cWPA)

Like WPA but for playoff situations, where each game has championship implications.

Shutdown/Meltdown Ratio (SD/MD)

Reliever-specific metric tracking dominant outings vs. disastrous ones:

  • Shutdown: Enters high-leverage situation, decreases opposition win probability by 6%+
  • Meltdown: Enters high-leverage situation, decreases own team win probability by 6%+
  • Good relievers: SD/MD ratio above 2.0

The Rise and Evolution of the Closer Role

Pre-1970: The Fireman

Early relievers were "firemen" who entered in any high-leverage situation:

  • Multi-inning appearances: Often pitched 2-4 innings
  • Mid-game entries: Could enter in the 6th or 7th inning
  • No specialization: Pitched to both lefties and righties

1970s-1980s: The Closer Emerges

Teams began designating ninth-inning specialists:

  • Bruce Sutter: Revolutionized role with split-finger fastball (1979: 37 saves, 2.22 ERA)
  • Rollie Fingers: First reliever to win MVP (1981: 28 saves, 1.04 ERA)
  • Goose Gossage: Power closer prototype (84 career WAR as reliever)

1990s: One-Inning Closer Dominance

LaRussa's use of Eckersley created the modern template:

  • Dennis Eckersley (1988-1992): 220 saves, 1.91 ERA, 1.00 WHIP in relief
  • Mariano Rivera: Most successful closer ever (652 saves, 2.21 ERA, 0.70 postseason ERA)
  • Closer became high-status: Top draft picks and prospects assigned to role

2000s: Setup Man Specialization

The seventh and eighth innings got their own specialists:

  • Setup men valued: High-leverage middle relief became critical
  • Handedness matching: LOOGYs (Lefty One-Out Guys) proliferated
  • Three-headed monster: 7th-8th-9th inning specialists

2010s-Present: Analytics-Driven Bullpen Management

Modern teams optimize reliever usage based on leverage:

  • Best reliever in highest leverage: Might be the 8th inning with heart of order
  • Matchup-based closers: Different closers vs. RHH and LHH-heavy lineups
  • Openers and bulk guys: Blurring starter/reliever distinction
  • Fewer saves, more holds: Best relievers used outside save situations

Modern Bullpen Management Analytics

Leverage Index (LI)

Measures the criticality of a game situation (average = 1.0):

SituationLeverage Index
9th inning, tie game, bases loaded4.5
9th inning, 1-run lead, bases empty2.0
8th inning, tie game, runner on 2nd2.8
9th inning, 4-run lead0.3

Rest vs. Effectiveness

Research shows diminishing returns from extra rest:

  • Back-to-back days: Minimal performance decline for most relievers
  • Three consecutive days: Slight decline in velocity and command
  • Optimal usage: 2-3 appearances per week for closers

Platoon Splits Management

Modern bullpens carry specialists for specific matchups:

  • LOOGY usage declining: 3-batter minimum rule reduced value
  • Extreme splits rewarded: Relievers with 150+ OPS splits against one handedness

Python Code Examples

Fetching and Analyzing Pitching W/L/SV Data

from pybaseball import pitching_stats, statcast_pitcher
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Get pitching statistics for multiple seasons
seasons = [2022, 2023, 2024]
all_stats = []

for year in seasons:
    stats = pitching_stats(year, qual=100)
    stats['Season'] = year
    all_stats.append(stats)

pitching_df = pd.concat(all_stats, ignore_index=True)

# Analyze correlation between wins and team quality
print("Correlation Analysis: Wins vs Performance Metrics")
print("=" * 60)
correlations = pitching_df[['W', 'ERA', 'FIP', 'xFIP', 'K/9', 'BB/9', 'HR/9']].corr()['W'].sort_values(ascending=False)
print(correlations)

# Win rates vs FIP analysis
def categorize_fip(fip):
    if fip < 3.0:
        return 'Elite (<3.0)'
    elif fip < 3.75:
        return 'Above Avg (3.0-3.75)'
    elif fip < 4.25:
        return 'Average (3.75-4.25)'
    else:
        return 'Below Avg (>4.25)'

pitching_df['FIP_Category'] = pitching_df['FIP'].apply(categorize_fip)

fip_win_analysis = pitching_df.groupby('FIP_Category').agg({
    'W': ['mean', 'median', 'std'],
    'L': ['mean', 'median'],
    'ERA': 'mean',
    'FIP': 'mean'
}).round(2)

print("\nWin Rates by FIP Category:")
print(fip_win_analysis)

# Identify pitchers with wins not matching performance
pitching_df['Win_Pct'] = pitching_df['W'] / (pitching_df['W'] + pitching_df['L'])
pitching_df['Expected_Win_Pct'] = 0.500 + (4.00 - pitching_df['FIP']) / 10

pitching_df['Win_Pct_Diff'] = pitching_df['Win_Pct'] - pitching_df['Expected_Win_Pct']

print("\nMost Unlucky Pitchers (Good FIP, Poor W-L Record):")
unlucky = pitching_df.nlargest(10, 'Win_Pct_Diff')[
    ['Name', 'Season', 'W', 'L', 'Win_Pct', 'ERA', 'FIP', 'Win_Pct_Diff']
]
print(unlucky.to_string(index=False))

print("\nMost Lucky Pitchers (Poor FIP, Good W-L Record):")
lucky = pitching_df.nsmallest(10, 'Win_Pct_Diff')[
    ['Name', 'Season', 'W', 'L', 'Win_Pct', 'ERA', 'FIP', 'Win_Pct_Diff']
]
print(lucky.to_string(index=False))

# Visualization: Wins vs FIP
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# Scatter plot: Wins vs FIP
axes[0].scatter(pitching_df['FIP'], pitching_df['W'], alpha=0.6)
axes[0].set_xlabel('FIP', fontsize=12)
axes[0].set_ylabel('Wins', fontsize=12)
axes[0].set_title('Wins vs FIP (2022-2024)', fontsize=14)
axes[0].grid(True, alpha=0.3)

# Add trend line
z = np.polyfit(pitching_df['FIP'], pitching_df['W'], 1)
p = np.poly1d(z)
axes[0].plot(pitching_df['FIP'], p(pitching_df['FIP']), "r--", alpha=0.8,
             label=f'Trend: W = {z[0]:.2f}*FIP + {z[1]:.2f}')
axes[0].legend()

# Box plot: Wins by FIP category
pitching_df.boxplot(column='W', by='FIP_Category', ax=axes[1])
axes[1].set_xlabel('FIP Category', fontsize=12)
axes[1].set_ylabel('Wins', fontsize=12)
axes[1].set_title('Win Distribution by FIP Category', fontsize=14)
plt.suptitle('')

plt.tight_layout()
plt.savefig('wins_vs_fip_analysis.png', dpi=300)
plt.show()

Save Opportunity Conversion Rates

from pybaseball import pitching_stats
import pandas as pd
import matplotlib.pyplot as plt

# Get reliever statistics
relievers_2024 = pitching_stats(2024, qual=0)
closers = relievers_2024[relievers_2024['SV'] >= 10].copy()

# Calculate save conversion rate
closers['Save_Opportunities'] = closers['SV'] + closers['BS']
closers['Save_Pct'] = (closers['SV'] / closers['Save_Opportunities'] * 100).round(1)

# Add quality metrics
closers['K_BB_Ratio'] = (closers['SO'] / closers['BB']).round(2)
closers['Effectiveness_Score'] = (
    closers['Save_Pct'] * 0.4 +
    (100 - closers['ERA'] * 10) * 0.3 +
    closers['K/9'] * 2 * 0.3
)

print("2024 Closer Performance Analysis")
print("=" * 80)

top_closers = closers.nlargest(15, 'SV')[
    ['Name', 'Team', 'SV', 'BS', 'Save_Opportunities', 'Save_Pct',
     'ERA', 'FIP', 'K/9', 'BB/9', 'K_BB_Ratio']
].sort_values('Save_Pct', ascending=False)

print(top_closers.to_string(index=False))

# Analyze blown save situations
print("\n\nClosers with Most Blown Saves:")
blown_save_leaders = closers.nlargest(10, 'BS')[
    ['Name', 'Team', 'SV', 'BS', 'Save_Pct', 'ERA', 'FIP']
]
print(blown_save_leaders.to_string(index=False))

# Visualize save percentage vs ERA
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Save % vs ERA
axes[0, 0].scatter(closers['ERA'], closers['Save_Pct'], alpha=0.6, s=100)
axes[0, 0].set_xlabel('ERA', fontsize=11)
axes[0, 0].set_ylabel('Save Percentage', fontsize=11)
axes[0, 0].set_title('Save Conversion Rate vs ERA', fontsize=12)
axes[0, 0].grid(True, alpha=0.3)

# Save % vs FIP
axes[0, 1].scatter(closers['FIP'], closers['Save_Pct'], alpha=0.6, s=100, color='green')
axes[0, 1].set_xlabel('FIP', fontsize=11)
axes[0, 1].set_ylabel('Save Percentage', fontsize=11)
axes[0, 1].set_title('Save Conversion Rate vs FIP', fontsize=12)
axes[0, 1].grid(True, alpha=0.3)

# Save opportunities distribution
axes[1, 0].hist(closers['Save_Opportunities'], bins=20, color='skyblue', edgecolor='black')
axes[1, 0].set_xlabel('Save Opportunities', fontsize=11)
axes[1, 0].set_ylabel('Frequency', fontsize=11)
axes[1, 0].set_title('Distribution of Save Opportunities', fontsize=12)

# Save % distribution
axes[1, 1].hist(closers['Save_Pct'], bins=20, color='coral', edgecolor='black')
axes[1, 1].set_xlabel('Save Percentage', fontsize=11)
axes[1, 1].set_ylabel('Frequency', fontsize=11)
axes[1, 1].set_title('Distribution of Save Percentages', fontsize=12)
axes[1, 1].axvline(closers['Save_Pct'].mean(), color='red', linestyle='--',
                   label=f"Mean: {closers['Save_Pct'].mean():.1f}%")
axes[1, 1].legend()

plt.tight_layout()
plt.savefig('closer_analysis_2024.png', dpi=300)
plt.show()

# Statistical summary
print("\n\nCloser Performance Statistics Summary:")
print(f"Average Save Percentage: {closers['Save_Pct'].mean():.1f}%")
print(f"Median Save Percentage: {closers['Save_Pct'].median():.1f}%")
print(f"Average ERA: {closers['ERA'].mean():.2f}")
print(f"Average FIP: {closers['FIP'].mean():.2f}")
print(f"Average K/9: {closers['K/9'].mean():.2f}")
print(f"Average BB/9: {closers['BB/9'].mean():.2f}")

Visualizing Reliever Usage Patterns

from pybaseball import pitching_stats, playerid_lookup
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# Get reliever data
relievers = pitching_stats(2024, qual=20)
relievers = relievers[relievers['GS'] == 0]  # Only pure relievers

# Categorize relievers by role
def categorize_reliever(row):
    if row['SV'] >= 20:
        return 'Closer'
    elif row['HLD'] >= 15:
        return 'Setup'
    elif row['G'] >= 60:
        return 'Middle Relief (High Usage)'
    else:
        return 'Middle Relief'

relievers['Role'] = relievers.apply(categorize_reliever, axis=1)

# Usage pattern analysis
role_analysis = relievers.groupby('Role').agg({
    'G': 'mean',
    'IP': 'mean',
    'SV': 'mean',
    'HLD': 'mean',
    'BS': 'mean',
    'ERA': 'mean',
    'FIP': 'mean',
    'K/9': 'mean',
    'BB/9': 'mean'
}).round(2)

print("Reliever Usage Patterns by Role (2024)")
print("=" * 80)
print(role_analysis)

# Calculate innings per appearance
relievers['IP_per_G'] = (relievers['IP'] / relievers['G']).round(2)

# Visualize usage patterns
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Innings per appearance by role
role_order = ['Closer', 'Setup', 'Middle Relief (High Usage)', 'Middle Relief']
sns.boxplot(data=relievers, x='Role', y='IP_per_G', order=role_order, ax=axes[0, 0])
axes[0, 0].set_xlabel('Role', fontsize=11)
axes[0, 0].set_ylabel('Innings per Appearance', fontsize=11)
axes[0, 0].set_title('Workload by Reliever Role', fontsize=12)
axes[0, 0].tick_params(axis='x', rotation=45)

# Games pitched by role
sns.violinplot(data=relievers, x='Role', y='G', order=role_order, ax=axes[0, 1])
axes[0, 1].set_xlabel('Role', fontsize=11)
axes[0, 1].set_ylabel('Games Pitched', fontsize=11)
axes[0, 1].set_title('Appearance Frequency by Role', fontsize=12)
axes[0, 1].tick_params(axis='x', rotation=45)

# ERA by role
sns.boxplot(data=relievers, x='Role', y='ERA', order=role_order, ax=axes[1, 0])
axes[1, 0].set_xlabel('Role', fontsize=11)
axes[1, 0].set_ylabel('ERA', fontsize=11)
axes[1, 0].set_title('ERA by Reliever Role', fontsize=12)
axes[1, 0].tick_params(axis='x', rotation=45)
axes[1, 0].set_ylim(0, 6)

# K/9 vs BB/9 by role
for role, color in zip(role_order, ['red', 'blue', 'green', 'orange']):
    role_data = relievers[relievers['Role'] == role]
    axes[1, 1].scatter(role_data['BB/9'], role_data['K/9'],
                      label=role, alpha=0.6, s=80, color=color)
axes[1, 1].set_xlabel('BB/9', fontsize=11)
axes[1, 1].set_ylabel('K/9', fontsize=11)
axes[1, 1].set_title('Command and Strikeout Ability by Role', fontsize=12)
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('reliever_usage_patterns.png', dpi=300)
plt.show()

# High-leverage usage trends
print("\n\nHigh-Leverage Reliever Characteristics:")
high_leverage = relievers[
    ((relievers['SV'] >= 15) | (relievers['HLD'] >= 15)) &
    (relievers['ERA'] < 3.5)
]

print(f"\nElite High-Leverage Relievers (n={len(high_leverage)}):")
print(f"Average K/9: {high_leverage['K/9'].mean():.2f}")
print(f"Average BB/9: {high_leverage['BB/9'].mean():.2f}")
print(f"Average K/BB: {(high_leverage['SO'] / high_leverage['BB']).mean():.2f}")
print(f"Average FIP: {high_leverage['FIP'].mean():.2f}")
print(f"Average IP/G: {high_leverage['IP_per_G'].mean():.2f}")

R Code Examples

Comprehensive W/L/SV Analysis

library(baseballr)
library(dplyr)
library(ggplot2)
library(tidyr)

# Fetch pitching data for multiple seasons
seasons <- 2022:2024
all_pitching <- data.frame()

for (year in seasons) {
  data <- fg_pitch_leaders(
    startseason = year,
    endseason = year,
    qual = 100
  ) %>%
    mutate(Season = year)

  all_pitching <- bind_rows(all_pitching, data)
}

# Calculate win percentage and expected wins
all_pitching <- all_pitching %>%
  mutate(
    Win_Pct = W / (W + L),
    Expected_Win_Pct = 0.500 + (4.00 - FIP) / 10,
    Win_Luck = Win_Pct - Expected_Win_Pct,
    Games = W + L
  )

# Correlation analysis
cat("Correlation Between Wins and Performance Metrics\n")
cat("=" , rep("=", 59), "\n", sep="")

correlations <- all_pitching %>%
  select(W, ERA, FIP, xFIP, `K/9`, `BB/9`, `HR/9`, WAR) %>%
  cor(use = "complete.obs")

print(round(correlations[,"W"], 3))

# Win rates by FIP category
all_pitching <- all_pitching %>%
  mutate(
    FIP_Category = case_when(
      FIP < 3.0 ~ "Elite (<3.0)",
      FIP < 3.75 ~ "Above Avg (3.0-3.75)",
      FIP < 4.25 ~ "Average (3.75-4.25)",
      TRUE ~ "Below Avg (>4.25)"
    )
  )

fip_win_summary <- all_pitching %>%
  group_by(FIP_Category) %>%
  summarise(
    Count = n(),
    Avg_Wins = mean(W),
    Median_Wins = median(W),
    Avg_Win_Pct = mean(Win_Pct),
    Avg_ERA = mean(ERA),
    Avg_FIP = mean(FIP),
    Avg_WAR = mean(WAR)
  ) %>%
  arrange(Avg_FIP)

cat("\nWin Statistics by FIP Category:\n")
print(fip_win_summary, n = Inf)

# Identify lucky/unlucky pitchers
cat("\n\nMost Unlucky Pitchers (Good FIP, Poor Record):\n")
unlucky <- all_pitching %>%
  arrange(desc(Win_Luck)) %>%
  select(Name, Season, W, L, Win_Pct, ERA, FIP, WAR, Win_Luck) %>%
  head(10)
print(unlucky, n = 10)

cat("\nMost Lucky Pitchers (Poor FIP, Good Record):\n")
lucky <- all_pitching %>%
  arrange(Win_Luck) %>%
  select(Name, Season, W, L, Win_Pct, ERA, FIP, WAR, Win_Luck) %>%
  head(10)
print(lucky, n = 10)

# Visualization: Wins vs FIP with luck indicators
ggplot(all_pitching, aes(x = FIP, y = W)) +
  geom_point(aes(color = Win_Luck, size = WAR), alpha = 0.6) +
  geom_smooth(method = "lm", color = "red", linetype = "dashed", se = TRUE) +
  scale_color_gradient2(
    low = "#3498DB",
    mid = "#95A5A6",
    high = "#E74C3C",
    midpoint = 0,
    name = "Win Luck"
  ) +
  scale_size_continuous(name = "WAR", range = c(2, 10)) +
  labs(
    title = "Wins vs FIP: Identifying Luck (2022-2024)",
    subtitle = "Size = WAR, Color = Win % relative to FIP",
    x = "FIP (Fielding Independent Pitching)",
    y = "Wins"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 16, face = "bold"),
    plot.subtitle = element_text(size = 11),
    legend.position = "right"
  )

ggsave("wins_vs_fip_luck.png", width = 12, height = 8, dpi = 300)

# Win percentage distribution by FIP category
ggplot(all_pitching, aes(x = FIP_Category, y = Win_Pct, fill = FIP_Category)) +
  geom_boxplot(alpha = 0.7) +
  geom_jitter(width = 0.2, alpha = 0.3, size = 1) +
  scale_fill_brewer(palette = "RdYlGn", direction = -1) +
  labs(
    title = "Win Percentage Distribution by FIP Category",
    x = "FIP Category",
    y = "Win Percentage"
  ) +
  theme_minimal() +
  theme(
    legend.position = "none",
    axis.text.x = element_text(angle = 45, hjust = 1)
  )

ggsave("win_pct_by_fip_category.png", width = 10, height = 7, dpi = 300)

Save Opportunity Analysis in R

library(baseballr)
library(dplyr)
library(ggplot2)

# Get 2024 reliever data
relievers_2024 <- fg_pitch_leaders(
  startseason = 2024,
  endseason = 2024,
  qual = 0
) %>%
  filter(GS == 0, SV >= 5)  # Pure relievers with meaningful save totals

# Calculate save metrics
closers <- relievers_2024 %>%
  mutate(
    Save_Opp = SV + BS,
    Save_Pct = (SV / Save_Opp) * 100,
    K_BB_Ratio = SO / BB,
    Quality_Score = (Save_Pct * 0.4) + ((100 - ERA * 10) * 0.3) + (`K/9` * 2 * 0.3)
  ) %>%
  filter(Save_Opp >= 10)

# Summary statistics
cat("2024 Closer Performance Summary\n")
cat("=" , rep("=", 59), "\n", sep="")
cat(sprintf("Total Closers Analyzed: %d\n", nrow(closers)))
cat(sprintf("Average Save Percentage: %.1f%%\n", mean(closers$Save_Pct, na.rm = TRUE)))
cat(sprintf("Median Save Percentage: %.1f%%\n", median(closers$Save_Pct, na.rm = TRUE)))
cat(sprintf("Average Saves: %.1f\n", mean(closers$SV)))
cat(sprintf("Average Blown Saves: %.1f\n", mean(closers$BS)))

# Top closers by save percentage
cat("\n\nTop 15 Closers by Save Conversion Rate:\n")
top_closers <- closers %>%
  filter(Save_Opp >= 20) %>%
  arrange(desc(Save_Pct)) %>%
  select(Name, Team, SV, BS, Save_Opp, Save_Pct, ERA, FIP, `K/9`, `BB/9`) %>%
  head(15)

print(top_closers, n = 15)

# Blown save leaders
cat("\n\nClosers with Most Blown Saves:\n")
blown_saves <- closers %>%
  arrange(desc(BS)) %>%
  select(Name, Team, SV, BS, Save_Opp, Save_Pct, ERA, FIP) %>%
  head(10)

print(blown_saves, n = 10)

# Visualization: Save % vs ERA
ggplot(closers, aes(x = ERA, y = Save_Pct)) +
  geom_point(aes(size = Save_Opp, color = FIP), alpha = 0.7) +
  geom_smooth(method = "lm", se = TRUE, color = "red", linetype = "dashed") +
  scale_color_gradient(low = "#2ECC71", high = "#E74C3C", name = "FIP") +
  scale_size_continuous(name = "Save Opp", range = c(3, 12)) +
  labs(
    title = "Closer Effectiveness: Save Rate vs ERA (2024)",
    subtitle = "Size = Save Opportunities, Color = FIP",
    x = "ERA (Earned Run Average)",
    y = "Save Conversion Percentage"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(size = 15, face = "bold"))

ggsave("save_pct_vs_era.png", width = 11, height = 8, dpi = 300)

# Save opportunity distribution
ggplot(closers, aes(x = Save_Opp)) +
  geom_histogram(binwidth = 5, fill = "#3498DB", color = "black", alpha = 0.7) +
  geom_vline(aes(xintercept = mean(Save_Opp)),
             color = "red", linetype = "dashed", size = 1) +
  annotate("text", x = mean(closers$Save_Opp) + 5, y = Inf,
           label = sprintf("Mean: %.1f", mean(closers$Save_Opp)),
           vjust = 2, color = "red", size = 5) +
  labs(
    title = "Distribution of Save Opportunities (2024)",
    x = "Save Opportunities",
    y = "Number of Closers"
  ) +
  theme_minimal()

ggsave("save_opp_distribution.png", width = 10, height = 7, dpi = 300)

# Performance categories
closers <- closers %>%
  mutate(
    Performance = case_when(
      Save_Pct >= 90 ~ "Elite (90%+)",
      Save_Pct >= 80 ~ "Good (80-90%)",
      Save_Pct >= 70 ~ "Average (70-80%)",
      TRUE ~ "Struggling (<70%)"
    )
  )

performance_summary <- closers %>%
  group_by(Performance) %>%
  summarise(
    Count = n(),
    Avg_Save_Pct = mean(Save_Pct),
    Avg_ERA = mean(ERA),
    Avg_FIP = mean(FIP),
    Avg_K9 = mean(`K/9`),
    Avg_BB9 = mean(`BB/9`)
  )

cat("\n\nCloser Performance by Category:\n")
print(performance_summary, n = Inf)

Key Takeaways

  • Wins depend more on team quality than pitcher skill: Run support and bullpen strength heavily influence W-L records
  • The five-inning rule is arbitrary: Creates perverse incentives and fails to capture true effectiveness
  • Saves don't measure true impact: Ninth-inning bias and equal weighting of all saves obscure actual value
  • Modern metrics are superior: WAR, FIP, and WPA better isolate pitcher contributions
  • Bullpen specialization evolved dramatically: From firemen to closers to analytics-driven deployment
  • High-leverage situations matter most: Best relievers should pitch in highest-leverage spots, not just the ninth
  • Context matters for evaluation: Use traditional stats for historical context, advanced metrics for analysis
  • Save opportunities vary widely: Not all save situations are created equal in difficulty
  • Relief pitcher roles continue evolving: Analytics driving more flexible, matchup-based strategies
  • Combine multiple metrics: No single statistic tells the complete story of pitcher performance

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.