Stacked Bar Charts: Win-Loss Composition

BaseballBeginnerPython~5 min read

What you'll build

Wins and losses stacked into a single bar per team to show composition.

Wins and losses stacked into a single bar per team to show composition.
Data: Bundled sample (2023 MLB standings), retrieved June 2026

Here's the catch nobody mentions: a stacked bar chart is great until it quietly lies to you, and most tutorials never show you the difference. A plain bar compares totals; a stacked bar shows what those totals are made of — stack a team's wins and losses into one bar and the bar's length is games played, while the split inside shows the record. We'll build a win-loss stacked bar for the best teams of 2023, then pin down the one situation where stacking genuinely helps and the one where it misleads.

This builds on Your First Visualization. The data is the bundled sample_standings.csv (real 2023 MLB standings), so it runs offline.

  1. The data: two parts of one whole

    Each team's season splits cleanly into wins and losses that sum to games played — the natural shape for a stacked bar.

    python
    import pandas as pd
    
    df = pd.read_csv("sample_standings.csv")
    print(df.sort_values("W", ascending=False)[["Team", "W", "L", "G"]].head(6).to_string())
    Wins, losses, and games
          Team    W   L    G
    0   Braves  104  58  162
    1  Orioles  101  61  162
    2  Dodgers  100  62  162
    3     Rays   99  63  162
    4  Brewers   92  70  162
    5   Astros   90  72  162

    Every team played 162 games, so the Braves' 104–58 and the Dodgers' 100–62 will produce bars of identical length — what differs is where the wins-to-losses line falls inside each.

  2. Stack with the left argument

    Stacking is just drawing the second series starting where the first ends. For a horizontal chart, that means giving the losses a left= equal to the wins, so they begin at the end of the win segment.

    python
    import matplotlib.pyplot as plt
    
    top = df.sort_values("W", ascending=False).head(10).sort_values("W")
    fig, ax = plt.subplots(figsize=(8, 6))
    ax.barh(top["Abbr"], top["W"], label="wins")
    ax.barh(top["Abbr"], top["L"], left=top["W"], label="losses")   # stack starts at W
    ax.set_xlabel("games"); ax.legend(loc="lower right")
    fig.savefig("win_loss_stacked.png", dpi=144, bbox_inches="tight")
    Horizontal stacked bar chart of the top 10 MLB teams' win-loss records in 2023, each bar the same 162-game length split into a wins segment and a losses segment
    Data: Bundled sample (2023 MLB standings), retrieved June 2026

    For a vertical bar chart the same idea uses bottom= instead of left=. Either way, the rule is: each layer starts where the stack so far ends. Here every bar reaches 162, and the eye reads each team's record off where the color changes.

  3. When stacking helps — and when it lies

    Stacking shines when the parts genuinely sum to a meaningful whole and you care about composition: win-loss out of games, or scoring broken into singles/doubles/etc. It works because the first segment shares a common baseline (zero), so those segments are easy to compare. The trap is the upper segments: because losses all start at different points, comparing loss totals by eye is hard — your eye has to subtract. If the losses were the comparison you cared about, a grouped bar (or just plotting losses alone) would be honest where a stack is not. Choose the chart for the comparison you want the reader to make.

Troubleshooting

The two series overlap instead of stacking

You forgot the offset. The second barh needs left=top["W"] (or bottom= for vertical bars) so it starts at the end of the first. Without it, both draw from zero and the shorter one just hides in front.

My stack is in the wrong order or misaligned

Both layers must use the same categories in the same order. Sort once into top and pass top["Abbr"] to both calls — don't re-sort between them, or the wins and losses will belong to different teams.

Three-plus segments — how do I keep stacking?

Keep a running total. For each new segment, set left (or bottom) to the sum of all previous segments — e.g., left = seg1 + seg2 for the third. A small running-sum variable updated in a loop handles any number of layers.

Challenge yourself

Make a 100% stacked version: divide each segment by games played so every bar runs 0–1 and shows win percentage rather than counts — better when teams have played different numbers of games. Then add a vertical line at 0.5 to mark a winning record at a glance.

Get the code

Here's the complete, working script for this tutorial. It runs exactly as shown.

Download the finished script (52_stacked_bar_win_loss.py)

This script imports a small shared helper (and reads any bundled sample data) that live next to it in /downloads/ — grab these into the same folder so it runs as-is: sdt_common.py.

More Baseball tutorials

Your first real Statcast pull, cached, with an exit-velocity histogram.
Baseball Beginner

Pull Your First MLB Data with pybaseball

Install pybaseball, turn on caching, and pull a week of real Statcast data. End with a histogram of batted-ball exit velocity so you can see the data is genuinely there.

~8 min
A pitch-location heatmap for one pitcher with the strike zone drawn on top.
Baseball Intermediate

Make a Pitch-Location Heatmap in Python

Use a single pitcher's Statcast data to build a 2-D location heatmap, draw the strike zone from the catcher's view, and read what the hot spots tell you.

~8 min