Seaborn
In one line: statistical plots from tidy dataframes in one call, drawn by matplotlib underneath.
Version at time of writing: 0.13.2 · import seaborn as sns
The mental model
Seaborn is a dataset-oriented interface to Matplotlib. You do not tell it what to draw; you tell it which columns map to which visual channels, and it works out the rest — including the statistical transformation, the legend, the palette, and the axis labels.
sns.scatterplot(data=df, x="log2fc", y="neglog10p", hue="significant", size="baseMean")
Three things happened there that would have been 15 lines of matplotlib: the hue column was mapped to colours and a legend was built, the size column was mapped to marker areas with its own legend, and both axes were labelled from the column names.
The price of that convenience: your data must be tidy (long form). Seaborn's contract is "one row per observation, one column per variable". If your data is wide, melt it first.
The critical distinction
Seaborn has two kinds of function and confusing them causes most seaborn frustration:
| Axes-level | Figure-level | |
|---|---|---|
| Examples | scatterplot, boxplot, histplot, lineplot, heatmap |
relplot, catplot, displot, lmplot, pairplot, jointplot, clustermap |
| Draws into | an existing Axes you pass via ax= |
a Figure it creates and owns |
| Returns | the Axes |
a FacetGrid / JointGrid / ClusterGrid |
Accepts ax= |
yes | no — passing it is an error |
| Sizing | figsize on your plt.subplots |
height= and aspect= |
| Can facet | no | yes, via col= / row= |
Rule of thumb: axes-level when the plot is one panel in a figure you are composing; figure-level when you want faceting. See Figure-level vs Axes-level.
Why bioinformatics cares
The plots you make constantly — grouped boxplots of expression by condition, distributions split by batch, correlation heatmaps, faceted small multiples across tissues, clustermaps of expression — are one-liners in seaborn and half-page functions in raw matplotlib. clustermap in particular does hierarchical clustering, dendrograms and the heatmap in a single call.
Seaborn also does the statistical work you would otherwise get subtly wrong: bootstrap confidence intervals on aggregated lines, kernel density estimates with sensible bandwidth, and regression fits with CI bands.
Core notes
Figure-level vs Axes-level · Statistical Estimation in seaborn · Faceting · seaborn Themes and Palettes · seaborn objects Interface
Because seaborn is matplotlib, these matter too: Figure and Axes · Colormaps and Color · Saving Figures · Axes Styling and Annotation
Applied
Applied - Heatmaps and Clustermaps · Applied - Differential Expression Volcano Plot · Applied - Quality Control Plots
Install and check
pip install "seaborn>=0.13,<0.14"
python -c "import seaborn as sns; print(sns.__version__)"
Seaborn releases slowly — 0.13.2 dates from January 2024 and is still current in 2026. That is stability, not abandonment, but it does mean it occasionally lags pandas/matplotlib changes.
The 20% that gets 80% of the work done
import seaborn as sns
sns.set_theme(style="whitegrid", context="paper", palette="colorblind")
sns.boxplot(data=long, x="condition", y="expression", hue="tissue")
sns.histplot(data=df, x="af", bins=50, log_scale=(True, False))
sns.scatterplot(data=df, x="pc1", y="pc2", hue="batch", style="sex")
sns.heatmap(corr, cmap="vlag", center=0, square=True)
sns.clustermap(mat, z_score=0, cmap="vlag", figsize=(8, 10))
g = sns.relplot(data=long, x="dose", y="response", col="gene", col_wrap=4, kind="line")
g.set_axis_labels("Dose (µM)", "Response")
g.savefig("facets.pdf")
sns.set_theme() once at the top of a script is the cheapest way to make everything — including your raw matplotlib plots — look consistent.
Reaching through to matplotlib
This is the workflow that makes seaborn genuinely powerful:
fig, ax = plt.subplots(figsize=(5, 4))
sns.scatterplot(data=df, x="log2fc", y="neglog10p", ax=ax)
ax.axvline(0, color="grey", lw=0.8) # matplotlib
for _, r in top_hits.iterrows(): # matplotlib
ax.annotate(r["gene"], (r["log2fc"], r["neglog10p"]))
fig.savefig("volcano.pdf", bbox_inches="tight")
For figure-level functions the handles are g.figure, g.axes (an ndarray), and g.axes_dict (keyed by facet value).
Gotchas that bite newcomers
- Passing
ax=to a figure-level function →TypeError. Use the axes-level twin (relplot→scatterplot/lineplot). - Passing wide data and getting a nonsense plot. Melt first.
- Not realising the error bars are bootstrap 95% CIs by default, not SD or SEM. Set
errorbar=explicitly in anything you publish. See Statistical Estimation in seaborn. sns.heatmapdoes not cluster;sns.clustermapdoes.- KDE plots on bounded data (proportions, allele frequencies) bleed past 0 and 1. Use
histplotor setclip=.
See also
Matplotlib · Tidy Data · Pandas · Ecosystem Map