Comparing groups, such as A/B variants, regions or models, is one of the most common analysis tasks.
seaborn groups categorical plots into three families:
- Show every observation:
stripplot(jittered points) andswarmplot(non-overlapping points, good up to a few hundred per group). - Show the distribution:
boxplot(median, quartiles, whiskers at 1.5 IQR, outliers as points),violinplot(mirrored KDE) andboxenplot(more quantiles, for large data). - Show an estimate:
barplot(mean with a 95% confidence interval by default),pointplot(the same as points and lines, good for interactions) andcountplot(number of rows per category).
Put the category on x for vertical plots or on y for horizontal ones; seaborn infers the orientation. Control the order with order=[...], and split each category by a second variable with hue=, which dodges the groups side by side.
Layering is common: a box plot with a strip plot on top shows both summary and raw data. Pass the same ax to both.
A bar of the mean hides spread and sample size; prefer box, violin or points whenever the distribution matters. And barplot computes the mean itself: do not pass pre-aggregated values unless you also set errorbar=None, because a confidence interval from one value per group is meaningless.
In seaborn 0.13, passing palette= without hue= is deprecated; write hue="day", legend=False to color by the category.
Example
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
rng = np.random.default_rng(2)
df = pd.DataFrame({
"variant": np.repeat(["A", "B", "C"], 60),
"time_s": np.r_[rng.normal(30, 5, 60), rng.normal(26, 4, 60), rng.normal(31, 9, 60)],
})
fig, (a, b) = plt.subplots(1, 2, figsize=(9, 3.5), layout="constrained")
sns.boxplot(data=df, x="variant", y="time_s", hue="variant", legend=False,
fill=False, ax=a)
sns.stripplot(data=df, x="variant", y="time_s", color="0.3", alpha=0.5,
size=3, ax=a)
sns.barplot(data=df, x="variant", y="time_s", order=["B", "A", "C"], ax=b)
print(df.groupby("variant")["time_s"].mean().round(1).to_dict())
# {'A': 30.4, 'B': 25.9, 'C': 30.2}
print([t.get_text() for t in b.get_xticklabels()]) # ['B', 'A', 'C']
fig.savefig("variants.png")Remember
- Strip/swarm show raw points, box/violin show distributions, bar/point show estimates
- Use
order=andhue=to control and split categories - Layer a strip plot over a box plot to show summary and data together
Watch out for
- Passing already-averaged values to barplot, which then draws meaningless or missing error bars.