pythonforbio.
[WASM idle]
Plotting14/14

seaborn objects Interface

Starting sandbox…
Beginnerlesson

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= is color=; 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.Plot with plt. 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

scratch

No output yet — run the code to populate this drawer.

seaborn Themes and Palettesend of path