小提琴图 + 内嵌箱线图

下载 R / Python 代码与数据(747 KB)

用小提琴的轮廓展示各组数据的整体分布形状(单峰、双峰、偏态),再在内部嵌入箱线图标出中位数与四分位范围。适合在组间比较分布形状,而不只是比较中心位置。

图与代码

code/R/main.R
suppressPackageStartupMessages(library(ggplot2))

d <- read.csv("data/iris.csv", encoding = "UTF-8")
d$Species <- factor(d$Species, levels = c("setosa", "versicolor", "virginica"))

p <- ggplot(d, aes(x = Species, y = Sepal.Length, fill = Species)) +
  geom_violin(trim = TRUE, scale = "width", width = 0.9, alpha = 0.8,
              colour = "grey20", linewidth = 0.4) +
  geom_boxplot(width = 0.12, fill = "white", colour = "grey20",
               outlier.shape = NA, linewidth = 0.4) +
  scale_fill_manual(values = c(setosa = "#66C2A5", versicolor = "#FC8D62", virginica = "#8DA0CB")) +
  labs(x = "Species", y = "Sepal length (cm)") +
  theme_classic(base_size = 12) +
  theme(legend.position = "none")

ggsave("outputs/figure_r.png", p, width = 6, height = 4.5, dpi = 300, bg = "white")

code/py/main.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scienceplots  # noqa: F401
from scipy.stats import gaussian_kde

plt.style.use(["science", "no-latex"])
plt.rcParams.update({"font.family": "sans-serif", "font.size": 11})  # 与 R 版(theme_classic 无衬线)统一

d = pd.read_csv("data/iris.csv")
species = ["setosa", "versicolor", "virginica"]
colors = ["#66C2A5", "#FC8D62", "#8DA0CB"]


def bw_nrd0(x):
    # 与 R 的 bw.nrd0 相同的带宽规则(ggplot2 默认)
    q75, q25 = np.percentile(x, [75, 25])
    return 0.9 * min(np.std(x, ddof=1), (q75 - q25) / 1.34) * len(x) ** -0.2


fig, ax = plt.subplots(figsize=(6, 4.5))
for i, (sp, col) in enumerate(zip(species, colors)):
    y = d.loc[d["Species"] == sp, "Sepal.Length"].to_numpy()
    grid = np.linspace(y.min(), y.max(), 512)  # trim = TRUE:只画数据范围内
    dens = gaussian_kde(y, bw_method=bw_nrd0(y) / np.std(y, ddof=1))(grid)
    half = 0.45 * dens / dens.max()  # scale = "width":各组最大宽度相同
    ax.fill_betweenx(grid, i - half, i + half, facecolor=col, alpha=0.8, edgecolor="#333333", linewidth=0.6)
    ax.boxplot(y, positions=[i], widths=0.12, patch_artist=True, showfliers=False,
               boxprops=dict(facecolor="white", edgecolor="#333333", lw=0.6),
               medianprops=dict(color="#333333", lw=1.0),
               whiskerprops=dict(color="#333333", lw=0.6), capprops=dict(visible=False))

ax.set_xticks(range(3))
ax.set_xticklabels(species)
ax.set_xlim(-0.6, 2.6)
ax.set_xlabel("Species")
ax.set_ylabel("Sepal length (cm)")
ax.tick_params(axis="x", which="minor", bottom=False, top=False)  # 分类横轴不要次刻度
fig.savefig("outputs/figure_py.png", dpi=300, facecolor="white")

什么时候用

  • 想比较多个组的分布形状,而不只是均值或中位数
  • 每组样本量足够(通常几十个以上)以支撑密度估计
  • 希望同时给出分布轮廓和箱线图摘要

常见误用

  • 小提琴轮廓来自核密度估计,取决于带宽;带宽不同,峰的数量和平滑程度会变化,不宜过度解读细节
  • 本例按数据范围截断(trim),轮廓末端并不表示数据之外存在取值;不截断则会拖出数据范围之外的尾巴
  • 样本量很小(如每组 10 个以下)时,密度估计不可靠,直接画点或箱线图更诚实
  • 各组宽度在本例按“最大宽度相同”缩放,宽度比较的是形状而不是样本量;组间样本量不同时需另行标注

数据与下载

数据:R 内置数据集 iris(datasets 包)(R 内置数据集(随 R 分发,GPL-2 | GPL-3))。150 朵鸢尾花的萼片长度(Sepal.Length,单位 cm),分为 setosa、versicolor、virginica 三个物种,每种 50 朵。

下载 R / Python 代码与数据(747 KB)

下载包 sha256:c37cc77bf98ca682c107331f5cbff981171b74a37aa53e329b5527051b63f018