mrkeyoor.com_
Thu 06 Aug 15:43 UTC
PyPIDataupdated 06 Aug 2026

seaborn

seaborn is a layer on top of matplotlib for statistical charts. You hand it a tidy pandas DataFrame plus column names, and it does the parts that are tedious in raw matplotlib: splitting a series by a categorical column into colours, repeating the same chart once per group across a grid, computing and shading confidence intervals, choosing colour palettes that survive greyscale printing, and applying one coherent theme to everything. It also fits regressions, estimates kernel densities and draws correlation heatmaps in a single call. Everything it returns is still a matplotlib figure, so anything seaborn does not expose as a keyword argument you finish by hand in matplotlib afterwards.

Verdict

Still the quickest route from a DataFrame to a defensible statistical chart, and the documentation teaches you statistics rather than just listing arguments. The catch is stalled development: the newest release is from January 2024 and the newer objects interface has been unfinished since 2022, so build on the classic function API and expect no new features.

API stability4/5The function API has held steady since 0.11 apart from the 0.12 renames (ci= to errorbar=, FacetGrid.fig to .figure), both of which shipped with deprecation warnings. The objects interface is a separate story: the docs themselves call it under development.
Docs5/5seaborn.pydata.org is among the best documentation sites in Python data tooling. The tutorial teaches the underlying ideas (long-form data, figure-level versus axes-level, error bar semantics) instead of listing parameters, and every gallery thumbnail links to complete source. The objects interface pages are the one thin area.
Maintenance2/5No release since 0.13.2 in January 2024 despite the repo being pushed in July 2026, so merged fixes cannot be installed. 173 open issues (224 counting PRs) sit against what is effectively a single-maintainer project.
Ecosystem5/5About 8.4M downloads a week and the de facto statistical plotting library in Python courses, textbooks and notebooks, with a JOSS paper to cite. Every question has been asked, though pre-0.12 answers still use the retired ci= and .fig spellings.

Use it if

  • Your data is already a long-form DataFrame and you want colour-by-category, facet-by-category and error bars without writing the groupby and the loop over subplots yourself
  • You need statistical charts specifically: regression fits with confidence bands, KDEs and ECDFs, box and violin and strip plots, bootstrapped error bars, pair plots and correlation heatmaps
  • You want charts that look presentable by default; set_theme plus a named palette gets you further in one line than an hour of matplotlib rcParams
  • You are working in a notebook and iterating: one function call per chart, and the same call scales from a single axes to a faceted grid by swapping scatterplot for relplot
Skip it if

Setup reality

pip install seaborn brings numpy, pandas and matplotlib, with oddly specific exclusions (numpy 1.24.0 and matplotlib 3.6.1 are both blocked because of bugs in those exact releases). Anything statistical beyond the defaults sits behind an extra: KDE plots and regression confidence bands need scipy, and lowess smoothing plus the residual diagnostics need statsmodels, both installed with pip install seaborn[stats]. Missing them fails at draw time with an ImportError, not at import, so it can survive into production notebooks. The bigger cost is conceptual. Half the library is axes-level (scatterplot, histplot, boxplot) and accepts ax=; the other half is figure-level (relplot, displot, catplot, lmplot) and builds its own figure while returning a FacetGrid. They are not interchangeable, passing ax= to relplot raises, and the axes-level functions forward unknown keywords straight to matplotlib, so a stray col= produces a confusing matplotlib property error instead of a useful message. One last trap: sns.load_dataset() in every tutorial downloads CSVs from GitHub at call time and fails in an offline CI container.

Patterns

Apply a theme once for the whole sessionset-theme-and-palette

import seaborn as sns
import matplotlib.pyplot as plt

sns.set_theme(style="whitegrid", context="talk", palette="deep")

# named palettes and custom ones
sns.color_palette("crest", as_cmap=True)
sns.set_palette(["#1b9e77", "#d95f02", "#7570b3"])

plt.rcParams["figure.dpi"] = 120   # seaborn does not manage this

set_theme changes global matplotlib rcParams, so it affects plots drawn by other libraries in the same process too. The old sns.set() is a deprecated alias. context controls font and line scaling: paper, notebook, talk, poster.

Know which function makes the figurefigure-level-vs-axes-level

import matplotlib.pyplot as plt
import seaborn as sns

# axes-level: draws into an Axes you own
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
sns.scatterplot(data=df, x="flipper_mm", y="mass_g", hue="species", ax=axes[0])
sns.boxplot(data=df, x="species", y="mass_g", ax=axes[1])

# figure-level: owns the whole figure, returns a FacetGrid
g = sns.relplot(data=df, x="flipper_mm", y="mass_g", hue="species",
                col="island", kind="scatter", height=4)

relplot, displot, catplot, lmplot, pairplot and jointplot are figure-level and reject ax=. Their axes-level counterparts (scatterplot, histplot, boxplot, regplot) accept ax= but cannot facet. Choosing wrong is the single most common seaborn error.

Histograms, KDEs and ECDFsplot-distributions

sns.histplot(data=df, x="mass_g", hue="species",
             stat="density", common_norm=False, element="step", kde=True)

sns.displot(data=df, x="mass_g", col="island", kind="kde", fill=True)

sns.ecdfplot(data=df, x="mass_g", hue="species")

With hue and stat='density', the default common_norm=True normalises across all groups together, so a small group looks tiny; set common_norm=False to normalise each group separately. kde=True needs scipy, installed via seaborn[stats].

Compare groups with box, violin and bar plotscategorical-comparison

sns.boxplot(data=df, x="species", y="mass_g", hue="sex", fill=False)

sns.violinplot(data=df, x="species", y="mass_g", split=True, hue="sex", inner="quart")

sns.barplot(data=df, x="species", y="mass_g",
            estimator="median", errorbar=("pi", 95))

sns.catplot(data=df, x="species", y="mass_g", col="island", kind="box")

barplot aggregates with the mean by default, which surprises people who expect it to plot raw values. The ci= argument was replaced by errorbar= in 0.12: ('ci', 95) bootstraps a confidence interval, ('pi', 95) is a percentile interval, 'sd' is a standard deviation, and None turns error bars off and speeds things up considerably.

Fit and draw a regression lineregression-plot

sns.regplot(data=df, x="flipper_mm", y="mass_g", ci=95, scatter_kws={"alpha": 0.4})

sns.lmplot(data=df, x="flipper_mm", y="mass_g",
           hue="species", col="island", height=4)

sns.regplot(data=df, x="flipper_mm", y="mass_g", lowess=True)

sns.residplot(data=df, x="flipper_mm", y="mass_g")

These draw a fit but never report it: no coefficients, no p-value, no R-squared. If anyone will act on the number, fit it in statsmodels and plot that. lowess=True and residplot's higher-order options require statsmodels.

Heatmap of a correlation matrixcorrelation-heatmap

import numpy as np

corr = df.corr(numeric_only=True)
mask = np.triu(np.ones_like(corr, dtype=bool))

sns.heatmap(corr, mask=mask, annot=True, fmt=".2f",
            cmap="vlag", center=0, vmin=-1, vmax=1,
            square=True, linewidths=0.5)

Set center=0 with a diverging colormap or a correlation of 0.1 gets the same visual weight as -0.9. Without numeric_only=True, pandas raises on string columns. For clustered ordering use sns.clustermap, which is figure-level and returns its own grid.

Explore several variables at oncepair-and-joint-plots

sns.pairplot(df, vars=["flipper_mm", "mass_g", "bill_mm"],
             hue="species", diag_kind="kde", corner=True)

sns.jointplot(data=df, x="flipper_mm", y="mass_g",
              hue="species", kind="scatter", marginal_kws={"bins": 20})

sns.jointplot(data=df, x="flipper_mm", y="mass_g", kind="hex")

pairplot draws n-squared subplots, so pass vars= rather than letting it pick up thirty numeric columns and hang the kernel. corner=True halves the work by dropping the redundant upper triangle. kind='hex' does not accept hue.

Adjust titles, labels and legends on a FacetGridcustomise-facet-grid

g = sns.relplot(data=df, x="date", y="sales", col="region",
                col_wrap=3, kind="line", height=3, aspect=1.4)

g.set_axis_labels("Date", "Sales (USD)")
g.set_titles("{col_name}")
g.set(ylim=(0, None))
g.refline(y=target)
sns.move_legend(g, "upper left", bbox_to_anchor=(1.02, 1))

for ax in g.axes.flat:
    ax.tick_params(axis="x", rotation=45)

g.axes is a 2D numpy array of Axes unless you used col_wrap, in which case it is 1D; .axes.flat works for both. Reach the underlying figure with g.figure, since the old g.fig was deprecated in 0.12.

Get wide data into the shape seaborn wantsreshape-wide-to-long

long = wide.melt(
    id_vars=["date"],
    value_vars=["north", "south", "east"],
    var_name="region",
    value_name="sales",
)

sns.lineplot(data=long, x="date", y="sales", hue="region")

Passing a wide frame directly works for a few functions but throws away the column name as a variable, so you lose hue, col and the legend. Melt first and everything downstream becomes one keyword argument.

The newer declarative interfaceobjects-interface

import seaborn.objects as so

(
    so.Plot(df, x="flipper_mm", y="mass_g", color="species")
    .add(so.Dot(alpha=0.6))
    .add(so.Line(), so.PolyFit(order=1))
    .facet(col="island")
    .scale(color="deep")
    .label(x="Flipper (mm)", y="Mass (g)")
    .save("penguins.png", dpi=200, bbox_inches="tight")
)

This is the grammar-of-graphics rewrite added in 0.12 and the documentation still marks it as under development, with no stable release since. It coexists with the function API rather than replacing it, so mixing both in one codebase means two mental models.

Write a chart to a file correctlysave-figure

ax = sns.scatterplot(data=df, x="flipper_mm", y="mass_g")
ax.figure.savefig("scatter.png", dpi=200, bbox_inches="tight")

g = sns.relplot(data=df, x="flipper_mm", y="mass_g", col="island")
g.figure.savefig("facets.png", dpi=200, bbox_inches="tight")

import matplotlib.pyplot as plt
plt.close("all")   # in loops, or you leak figures

Axes-level functions return an Axes, so you save via ax.figure; figure-level ones return a grid whose figure is g.figure. Without bbox_inches='tight' an external legend gets cropped. In a loop, close figures or matplotlib warns after 20 and memory grows.

Finish the chart in matplotlibdrop-to-matplotlib

import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter

fig, ax = plt.subplots(figsize=(9, 5))
sns.lineplot(data=long, x="date", y="sales", hue="region", ax=ax)

ax.set_title("Monthly sales by region", loc="left", fontsize=14)
ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"${v:,.0f}"))
ax.axhline(target, ls="--", color="grey", lw=1)
ax.legend(title=None, frameon=False, ncols=3)
sns.despine(ax=ax)

This is the normal end state, not a failure: seaborn covers the statistics and the defaults, matplotlib covers everything else. Use the axes-level function when you know you will need this, because reaching into a FacetGrid's axes afterwards is more work.

Alternatives

PackageRegistryPick it when
matplotlibPyPIYou need control over every artist and do not want a wrapper deciding the layout for you.
plotlyPyPIThe chart ships in a web page or dashboard and needs hover, zoom and selection.
altairPyPIYou want a declarative grammar with interactivity for the web, backed by Vega-Lite.
plotninePyPIYou are coming from R and want a real ggplot2 grammar rather than seaborn's function-per-chart API.