datarekha

Seaborn — categorical plots

barplot, countplot, pointplot, swarmplot — what Seaborn computes for you, and which plot beats the others in each situation.

6 min read Beginner Storytelling with Visualisation Lesson 7 of 12

What you'll learn

  • `barplot` with the confidence intervals Seaborn computes by default
  • `countplot` for categorical counts (it's just a barplot with `count` baked in)
  • `pointplot` for comparing means across two categorical axes
  • `swarmplot` when every individual data point matters
  • The decision tree for picking one over the others

Before you start

The last lesson showed the spread of one variable. This one answers the flip side: when what you want across categories is a single statistic — a mean, a count, a rate — which plot computes it for you, and draws the uncertainty without a line of stats code?

You’ve got a categorical x-axis — plan tier, experiment arm, country — and you want to compare a number across it. Five plots cover almost every variant of this. Each one computes something different under the hood, and that’s exactly why picking the right one matters.

barplot — means with confidence intervals (the default)

The most-used categorical plot. Pass x (category) and y (continuous), and Seaborn computes the mean per category with bootstrap 95% confidence intervals drawn as error bars. You did not have to calculate either.

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

sns.set_theme(style="whitegrid", palette="colorblind")

# Mock A/B/C test — three arms, conversion rate per user
rng = np.random.default_rng(0)
arms = []
for arm, p in [("control", 0.085), ("variant_A", 0.102), ("variant_B", 0.094)]:
    convs = rng.binomial(1, p, size=4000)
    arms.append(pd.DataFrame({"arm": arm, "converted": convs}))
df = pd.concat(arms, ignore_index=True)

print(df.groupby("arm")["converted"].mean().round(4))

fig, ax = plt.subplots(figsize=(7, 4))
sns.barplot(data=df, x="arm", y="converted", errorbar=("ci", 95), ax=ax)
ax.set_ylabel("Conversion rate")
ax.set_title("A/B/C test — mean conversion + 95% CI")
fig.tight_layout()
plt.show()
arm
control      0.0802
variant_A    0.1022
variant_B    0.0948
Name: converted, dtype: float64
Bar chart of mean conversion rate for three A/B/C arms (control, variant_A, variant_B), each bar topped with a 95% bootstrap confidence-interval error bar. variant_A is highest at about 0.10.

One call: mean conversion per arm with 95% bootstrap CIs. variant_A (0.102) leads control (0.080); read the error bars before believing it.

Three things Seaborn just did for you:

  • Computed the mean of converted per arm (which is the conversion rate — mean of a 0/1 variable).
  • Bootstrapped (randomly resampled with replacement) the data 1 000 times by default to get a 95% confidence interval.
  • Drew them as error bars on top of the bars.

Without Seaborn, that’s groupby + statsmodels + several lines of matplotlib. With it, it’s one line.

errorbar — what to pass

Seaborn changed the API around this — modern Seaborn uses errorbar=:

sns.barplot(..., errorbar=("ci", 95))      # 95% bootstrap CI (default style)
sns.barplot(..., errorbar="sd")            # one standard deviation
sns.barplot(..., errorbar=("se", 1.96))    # 1.96 * standard error ≈ 95%
sns.barplot(..., errorbar=None)            # no error bars at all

Always state what your error bars mean in the figure caption. “Error bars are 95% bootstrap CI” is a one-line trust signal.

countplot — frequency of a category

When your y-axis is literally “count of rows,” reach for countplot. It’s a barplot with count baked in, so you only pass x (or y for horizontal bars).

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

sns.set_theme(style="whitegrid", palette="colorblind")
rng = np.random.default_rng(0)
df = pd.DataFrame({
    "plan": rng.choice(["Free", "Pro", "Business", "Enterprise"],
                       size=1500, p=[0.55, 0.28, 0.12, 0.05]),
    "region": rng.choice(["NA", "EU", "APAC"], size=1500, p=[0.5, 0.3, 0.2]),
})

fig, ax = plt.subplots(figsize=(8, 4))
sns.countplot(data=df, y="plan", hue="region",
              order=["Enterprise", "Business", "Pro", "Free"], ax=ax)
ax.set_title("Customers per plan, split by region")
ax.set_xlabel("Customer count")
fig.tight_layout()
plt.show()
Horizontal grouped bar chart counting customers per plan tier (Enterprise, Business, Pro, Free in that explicit order), each split into three region bars (NA, EU, APAC). Free is by far the largest tier.

countplot with order= (not alphabetical) and hue=“region” — a grouped bar chart for free.

Two patterns to internalize: order= lets you control category order explicitly (don’t trust default alphabetical), and hue= (maps a second categorical variable to color) adds a second split. Combined, you get a grouped bar chart for free.

pointplot — means as points with error bars

pointplot shows the same statistic as barplot (mean + CI) but as points connected by a line. Useful when you have two categorical axes and want to see the interaction.

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

sns.set_theme(style="whitegrid", palette="colorblind")

# Same A/B test, but split by region — does the lift vary?
rng = np.random.default_rng(1)
rows = []
base = {"control": 0.085, "variant_A": 0.102, "variant_B": 0.094}
region_mult = {"NA": 1.0, "EU": 1.1, "APAC": 0.9}
for arm, p in base.items():
    for region, mult in region_mult.items():
        convs = rng.binomial(1, p * mult, size=1500)
        for c in convs:
            rows.append((arm, region, c))
df = pd.DataFrame(rows, columns=["arm", "region", "converted"])

fig, ax = plt.subplots(figsize=(7.5, 4.5))
sns.pointplot(data=df, x="arm", y="converted", hue="region",
              errorbar=("ci", 95), dodge=0.25, ax=ax)
ax.set_ylabel("Conversion rate")
ax.set_title("A/B/C test — does the lift vary by region?")
fig.tight_layout()
plt.show()
Point plot of mean conversion across three arms (control, variant_A, variant_B) with three region series (NA, EU, APAC) connected by lines and dodged apart, each point carrying a 95% CI. EU sits highest, APAC lowest, and the lines stay roughly parallel.

pointplot with a hue — connected means across two categorical axes make an interaction (or its absence) jump out.

The lines connecting points across arms make it easy to see whether the pattern changes by region — that’s an interaction effect (when the impact of one variable depends on the level of another). dodge=0.25 nudges the three region series apart horizontally so their error bars don’t overlap.

swarmplot — every observation, no overlap

When you have a small-to-medium sample (say, 30 to a few hundred per category), nothing beats showing every point. swarmplot does that without stacking dots — it nudges them sideways to keep them visible.

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

sns.set_theme(style="whitegrid", palette="colorblind")
rng = np.random.default_rng(2)

# Latency numbers from three deployment configs, 60 requests each
df = pd.DataFrame({
    "config": np.repeat(["baseline", "tuned", "tuned+cache"], 60),
    "latency_ms": np.concatenate([
        rng.normal(180, 25, 60),
        rng.normal(150, 22, 60),
        rng.normal(110, 18, 60),
    ]),
})

fig, ax = plt.subplots(figsize=(7, 4.5))
sns.boxplot(data=df, x="config", y="latency_ms",
            color="white", showfliers=False, width=0.4, ax=ax)
sns.swarmplot(data=df, x="config", y="latency_ms",
              size=4, ax=ax)
ax.set_ylabel("Latency (ms)")
ax.set_title("Request latency by config — boxplot + swarmplot overlay")
fig.tight_layout()
plt.show()
Three configs (baseline, tuned, tuned+cache) with a white boxplot summary overlaid by a swarm of every individual latency point. Latency drops steadily from baseline (~180ms) to tuned+cache (~110ms), with all 60 points per config visible and non-overlapping.

Boxplot summary + swarm of every point — the “trust me, nothing’s hidden” chart for small experiments.

The boxplot gives you the summary; the swarm gives you the actual data. This is the gold-standard “trust me” chart for small experiments — nothing is hidden, the outliers are visible, the median is visible.

For larger samples (n > 500 per category), swarmplot gets unhappy because it can’t fit all the dots; use stripplot (no nudging, just jitter) or fall back to violin/box.

Decision tree

QuestionPlot
Mean per category, with uncertaintybarplot
Counts of each categorycountplot
Mean per category, two categorical axes (look for interactions)pointplot
Show every single observation, small/medium nswarmplot
Compare full distribution shape across categoriesviolinplot
Compare summary distribution (quartiles, outliers) across categoriesboxplot

Same data, five views — which one hides the story?

Bar: comparing means at a glance
406080100A47B70C65D95
Group AGroup BGroup C bimodalGroup Dy = latency (ms)
Notice: Groups B and C have nearly the same bar height (~65 ms). Switch to Violin or Swarm — Group C is actually bimodal, split between ~44 ms and ~85 ms. The bar's mean hid a two-cluster pattern entirely.

In one breath

When you compare a statistic across categories, Seaborn computes it for you. barplot(x, y) draws the mean per category with a 95% bootstrap CI as error bars — for a 0/1 column that mean is the conversion rate, all in one line (state what the error bars mean via errorbar=). countplot is a barplot with count baked in — pass order= (never trust alphabetical) and hue= for a grouped chart. pointplot shows the same mean+CI as connected points, ideal for spotting an interaction across two categorical axes (dodge= separates the series). swarmplot shows every observation without overlap — the honest small-experiment chart — but switch to stripplot or a violin past a few hundred points per category. Read the error bars before believing any gap.

Practice

Quick check

0/3
Q1You call `sns.barplot(data=df, x='arm', y='converted')` on a 0/1 outcome with no other arguments. What does the bar height represent?
Q2When does `swarmplot` stop working well?
Q3You want to compare conversion rate across three experiment arms AND three regions on one plot. Which is the most natural choice?

A question to carry forward

Look at the pointplot — we crossed two categorical axes (arm × region) and read off whether the pattern held. That worked for three arms and three regions. But what if you had twelve features and wanted every pairwise correlation, or a dense grid of values where colour, not bar height, has to carry the number? Bars and points would drown.

So the question to carry forward is: how do you visualise a whole matrix at once — a correlation table, a confusion matrix, two categoricals crossed into a grid of values? The next lesson, heatmaps and pairplot, encodes magnitude as colour so an entire matrix reads in one glance, and pairplot plots every variable against every other in a single command — the fastest first look at an unfamiliar dataset.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
What is the difference between a histogram, a bar chart, and a KDE plot, and when do you use each?

A bar chart displays counts or aggregates for distinct categories separated by gaps; a histogram displays the distribution of a single continuous variable by dividing it into adjacent bins with no gap; a KDE (kernel density estimate) plot is a smoothed, continuous approximation of the same distribution without requiring a bin-width choice.

How do you choose the right chart type for a given analytical question?

Match the chart to the relationship in the data: comparison across categories calls for bars, trends over continuous time call for lines, correlation between two numeric variables calls for a scatter plot, and distribution shape calls for a histogram or box plot. The question you are answering — not aesthetics — drives the choice.

When should you avoid a pie chart, and what should you use instead?

Pie charts work only when you have two to three parts whose proportions differ substantially and sum to a meaningful whole. Beyond that, humans compare angles and arc lengths poorly, making slices of similar size indistinguishable. A sorted bar chart almost always communicates the same information more accurately.

Compare Parquet, CSV, and Avro as big-data file formats — when do you use each?

Parquet is a columnar, compressed format optimized for analytical reads — only the queried columns are scanned. Avro is row-oriented, schema-embedded, and optimized for write-heavy pipelines and Kafka serialization. CSV is human-readable but schema-less, uncompressed, and slow at scale — use it only at system boundaries where a downstream tool requires it.

Related lessons

Explore further

Skip to content