wRC+ (Weighted Runs Created Plus)
wRC+ Explained: The Ultimate Offensive Metric
Weighted Runs Created Plus (wRC+) has emerged as one of the most comprehensive offensive metrics in modern baseball analytics. Unlike traditional statistics, wRC+ measures a player's total offensive value in terms of runs created, scaled to league average (100) and adjusted for park effects.
What Does wRC+ Measure?
wRC+ quantifies offensive production by measuring how many runs a player creates through their offensive actions. It incorporates every offensive event—singles, doubles, triples, home runs, walks—and weights them according to their actual run value. The "+" means it's normalized to 100 as league average.
- wRC+ of 120: Player is 20% better than average
- wRC+ of 100: League average production
- wRC+ of 80: Player is 20% below average
The Foundation: wOBA
wRC+ is built on wOBA (Weighted On-Base Average), which assigns specific weights to each offensive event:
wOBA = (0.69×uBB + 0.72×HBP + 0.88×1B + 1.24×2B + 1.56×3B + 1.95×HR) / (AB + BB - IBB + SF + HBP)
wRC+ Calculation
The calculation involves converting wOBA to runs created, adjusting for park factors, then scaling to 100:
- Calculate wOBA using weighted offensive events
- Convert to wRC (weighted runs created)
- Adjust for park factors
- Scale to league average (100)
wRC+ Benchmarks
| wRC+ Range | Classification | Description |
|---|---|---|
| 160+ | Legendary | Historic season; MVP-caliber |
| 150-160 | Elite | Superstar level; top 5-10 in league |
| 140-150 | Excellent | All-Star caliber |
| 120-140 | Great | Above-average regular |
| 110-120 | Above Average | Quality hitter |
| 90-110 | Average | League-average production |
| 80-90 | Below Average | Subpar offensive performance |
| <80 | Poor | Significant offensive weakness |
Python Implementation
import pybaseball as pyb
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
pyb.cache.enable()
# Get 2024 batting statistics
batting_2024 = pyb.batting_stats(2024, qual=300)
# Top 10 by wRC+
print("Top 10 Players by wRC+ (2024)")
print("=" * 60)
top_wrc = batting_2024.nlargest(10, 'wRC+')[['Name', 'Team', 'PA', 'wOBA', 'wRC+']]
print(top_wrc.to_string(index=False))
# Summary statistics
print("\nwRC+ Summary Statistics:")
print(f"Mean: {batting_2024['wRC+'].mean():.1f}")
print(f"Median: {batting_2024['wRC+'].median():.1f}")
print(f"Std Dev: {batting_2024['wRC+'].std():.1f}")
# Categorize players
def categorize_wrc(wrc):
if wrc >= 160: return 'Legendary (160+)'
elif wrc >= 150: return 'Elite (150-160)'
elif wrc >= 140: return 'Excellent (140-150)'
elif wrc >= 120: return 'Great (120-140)'
elif wrc >= 110: return 'Above Avg (110-120)'
elif wrc >= 90: return 'Average (90-110)'
else: return 'Below Avg (<90)'
batting_2024['wRC+_Category'] = batting_2024['wRC+'].apply(categorize_wrc)
print("\nPlayer Distribution:")
print(batting_2024['wRC+_Category'].value_counts())
# Visualization: wRC+ distribution
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Histogram
axes[0].hist(batting_2024['wRC+'], bins=25, edgecolor='black', alpha=0.7)
axes[0].axvline(x=100, color='red', linestyle='--', linewidth=2, label='League Avg')
axes[0].set_xlabel('wRC+')
axes[0].set_ylabel('Frequency')
axes[0].set_title('Distribution of wRC+ (2024)')
axes[0].legend()
# wRC+ vs Batting Average
axes[1].scatter(batting_2024['AVG'], batting_2024['wRC+'], alpha=0.6)
axes[1].axhline(y=100, color='red', linestyle='--', alpha=0.5)
axes[1].set_xlabel('Batting Average')
axes[1].set_ylabel('wRC+')
axes[1].set_title('wRC+ vs Batting Average')
plt.tight_layout()
plt.savefig('wrc_plus_analysis.png', dpi=300)
plt.show()
# Correlation with traditional stats
print("\nCorrelation with Traditional Stats:")
for stat in ['AVG', 'OBP', 'SLG', 'OPS', 'HR']:
corr = batting_2024['wRC+'].corr(batting_2024[stat])
print(f"wRC+ vs {stat}: {corr:.3f}")
R Implementation
library(baseballr)
library(dplyr)
library(ggplot2)
# Get FanGraphs batting data
batting_2024 <- fg_batter_leaders(
startseason = 2024,
endseason = 2024,
qual = 300,
ind = 1
)
# Top performers by wRC+
cat("Top 10 Players by wRC+ (2024)\n")
top_performers <- batting_2024 %>%
arrange(desc(wRC_plus)) %>%
select(Name, Team, PA, wOBA, wRC_plus) %>%
head(10)
print(top_performers)
# Summary statistics
cat("\nwRC+ Summary Statistics:\n")
cat(sprintf("Mean: %.1f\n", mean(batting_2024$wRC_plus, na.rm = TRUE)))
cat(sprintf("Median: %.1f\n", median(batting_2024$wRC_plus, na.rm = TRUE)))
# Categorize players
batting_2024 <- batting_2024 %>%
mutate(
wRC_category = case_when(
wRC_plus >= 160 ~ "Legendary (160+)",
wRC_plus >= 150 ~ "Elite (150-160)",
wRC_plus >= 140 ~ "Excellent (140-150)",
wRC_plus >= 120 ~ "Great (120-140)",
wRC_plus >= 110 ~ "Above Avg (110-120)",
wRC_plus >= 90 ~ "Average (90-110)",
TRUE ~ "Below Avg (<90)"
)
)
# Distribution visualization
ggplot(batting_2024, aes(x = wRC_plus)) +
geom_histogram(bins = 25, fill = "steelblue", color = "black", alpha = 0.7) +
geom_vline(xintercept = 100, color = "red", linetype = "dashed", size = 1) +
labs(
title = "Distribution of wRC+ (2024 Season)",
x = "wRC+",
y = "Count"
) +
theme_minimal()
ggsave("wrc_distribution.png", width = 10, height = 6, dpi = 300)
# wRC+ vs traditional stats
ggplot(batting_2024, aes(x = AVG, y = wRC_plus)) +
geom_point(alpha = 0.6, size = 3, color = "steelblue") +
geom_hline(yintercept = 100, color = "red", linetype = "dashed") +
geom_smooth(method = "lm", se = TRUE, color = "darkgreen") +
labs(
title = "wRC+ vs Batting Average (2024)",
x = "Batting Average",
y = "wRC+"
) +
theme_minimal()
# Correlation analysis
correlations <- batting_2024 %>%
summarise(
AVG = cor(wRC_plus, AVG, use = "complete.obs"),
OBP = cor(wRC_plus, OBP, use = "complete.obs"),
SLG = cor(wRC_plus, SLG, use = "complete.obs"),
OPS = cor(wRC_plus, OPS, use = "complete.obs")
)
print(correlations)
wRC+ vs Traditional Statistics
Traditional stats have significant limitations that wRC+ addresses:
- Batting Average: Treats all hits equally; ignores walks
- OBP: Better, but still treats all hits equally
- SLG: Weights hits by bases, but ignores walks entirely
- OPS: Combines OBP and SLG, but doesn't adjust for park/league
- RBIs: Heavily dependent on teammates and lineup position
wRC+ addresses all these issues by properly weighting events, including walks, and adjusting for context.
Key Takeaways
- 100 is average: Instant interpretation - points above/below = percentage difference
- Park-adjusted: Fair comparisons across all ballparks
- Era-adjusted: Compare players across different offensive environments
- Comprehensive: Includes all offensive contributions weighted by run value
- Best single offensive metric: Captures total offensive value better than any traditional stat