Scales, properties and custom stats

seaborn · Expert · 16 min

Explicit scales and custom transforms let the objects interface express plots no built-in function supports.

In seaborn.objects, every variable assigned to a property (x, y, color, pointsize, linewidth, marker, alpha ...) passes through a scale. .scale() controls each one:

  • so.Continuous() with .tick(...) and .label(like="{x:.0%}"), or a transform string: .scale(y="log"), "symlog", "sqrt".
  • so.Nominal(order=[...]) for categories, including numbers you want treated as categories: .scale(color=so.Nominal(["#999", "C3"])).
  • so.Temporal() for dates, so.Boolean() for True/False.
  • Passing a palette name, list or dict maps values to colors directly: .scale(color="flare").

You can also bypass scaling with identity values: .add(so.Dot(color="C3")) sets a literal property on the mark rather than mapping a variable.

To extend the system, subclass so.Stat (a dataclass) and implement __call__(self, data, groupby, orient, scales), returning a transformed DataFrame. data has columns named after properties (x, y, color...), orient is "x" or "y" (the axis that holds groups), and groupby.apply(data, func) runs func per group, keeping the grouping columns. Custom so.Move classes work the same way but adjust positions.

This is the same machinery built-ins use, so custom stats compose with facets, color grouping and other layers. Note that the base classes are only lightly documented and the API may still change; pin your seaborn version when you rely on it.

Example

import matplotlib
matplotlib.use("Agg")
from dataclasses import dataclass
import pandas as pd
import seaborn.objects as so
import matplotlib.pyplot as plt

@dataclass
class Trimmed(so.Stat):
    cut: float = 0.1
    def __call__(self, data, groupby, orient, scales):
        other = {"x": "y", "y": "x"}[orient]
        def f(df):
            lo, hi = df[other].quantile([self.cut, 1 - self.cut])
            return pd.DataFrame({orient: [df[orient].iloc[0]],
                                 other: [df[other].clip(lo, hi).mean()]})
        return groupby.apply(data, f)

df = pd.DataFrame({"k": list("aaaaabbb"), "v": [1, 2, 3, 4, 100, 2, 3, 4]})
p = (so.Plot(df, x="k", y="v", color="k").add(so.Bar(), Trimmed(cut=0.2))
       .scale(color=so.Nominal(["C3", "0.6"], order=["b", "a"])))
fig = plt.figure()
p.on(fig).plot()
print([float(round(b.get_height(), 1)) for b in fig.axes[0].patches])  # [6.8, 3.0]
print(df.groupby("k")["v"].mean().round(1).to_dict())  # {'a': 22.0, 'b': 3.0}

Remember

  • Every mapped property passes through a Scale you can set with .scale()
  • Literal mark arguments like so.Dot(color='C3') bypass scaling
  • Subclass so.Stat and implement __call__ to add custom transforms

Watch out for

  • Using a numeric column for color and getting a continuous gradient when you meant categories; wrap it in so.Nominal().

Check yourself

  1. Your 'year' column holds integers 2024-2026 and you want three distinct colors, not a gradient. What do you do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: seaborn.pydata.org/tutorial/properties.html

Builds on

Unlocks

Nothing builds on this yet.