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):
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.