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

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 朵。
下载包 sha256:c37cc77bf98ca682c107331f5cbff981171b74a37aa53e329b5527051b63f018