Same wOBA, Different Story

Padres
Mariners
Mets
Author

Oliver Chang

Published

August 3, 2025

Introduction: The Beauty of Baseball’s Numbers

Manny Machado, Juan Soto, and Eugenio Suarez are three all-star caliber baseball players. The trio consistently set baseball discourse ablaze. But there’s one question in baseball circles that has been debated for years: “Who is better?”

Difference between .300 and .275 hitter is one hit every two weeks. If you see both 15 games a year, there’s a 40% chance that the .275 hitter will have more hits than the .300 hitter. - Moneyball, Michael Lewis

Sure we can all watch them play, but baseball is a game of numbers. We cannot rely on our eyes, heuristics, or biases. In following the MLB trade deadline on Twitter, I saw a tweet from Jeff Passan that described Soto, Suarez, and Machado in three different aspects.

To summarize, Passan describes three metrics: batting average (BA), on-base percentage (OBP), and slugging (SLG). Passan shares the Moneyball principle in that modern statistics have turn to other statistics to evaluate a hitter; old baseball viewed walks as a “lesser” hit. Each of these metrics tells a different story about the players. BA is the simplest, describing how often a player gets a hit. OBP is the percentage of times a player reaches base, which includes hits, walks, and hit by pitches. Slugging is the total number of bases a player gets per at-bat, which includes singles, doubles, triples, and home runs - its like batting average but weighted by the type of hit.

Following the definitions, Passan shares slashlines for Machado, Soto, and Suarez. A slashline is a shorthand way to describe a player’s hitting statistics, typically in the format BA/OBP/SLG.

  • Machado: .301/.360/.505
  • Soto: .247/.380/.480
  • Suarez: .245/.316/.565

Machado is described as the best “all-around hitter” of the three, with the highest batting average. Soto is praised for his exceptional on-base skills. He is also in the 100th percentile for walk rate. Suarez is noted for his power-hitting ability.

Code
import plotly.graph_objects as go
import pandas as pd

image_urls = [
    "https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_426,q_auto:best/v1/people/592518/headshot/67/current", # Machado
    "https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_426,q_auto:best/v1/people/665742/headshot/67/current", # Soto
    "https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_426,q_auto:best/v1/people/553993/headshot/67/current"  # Suárez
]

# Data for the three players
data = {
    'Player': ['Manny Machado', 'Juan Soto', 'Eugenio Suárez'],
    'BA': [0.301, 0.247, 0.245],
    'OBP': [0.360, 0.380, 0.316],
    'SLG': [0.505, 0.480, 0.565]
}

df = pd.DataFrame(data)

# Create the figure
fig = go.Figure()

# Add a bar trace for each statistic
fig.add_trace(go.Bar(
    x=df['Player'],
    y=df['BA'],
    name='Batting Avg (BA)',
    marker_color='rgba(54, 162, 235, 0.7)',
    hovertemplate='%{y:.3f}<extra></extra>'
))

fig.add_trace(go.Bar(
    x=df['Player'],
    y=df['OBP'],
    name='On-Base % (OBP)',
    marker_color='rgba(75, 192, 192, 0.7)',
    hovertemplate='%{y:.3f}<extra></extra>'
))

fig.add_trace(go.Bar(
    x=df['Player'],
    y=df['SLG'],
    name='Slugging % (SLG)',
    marker_color='rgba(255, 99, 132, 0.7)',
    hovertemplate='%{y:.3f}<extra></extra>'
))

# --- Add Headshot Images ---
for i, player in enumerate(df['Player']):
    fig.add_layout_image(
        dict(
            source=image_urls[i],
            xref="x",
            yref="paper",
            x=player,  # Use player name to anchor on x-axis
            y=-0.30,    # Position below the x-axis
            sizex=0.25, # Size of the image
            sizey=0.25,
            xanchor="center",
            yanchor="bottom"
        )
    )

# Update the layout for a clean, grouped bar chart
fig.update_layout(
    barmode='group',
    title={
        'text': '<b>Player "Slash Line" Comparison</b>',
        'y':0.9,
        'x':0.5,
        'xanchor': 'center',
        'yanchor': 'top'
    },
    xaxis_title='Player',
    yaxis_title='Rate',
    yaxis_range=[0, 0.6],
    legend_title_text='Statistic:',
    legend=dict(
        orientation="h",
        yanchor="bottom",
        xanchor="left",
        y=1.02,
        x=0.2,
    ),
    template='plotly_white',
    font=dict(
        family="Inter, sans-serif",
        size=12
    ),
    margin=dict(b=120) # Increase bottom margin to make space for images
)


# Show the figure
fig.show()

What is wOBA?

What I found most interesting is that all three players have the same wOBA (weighted on-base average) of .370 (or .369 for Soto, to be precise). If they have the same overall grade, how are they so different? And what can a deeper dive into the numbers tell us about who might be the “best” of the three?

wOBA is a more advanced metric that combines a player’s ability to get on base and hit for power into a single number. It assigns different weights to different types of hits and events, providing a more comprehensive measure of a player’s offensive contribution. Weights for wOBA are derived from the run expectancy of each event, making it a more accurate reflection of a player’s value than traditional stats like BA, OBP, or SLG.

You can think of wOBA as a GPA-like metric for hitters. A single is a C, a double is a B, a home run is an A, a walk is a B-, and a strikeout is an F. wOBA averages all those grades together. The grades are scaled by how much each event contributes to scoring runs.

Below is the formula for wOBA in 2025 per FanGraphs:

Season wOBA wOBAScale wBB wHBP w1B w2B w3B wHR runSB runCS R/PA R/W cFIP
2025 .314 1.239 .692 .723 .884 1.256 1.591 2.048 .200 -.406 .117 9.710 3.102

Looking at doubles (w2B) we see that they are worth 1.256 runs, while home runs (wHR) are worth 2.048 runs. Walks (wBB) are worth .692 runs. This means that a player who hits a lot of doubles and home runs will have a higher wOBA than a player who hits a lot of singles, even if they have the same batting average.

Going Deeper: xwOBA (The “For the Nerds” Section)

xwOBA (expected weighted on-base average) is a metric that estimates what a player’s wOBA should be based on the quality of their contact, strikeouts, and walks. It uses Statcast data to analyze the exit velocity and launch angle of batted balls, as well as the player’s plate discipline. According to MLB, xwOBA is more “indicative” of a player’s true talent level than wOBA, which can be influenced by luck.

For example, Andrew Vaughn has a wOBA of .289 but an xwOBA of .325. This suggests that Vaughn has been unlucky and should be hitting better than he has been. I actually think the Brewers made a brilliant move trading for Vaughn, given his expected statistics and age (27).

Put it simply, if

  • wOBA > xwOBA, the player has been lucky. Bloop hits, seeing-eye singles, or defensive misplays are contributing to their success. They might be due for some regression.
  • wOBA < xwOBA, the player has been unlucky. They are hitting the ball hard, but right at defenders. They are “barreling up” the ball without the results to show for it. They could be due for a hot streak.
Note

Checkout my previous post on lucky (and unlucky) hitters!

Analyzing Our Three Players with xwOBA

Here are the current xwOBA values for our three players:

  • Machado: .394 (94th percentile)
  • Soto: .438 (100th percentile)
  • Suarez: .346 (66th percentile)

Machado and Soto are both outperforming their wOBA, suggesting that they are hitting the ball with authority and their numbers should be better than they are. Suarez is underperforming his wOBA, suggesting that the quality of contact might not sustain this exact level of production.

Code
import plotly.graph_objects as go
import pandas as pd

# --- Data Setup ---
data = {
    'Player': ['Manny Machado', 'Juan Soto', 'Eugenio Suárez'],
    'wOBA': [0.370, 0.369, 0.370],
    'xwOBA': [0.394, 0.438, 0.346]
}
df = pd.DataFrame(data)

# --- Player Headshot URLs ---
# Using official MLB headshot images for better quality
image_urls = [
    "https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_426,q_auto:best/v1/people/592518/headshot/67/current", # Machado
    "https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_426,q_auto:best/v1/people/665742/headshot/67/current", # Soto
    "https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_426,q_auto:best/v1/people/553993/headshot/67/current"  # Suárez
]


# --- Chart Creation ---
fig = go.Figure()

# Add bar traces
fig.add_trace(go.Bar(
    x=df['Player'], y=df['wOBA'], name='wOBA (Actual)',
    marker_color='rgba(54, 162, 235, 0.7)',
    hovertemplate='%{y:.3f}<extra></extra>'
))
fig.add_trace(go.Bar(
    x=df['Player'], y=df['xwOBA'], name='xwOBA (Expected)',
    marker_color='rgba(255, 159, 64, 0.7)',
    hovertemplate='%{y:.3f}<extra></extra>'
))

# --- Add Headshot Images ---
for i, player in enumerate(df['Player']):
    fig.add_layout_image(
        dict(
            source=image_urls[i],
            xref="x",
            yref="paper",
            x=player,  # Use player name to anchor on x-axis
            y=-0.28,    # Position below the x-axis
            sizex=0.27, # Size of the image
            sizey=0.27,
            xanchor="center",
            yanchor="bottom"
        )
    )

# --- Update Layout ---
fig.update_layout(
    barmode='group',
    title={
        'text': '<b>wOBA vs. xwOBA Comparison</b>',
        'y':0.99, 'x':0.5, 'xanchor': 'center', 'yanchor': 'top'
    },
    xaxis_title=None,
    yaxis_title='Value',
    yaxis_range=[0.0, 0.45],
    # legend=dict(
    #     orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5
    # ),
    template='plotly_white',
    font=dict(family="Inter, sans-serif", size=12),
    margin=dict(b=120) # Increase bottom margin to make space for images
)

fig.show()

Conclusion: Who do you want at the plate?

From Passan’s tweet, we saw three different types of hitters. While they excely in different metrics, they all share the same wOBA. We then investigated xwOBA, which gives us a deeper understanding of their performance. Based on xwOBA, Soto is the best hitter of the three, followed by Machado, and then Suarez.

So in the end, the number was a mirage. A tidy .370 wOBA that made three very different men look like statistical clones. But baseball, like any market, is filled with inefficiencies for those who know where to look. And looking at xwOBA was like looking at the audited financials instead of the press release. It revealed that one of these hitters was getting lucky, another was earning his keep, and the third, Juan Soto, was the undervalued asset — despite already being a superstar. The scoreboard showed a tie, but the underlying physics of their batted balls told you who was really winning the game.

Peace ✌️