How to make `seaborn.violinplot` compatible with Y symmetrical logarithm scale?
Oct 8, 7:49pm

Why it breaks

ax.set_yscale("symlog") after sns.violinplot() will render, but the violin shape will be wrong: seaborn computes the KDE on a linear grid in raw data units, then the finished polygon is just stretched onto the non-linear axis. The violin then doesn't follow the data visually (Stack Overflow, seaborn issue #2258). The correct approach is to compute the density in transformed space.

Option 1 — plain log scale (seaborn ≥ 0.13)

If your values are strictly positive, it's now built in via log_scale, which computes the KDE in log space and sets the axis for you (seaborn docs):

python
sns.violinplot(data=df, x="group", y="value", log_scale=10)

log_scale does not accept symlog, so for data crossing zero you need the trick below.

Option 2 — symlog: transform the data yourself

Use matplotlib's exact symlog transform, plot on a linear axis, then relabel the ticks back to original values. The KDE/bandwidth/cut are then computed in symlog space, which is what makes the shape correct:

python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.scale import SymmetricalLogScale

linthresh = 1  # linear region half-width
scale = SymmetricalLogScale(axis=None, base=10, linthresh=linthresh)
tr = scale.get_transform()          # y -> transformed
inv = tr.inverted().transform      # transformed -> y

df["value_t"] = tr.transform(df["value"].to_numpy())

ax = sns.violinplot(data=df, x="group", y="value_t", inner="quartile")

# relabel ticks with the original data values
ticks = ax.get_yticks()
ax.set_yticks(ticks)
ax.set_yticklabels([f"{v:g}" for v in inv(ticks)])
ax.set_ylabel("value (symlog)")

This keeps everything seaborn offers (hue, split, inner, etc.), with the only cost being cosmetic tick relabeling (no minor log ticks — you can add a SymmetricalLogLocator/LogFormatter on the transformed axis if needed).

Option 3 — real symlog axis with manual KDE

If you want a true symlog axis (with its native ticks/formatter), compute the KDE yourself in transformed space and draw with fill_betweenx:

python
from scipy.stats import gaussian_kde

fig, ax = plt.subplots()
for i, (name, vals) in enumerate(df.groupby("group")["value"]):
    z = tr.transform(vals.to_numpy())
    kde = gaussian_kde(z)
    pad = 2 * kde.factor * z.std()  # ~ equivalent of seaborn's cut=2
    grid = np.linspace(z.min() - pad, z.max() + pad, 200)
    w = kde(grid)
    w = 0.4 * w / w.max()           # half-width
    yv = inv(grid)
    ax.fill_betweenx(yv, i - w, i + w, alpha=.7)
    ax.plot([i, i], [vals.min(), vals.max()], color="k", lw=1)

ax.set_xticks(range(df["group"].nunique()), df["group"].unique())
ax.set_yscale("symlog", linthresh=linthresh)

Notes

  • In both options 2 and 3 the bandwidth acts in symlog units, so it adapts across decades — exactly the behavior you want on a symlog display.
  • If the discontinuous gradient of symlog at ±linthresh bothers you visually, matplotlib's asinh scale is a smooth alternative (matplotlib docs) — but the same "transform first, then estimate density" requirement applies, since seaborn only special-cases plain log via log_scale.
Oct 8, 7:49pm

This is a shared conversation and may include unverified content that doesn't reflect Mistral AI's views. Login to resume chatting.

Vibe can make mistakes. Check answers. Learn more