目录8
文章/Matplotlib/第 11 章
小提琴图
Matplotlib · 第 11 / 43 章
小提琴图 violinplot()
小提琴 = 箱线图 + 核密度估计。箱线图只给五数,小提琴把整条分布的「胖瘦」都画出来,所以能看出双峰——这是它相对箱线图唯一的、也是决定性的优势。
代价是:形状由 bw_method 决定,换个带宽就是另一把小提琴,所以它比箱线图「主观」。
Axes.violinplot(
dataset, # 数据:数组的列表(★ 参数名不是 x)
positions=None, # 每组位置,默认 1, 2, 3...
widths=0.5, # 小提琴最大宽度(数据坐标)
showmeans=False, # 显示均值
showmedians=False, # ★ 显示中位数(默认 False,最容易忘)
showextrema=True, # 显示极值端点和中轴
quantiles=None, # 额外分位线,嵌套列表:一组一个数组
points=100, # 每半个小提琴的密度采样点数
bw_method=None, # KDE 带宽:'scott' / 'silverman' / 浮点
side="both", # 'both' / 'low' / 'high'
orientation="vertical", # 朝向(替代已废弃的 vert)
)| 参数 | 一句话 | 默认值的影响 |
|---|---|---|
dataset |
数据 | 参数名是 dataset,不是 x |
positions |
位置 | 1, 2, 3…;与 widths 配合 |
widths |
最大宽度 | 0.5;多组密集时可加大到 0.9 |
bw_method |
KDE 带宽 | None → 'scott',随 n 自适应 |
showmedians |
中位数线 | False——默认没有中位数 |
showmeans |
均值线 | False |
showextrema |
极值 + 中轴 | True |
quantiles |
分位线 | 必须嵌套,平铺报错 |
side |
画哪半边 | 'both';单边用于同位置叠两组 |
points |
采样点数 | 100,只影响曲线平滑度 |
默认只有「骨架」,没有中位数
fig, ax = plt.subplots()
ax.violinplot(groups)
print(sorted(ax.violinplot(groups).keys()))
# 输出: ['bodies', 'cbars', 'cmaxes', 'cmins']
plt.show()
bodies 是轮廓,cmins / cmaxes 是上下两根极值横杠,cbars 是中间那根竖轴。没有中位数线——要看得自己开:
ax.violinplot(groups, showmedians=True, showmeans=True)
print(sorted(ax.violinplot(groups, showmedians=True, showmeans=True).keys()))
# 输出: ['bodies', 'cbars', 'cmaxes', 'cmeans', 'cmedians', 'cmins']多出来的 cmeans / cmedians 就是两条线。这是小提琴图最常见的「图怎么不对」——不是 bug,是默认省掉了。
showmedians 和 showmeans 是两个独立开关,只给 showmeans=True 仍然不会有中位数。
bw_method:胖瘦由它决定
for bw, color in [(0.12, "tab:blue"), (0.35, "tab:orange"), (1.2, "tab:green")]:
parts = ax.violinplot(groups[0], positions=[1], widths=0.85,
bw_method=bw, showextrema=False)
body = parts["bodies"][0]
body.set_facecolor(color)
body.set_edgecolor(color)
body.set_alpha(0.35 if bw > 0.5 else 0.55)
body.set_label(f"bw_method={bw}")
ax.legend()
ax.set_xticks([1], ["A"])
plt.show()
- 数值越小 → 核越窄 → 曲线毛糙、长出尖刺,
0.12已经把随机噪声当成结构了。 - 数值越大 → 核越宽 → 越来越像椭圆,
1.2时双峰会被抹平——正好毁掉小提琴图唯一的价值。
默认 None 等价于 "scott",公式是 n^(-1/5):样本量越大带宽越小。所以同一份分布,采样 50 个点和 5000 个点画出来形状不同,这不算 bug。
side / widths:同位置叠两组
ax.violinplot(groups[0], positions=[1], side="low", showextrema=False)
ax.violinplot(groups[1], positions=[1], side="high", showextrema=False)
ax.set_xticks([1], ["A"])side="low" 只画左半、side="high" 只画右半,两组可以站在同一个位置上背靠背对比,不用错开也不用挤窄。
ax.violinplot(groups, widths=0.9) # 默认 0.5,组多时加宽更饱满widths 单位是数据坐标,和 positions 的间距同一套:位置间距 1、宽度 0.9 时相邻小提琴几乎贴住。
quantiles:额外分位线
ax.violinplot(groups, quantiles=[[0.25, 0.75]] * 3, showextrema=False)外层一组一个数组——这是它和别的参数最不一样的地方。平铺会直接报错:
ax.violinplot(groups, quantiles=[0.25, 0.75])
# ValueError: List of violinplot statistics and quantiles values must have the same length报错信息看着莫名其妙,原因是它拿 len(quantiles)(2)去和组数(3)比。三组各标 Q1/Q3 就得写 [[0.25, 0.75]] * 3。
线数也对得上:3 组 × 2 个分位 = 6 条线,全塞在 cquantiles 这一个 LineCollection 里:
vp = ax.violinplot(groups, quantiles=[[0.25, 0.75]] * 3, showextrema=False)
print(len(vp["cquantiles"].get_segments()))
# 输出: 6points / showextrema
ax.violinplot(groups, points=200) # 采样更密,曲线更顺
ax.violinplot(groups, showextrema=False) # 去掉极值横杠和中轴,只留轮廓points 只是每条边的采样点数,不改变形状——画出来一样,只是文件更大。showextrema=False 常和小提琴图叠加散点时一起用,因为中轴会把散点串起来。
返回对象
vp = ax.violinplot(groups) # 默认只有 4 个键
print(sorted(vp.keys()))
# 输出: ['bodies', 'cbars', 'cmaxes', 'cmins']
vp = ax.violinplot(groups, showmeans=True, showmedians=True,
quantiles=[[0.25, 0.75]] * len(groups)) # ★ 嵌套列表
print(sorted(vp.keys()))
# 输出: ['bodies', 'cbars', 'cmaxes', 'cmeans', 'cmedians', 'cmins', 'cquantiles']
print(isinstance(vp["bodies"], list), len(vp["bodies"]))
# 输出: True 3
print(type(vp["cmedians"]).__name__)
# 输出: LineCollection只有 bodies 是 list(一组一个 FillBetweenPolyCollection),其余每个键都是单个 Collection,装的是一次性建好的全部线段。所以逐组上色要写 vp["bodies"][i],而改中位线颜色直接 vp["cmedians"].set_color(...) 一把全改。
vp = ax.violinplot(groups, showmedians=True)
vp["bodies"][0].set_facecolor("#AEC7E8")
vp["bodies"][0].set_alpha(0.8) # 默认 alpha 是 0.3
vp["cmedians"].set_color("#C44E52")
vp["cmedians"].set_linewidth(2)
plt.show()注意事项
showmedians默认False,图上没有中位数线。 排第一。- 参数名是
dataset,不是x,boxplot和violinplot在这里不一致。 quantiles必须嵌套,外层长度要等于组数。bw_method决定形状,默认'scott'随样本量变——同一份分布不同 n 画出来不一样是正常的。- 宽度按各组自己的峰值归一,所以小提琴的「胖」只表示组内密度的相对高低,不同组之间不能比幅度。
- 没有
tick_labels参数,标签得自己ax.set_xticks([1, 2, 3], names)。 vert已废弃,用orientation="horizontal"。- 数据量少于 30 个点就不必用小提琴——KDE 在稀疏样本上纯粹是画出来的想象力,直接用箱线图或散点。
综合示例:带宽对比 / 背靠背 / 分位线全开
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
rng = np.random.default_rng(7)
# 双峰数据:小提琴能看出两个鼓包,箱线图不能
bimodal = np.concatenate([rng.normal(-2, 0.55, 400), rng.normal(2, 0.55, 400)])
ctrl = rng.normal(0, 1.4, 800)
fig, axes = plt.subplots(1, 3, figsize=(15, 4.6))
# ---- (a) 带宽三档:形状完全是三张不同的图 ----
for bw, color in [(0.1, "tab:blue"), (0.3, "tab:orange"), (0.9, "tab:green")]:
parts = axes[0].violinplot(bimodal, positions=[1], widths=0.8,
bw_method=bw, showextrema=False)
body = parts["bodies"][0]
body.set_facecolor(color)
body.set_edgecolor(color)
body.set_alpha(0.45 if bw > 0.5 else 0.6)
body.set_label(f"bw={bw}")
axes[0].legend(fontsize=8)
axes[0].set_xticks([1], ["双峰数据"])
axes[0].set_title("(a) 同一份数据,三个带宽")
# ---- (b) side='low' / 'high':同位置背靠背 ----
axes[1].violinplot(ctrl, positions=[1], side="low", widths=0.9,
showmedians=True, showextrema=False)
axes[1].violinplot(bimodal, positions=[1], side="high", widths=0.9,
showmedians=True, showextrema=False)
axes[1].set_xticks([1], ["左: 正态 / 右: 双峰"])
axes[1].set_title("(b) side=low / high 背靠背")
# ---- (c) 中位数 + 分位线 + 均值全开 ----
vp = axes[2].violinplot([ctrl, bimodal], positions=[1, 2], widths=0.8,
showmeans=True, showmedians=True, showextrema=True,
quantiles=[[0.1, 0.25, 0.75, 0.9]] * 2)
for i, body in enumerate(vp["bodies"]):
body.set_facecolor(plt.cm.Set2(i, alpha=0.7))
body.set_edgecolor("none")
vp["cmedians"].set_color("#C44E52")
vp["cmedians"].set_linewidth(2)
vp["cmeans"].set_color("black")
vp["cmeans"].set_linestyle("--")
vp["cquantiles"].set_color("#555555")
vp["cquantiles"].set_linewidth(0.8)
axes[2].set_xticks([1, 2], ["正态", "双峰"])
axes[2].set_title("(c) 均值 + 中位数 + 4 条分位线")
plt.tight_layout()
plt.show()(a) 是这个参数最该被记住的一课:同一份双峰数据,bw=0.1 画出四五个尖刺,bw=0.9 画出一个平滑单峰,只有中间那档接近真相。看到小提琴图有多峰,先怀疑带宽。