How seaborn estimates: bootstrap and binning

seaborn · Expert · 15 min

Knowing the statistics behind the defaults lets you defend a figure, reproduce it exactly and avoid slow plots on large data.

Confidence intervals in barplot, pointplot, lineplot, regplot and so.Est come from a nonparametric bootstrap: resample the rows of each group with replacement n_boot times (default 1000), recompute the estimator each time, and take the percentiles of those estimates (2.5th and 97.5th for 95%). Consequences:

  • Results vary slightly from run to run unless you pass seed=.
  • Cost grows with rows times n_boot; on millions of rows use errorbar="se" (analytic for the mean) or None.
  • The bootstrap assumes independent rows. With repeated measures, pass units="subject" so whole subjects are resampled instead of individual rows, which gives honestly wider intervals.
  • weights= (added in 0.13 for some functions) changes the estimator to a weighted mean.

KDE bandwidth uses Scott's rule by default (via a Gaussian kernel), scaled by bw_adjust. It assumes roughly unimodal, unbounded data; multimodal or bounded data needs a smaller bw_adjust or cut=0.

Histogram bins default to NumPy's "auto" rule, which takes the larger number of bins from the Sturges and Freedman-Diaconis rules; discrete=True centers one bin on each integer, which is right for counts.

When in doubt, reproduce the number yourself, as the example does, and check that the plot shows what you would report.

Example

import matplotlib
matplotlib.use("Agg")
import numpy as np
import pandas as pd
import seaborn as sns

rng = np.random.default_rng(9)
df = pd.DataFrame({"g": ["a"] * 200, "v": rng.exponential(10, 200)})

ax = sns.pointplot(data=df, x="g", y="v", errorbar=("ci", 95),
                   n_boot=2000, seed=42)
lo, hi = ax.lines[1].get_ydata()          # lines[0] is the point, [1] its CI
print(round(lo, 2), round(hi, 2))         # 9.03 12.1

# Reproduce a percentile bootstrap by hand
boot_rng = np.random.default_rng(42)
v = df["v"].to_numpy()
means = [boot_rng.choice(v, v.size).mean() for _ in range(2000)]
print(np.percentile(means, [2.5, 97.5]).round(2))   # very close to seaborn's
print(round(v.mean(), 2), round(v.std(ddof=1) / np.sqrt(v.size), 2))  # mean, se

Remember

  • Default CIs are percentile bootstraps: n_boot resamples, then percentiles
  • Use seed= for reproducibility and units= for repeated measures
  • KDE uses Scott's rule times bw_adjust; bins default to NumPy's 'auto'

Watch out for

  • Bootstrapping rows of repeated-measures data, which treats correlated rows as independent and gives too-narrow intervals.

Check yourself

  1. Each of 20 subjects contributes 50 rows. Which option gives a more honest CI for the group mean in lineplot?

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

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