Wins, Losses, and Saves
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:
| Scenario | Innings | Result | Outcome |
|---|---|---|---|
| Starter A dominates | 4.2 IP, 0 ER | Team wins 2-0 | No win (reliever gets W) |
| Starter B struggles | 5.0 IP, 7 ER | Team wins 10-8 | Gets the win |
| Starter C perfect | 4.0 IP, 0 H, 0 BB | Team wins 1-0 | No 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 Size | Save Available? | Managerial Incentive |
|---|---|---|
| 1-3 runs | Yes | Use best reliever |
| 4+ runs | No (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:
| Factor | Correlation with Wins | Correlation with ERA |
|---|---|---|
| Team runs scored | 0.65 | 0.12 |
| Team defensive efficiency | 0.48 | 0.55 |
| Pitcher strikeout rate | 0.31 | -0.67 |
| Pitcher walk rate | -0.22 | 0.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:
| Situation | Difficulty | Save Earned? |
|---|---|---|
| 9th inning, 3-run lead, bases empty | Low | Yes |
| 9th inning, 1-run lead, bases loaded, 0 outs | Extreme | Yes |
| 8th inning, 1-run lead, bases loaded (gets 2 outs, leaves) | Very high | No (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 Value | Player Quality |
|---|---|
| 8+ WAR | MVP candidate |
| 5-8 WAR | All-Star |
| 2-5 WAR | Solid regular |
| 0-2 WAR | Role player |
| <0 WAR | Below 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):
| Situation | Leverage Index |
|---|---|
| 9th inning, tie game, bases loaded | 4.5 |
| 9th inning, 1-run lead, bases empty | 2.0 |
| 8th inning, tie game, runner on 2nd | 2.8 |
| 9th inning, 4-run lead | 0.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