目录8

文章Matplotlib第 11 章

小提琴图

Matplotlib · 第 11 / 43 章

小提琴图 violinplot()

小提琴 = 箱线图 + 核密度估计。箱线图只给五数,小提琴把整条分布的「胖瘦」都画出来,所以能看出双峰——这是它相对箱线图唯一的、也是决定性的优势。

代价是:形状由 bw_method 决定,换个带宽就是另一把小提琴,所以它比箱线图「主观」。

Python13 行
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,只影响曲线平滑度

默认只有「骨架」,没有中位数

Python5 行
fig, ax = plt.subplots()
ax.violinplot(groups)
print(sorted(ax.violinplot(groups).keys()))
# 输出: ['bodies', 'cbars', 'cmaxes', 'cmins']
plt.show()

小提琴图最小示例:默认只有极值横杠和中轴

bodies 是轮廓,cmins / cmaxes 是上下两根极值横杠,cbars 是中间那根竖轴。没有中位数线——要看得自己开:

Python3 行
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:胖瘦由它决定

Python11 行
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()

bw_method 三档对比:0.12 毛糙、0.35 适中、1.2 过度平滑

  • 数值越小 → 核越窄 → 曲线毛糙、长出尖刺,0.12 已经把随机噪声当成结构了。
  • 数值越大 → 核越宽 → 越来越像椭圆,1.2 时双峰会被抹平——正好毁掉小提琴图唯一的价值。

默认 None 等价于 "scott",公式是 n^(-1/5):样本量越大带宽越小。所以同一份分布,采样 50 个点和 5000 个点画出来形状不同,这不算 bug。

side / widths:同位置叠两组

Python3 行
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" 只画右半,两组可以站在同一个位置上背靠背对比,不用错开也不用挤窄。

Python1 行
ax.violinplot(groups, widths=0.9)      # 默认 0.5,组多时加宽更饱满

widths 单位是数据坐标,和 positions 的间距同一套:位置间距 1、宽度 0.9 时相邻小提琴几乎贴住。

quantiles:额外分位线

Python1 行
ax.violinplot(groups, quantiles=[[0.25, 0.75]] * 3, showextrema=False)

外层一组一个数组——这是它和别的参数最不一样的地方。平铺会直接报错:

Python2 行
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 里:

Python3 行
vp = ax.violinplot(groups, quantiles=[[0.25, 0.75]] * 3, showextrema=False)
print(len(vp["cquantiles"].get_segments()))
# 输出: 6

points / showextrema

Python2 行
ax.violinplot(groups, points=200)        # 采样更密,曲线更顺
ax.violinplot(groups, showextrema=False) # 去掉极值横杠和中轴,只留轮廓

points 只是每条边的采样点数,不改变形状——画出来一样,只是文件更大。showextrema=False 常和小提琴图叠加散点时一起用,因为中轴会把散点串起来。

返回对象
Python14 行
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(...) 一把全改。

Python7 行
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 在稀疏样本上纯粹是画出来的想象力,直接用箱线图或散点。

综合示例:带宽对比 / 背靠背 / 分位线全开

Python52 行
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 画出一个平滑单峰,只有中间那档接近真相。看到小提琴图有多峰,先怀疑带宽。