seaborn objects Interface
In one line: seaborn's grammar-of-graphics API — compose a plot from marks, stats and scales instead of picking a named function.
Introduced in seaborn 0.12 (2022), still marked experimental as of 0.13.2. Worth knowing because it is the direction seaborn is heading, and because it expresses some plots much more cleanly than the classic API.
The idea
Instead of choosing a function (scatterplot, lineplot, boxplot), you declare a data-to-visual mapping and add layers:
import seaborn.objects as so
(
so.Plot(df, x="log2fc", y="neglog10p", color="significant")
.add(so.Dots(pointsize=3, alpha=0.5))
.label(x="log$_2$ fold change", y="$-$log$_{10}$ $p_{adj}$")
.theme({"axes.grid": True})
.save("volcano.pdf", bbox_inches="tight")
)
Anyone who has used ggplot2 will recognise this immediately. It is the same grammar: data + aesthetic mapping + geometric marks + statistical transforms + scales + facets.
Marks
so.Dot(); so.Dots() # points (Dots is the faster bulk version)
so.Line(); so.Lines()
so.Path() # line following data order, not sorted x
so.Area(); so.Band() # filled regions
so.Bar(); so.Bars()
so.Range() # error bars
so.Text()
Stats — transformations applied before drawing
so.Agg("mean") # aggregate y within x
so.Est("mean", errorbar="se") # aggregate + uncertainty
so.Hist(bins=50, stat="density")
so.KDE()
so.Count()
so.Perc([25, 50, 75])
so.PolyFit(order=2)
Moves — resolve overplotting
so.Dodge() # side by side
so.Jitter(0.3) # random offset
so.Stack() # stacked bars
so.Shift(x=0.1)
so.Dodge() + so.Jitter() # composable!
so.Dodge() + so.Jitter() composing is something the classic API cannot express — stripplot has dodge= and jitter= as separate flags with fixed interaction.
Layering
(
so.Plot(long, x="condition", y="expression", color="tissue")
.add(so.Dots(alpha=0.4), so.Jitter(0.2), so.Dodge()) # raw points
.add(so.Range(), so.Est("mean", errorbar="se"), so.Dodge()) # error bars
.add(so.Dot(pointsize=8), so.Agg("mean"), so.Dodge()) # means
.scale(color="colorblind")
.facet(col="gene", wrap=4)
.share(y=False)
.layout(size=(10, 6))
.label(y="log$_2$ CPM")
)
Each .add() is a layer with its own mark, stat and move. Building "points + mean + error bar, dodged by group, faceted by gene" is direct here and genuinely awkward in the classic API (three separate calls that must be given matching dodge and order arguments).
Scales
.scale(x="log", y="log")
.scale(color="viridis")
.scale(color=so.Continuous("rocket", norm=(0, 10)))
.scale(pointsize=so.Continuous().tick(at=[1, 10, 100]))
.scale(color=so.Nominal(["#4C72B0", "#DD8452"], order=["control", "treated"]))
Facets and pairs
.facet(col="gene", row="tissue", wrap=None)
.pair(x=["PC1", "PC2"], y=["PC3", "PC4"])
.share(x=True, y=False)
Output
p = so.Plot(...).add(...)
p.show()
p.save("out.pdf", bbox_inches="tight", dpi=300)
f = mpl.figure.Figure(figsize=(6, 4))
p.on(f).plot() # draw into an existing Figure
p.on(ax).plot() # or an existing Axes
.on() is how you compose an objects plot into a larger matplotlib layout — the equivalent of ax= in the classic API.
Classic vs objects
| Classic | objects | |
|---|---|---|
| Maturity | stable since 2012 | experimental since 2022 |
| Documentation, tutorials | abundant | growing |
| Learning curve | pick a function | learn the grammar |
| Common plots | one line | slightly more verbose |
| Layered / composite plots | awkward | natural |
| Consistency | some parameters differ per function | uniform |
| StackOverflow answers | thousands | few |
Recommendation for now: learn the classic API first — it is what tutorials, colleagues and error messages assume. Reach for objects when you hit a plot the classic API makes painful, or if you are coming from ggplot2 and the grammar is already in your head.
Bioinformatics examples
# expression by condition, faceted, with points and means
(
so.Plot(long, x="condition", y="log_cpm", color="tissue")
.add(so.Dots(alpha=0.3), so.Jitter(0.25), so.Dodge())
.add(so.Range(), so.Est("mean", errorbar="ci"), so.Dodge())
.facet(col="gene", wrap=4)
.share(y=False)
.label(y="log$_2$ CPM", x="")
)
# dose-response with a fitted curve over the raw points
(
so.Plot(dose, x="conc", y="viability", color="cell_line")
.add(so.Dots(alpha=0.5))
.add(so.Line(), so.PolyFit(order=3))
.scale(x="log")
.label(x="Concentration (µM)", y="Viability")
)
# stacked variant consequences per sample
(
so.Plot(variants, x="sample", color="consequence")
.add(so.Bars(), so.Count(), so.Stack())
.scale(color="Set2")
.layout(size=(10, 4))
)
# distribution comparison, density-normalised
(
so.Plot(long, x="log_cpm", color="condition")
.add(so.Area(alpha=0.4), so.KDE())
.label(x="log$_2$ CPM")
)
The third example — stacked counts by category — is one line here and needs a manual crosstab plus DataFrame.plot(kind="bar", stacked=True) in the classic API.
Common mistakes
- Expecting classic parameters to work.
hue=iscolor=;figsize=is.layout(size=...). - Forgetting the API is still experimental. It can change between minor versions; pin seaborn if you rely on it.
- Mixing
so.Plotwithplt.calls. Use.on(ax)to compose. .add()order. Later layers draw on top; put raw points before summaries.- Searching for help. Most seaborn answers online are for the classic API.
See also
Seaborn · Figure-level vs Axes-level · Common Plot Types · Statistical Estimation in seaborn · Faceting