wRC+ (Weighted Runs Created Plus)

Intermediate 10 min read 0 views Nov 26, 2025

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:

  1. Calculate wOBA using weighted offensive events
  2. Convert to wRC (weighted runs created)
  3. Adjust for park factors
  4. Scale to league average (100)

wRC+ Benchmarks

wRC+ RangeClassificationDescription
160+LegendaryHistoric season; MVP-caliber
150-160EliteSuperstar level; top 5-10 in league
140-150ExcellentAll-Star caliber
120-140GreatAbove-average regular
110-120Above AverageQuality hitter
90-110AverageLeague-average production
80-90Below AverageSubpar offensive performance
<80PoorSignificant 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

Discussion

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