资讯动态

手把手复现Nature风格小提琴图:从KDE到显著性标注

发布时间:2026/10/21 5:32:21 来源:尧图企业网站定制
第三篇终于来了。前两篇把柱状图和箱线图的复现流程钉死之后不少读者留言说最想看的还是这张小提琴图。原因不难猜它能把数据的分布形态和组间差异同时画出来信息密度比箱线图高一大截美观程度也明显更能打。但市面上的教程要么停留在一行 seaborn 代码上要么还原度差得远和顶刊出版风格差了十万八千里。这篇不打算说空话。我直接拿一份模拟的实验分组数据把一张“能进 Nature 组图”的小提琴图完整复现出来从 KDE 核密度计算、左右镜像填充到箱线叠加、散点抖动、显著性标注所有关键参数都写到位。想给自己论文配图、做数据分析后快速出图的研究者或者被默认主题折磨过的 Python 绘图玩家都能从这篇里找到能直接抄走的方案。1. 动手设计小提琴图的本质与方案取舍1.1 小提琴图比箱线图多了什么小提琴图说白了就是“箱线图 旋转 90 度的核密度曲线”。箱线图只给你五个数字最小值、下四分位数、中位数、上四分位数、最大值外加一堆离散的离群点。它方便但信息损失严重——两组数据的五数概括可能完全一样实际分布却一个单峰一个双峰这个差异在箱线图里是看不出来的。小提琴图用核密度估计KDE把每个分组的数据分布拟合成一条平滑曲线曲线横过来放在分组轴上左右镜像填充后就像一个“倒着的小提琴”。从这里你能直观看出分布是偏左还是偏右、是否有双峰甚至能对比不同组的峰宽与拖尾。对于生物实验里常见的“对照组平缓、处理组出现双峰”这类情况小提琴图几乎是标配。不过要提醒一句信息量提升是有代价的。箱线图哪怕是几十个样本也能稳得住小提琴图在样本量不足时那条曲线会被带宽参数左右画出来的形状未必可靠。具体阈值后面我会专门说。1.2 为什么不用 seaborn 默认图很多人的第一反应是seaborn.violinplot()一行代码不就完了确实能出图但离 Nature 风格差得远。我用 seaborn 默认参数画过一版问题有三个。第一各组小提琴宽度不统一。seaborn 默认会把每条密度曲线按组内最大值归一化这本身没问题但分组之间的“胖瘦”会和样本量、离群值混在一起组间视觉上的宽度对比容易误导读者。第二默认配色太花哨红黄蓝紫一股脑上既不满足色盲友好又和黑白印刷场景冲突。第三也是最关键的当你需要精确标注组间显著性、控制每一个字体字号、把图无缝嵌进分面组图时默认 API 的调整空间不够。所以这次我选择基于 matplotlib 底层手写。听起来麻烦实际写完一个绘图函数之后后面每组数据只是换参数调用的事改起来反而比反复调 seaborn 的inner、linewidth更顺手。1.3 成图规格与代码结构先定一个最终目标避免画到一半跑偏。输出格式PDF 矢量文件后续用软件拼组图也不会糊。物理尺寸单栏图宽度 8.5 cm 左右符合主流期刊排版需求。字体Helvetica 或 Arial全部字号 7 pt坐标轴标题可稍大。配色一套色盲友好的三色方案填充透明度 0.60.7。结构数据准备 → KDE 计算 → 主体绘制 → 细节定制 → 输出保存。我在下文按照这个流程展开每个环节都会贴代码和参数说明。2. 数据准备与分组设计2.1 数据结构与读取先造一份模拟数据。假设某课题组比较三种条件下的细胞活性测量值数据结构就是长表一列是分组一列是数值。import numpy as np import pandas as pd rng np.random.default_rng(42) group_a rng.normal(50, 8, size40) group_b rng.normal(62, 12, size40) group_c np.concatenate([ rng.normal(45, 5, size25), rng.normal(70, 6, size15) ]) df pd.DataFrame({ group: [Ctrl] * 40 [Treat1] * 40 [Treat2] * 40, value: np.concatenate([group_a, group_b, group_c]) })注意第三组我故意做了双峰结构这是小提琴图最能体现价值的地方箱线图看到的是一个扁平的大盒子而小提琴图能把两个峰值清清楚楚地展示出来。数据可以从 CSV 读进来# df pd.read_csv(your_data.csv)只要保证group列是类别、value列是数值型后面所有代码都不用改。2.2 样本量多少人、多少个点才适合这是我在实际投稿过程中被审稿人问过的问题你的样本量足够画小提琴图吗我的经验法则是单组样本量低于 10 时KDE 的误差会非常大曲线会出现明显锯齿或者拖尾这时画小提琴图不如直接画箱线图加散点。样本量在 1030 之间可以画但需要手动选择较宽的带宽并且要在图注里注明数据点数量。样本量在 30 以上基本可以放心画KDE 会稳定很多。为什么会这样因为核密度估计本质是用一组核函数去盖住每个数据点然后求和。数据点太少核函数叠不出来光滑的峰就会变成一坨不规则的波浪线。你当然可以用更宽的带宽强行磨平但那属于用图形参数掩盖数据缺陷不太诚实。所以拿到数据先看一眼df.groupby(group)[value].count()心里有数再决定要不要用这张图。2.3 数据清洗与缺失值处理小提琴图对离群值比对箱线图更敏感。一个极端离群值会把 KDE 曲线向右拖出长长的尾巴导致整个小提琴看起来“飘”了。处理办法分两种一是确认离群值来自测量错误或录入错误可以直接删除二是异常值确实是生物学差异那就保留同时把 y 轴范围裁到合理区域并在作图时选择更稳健的带宽。df df.dropna(subset[value]) df df[np.abs(df[value] - df[value].mean()) 4 * df[value].std()]直接删掉 4 个标准差之外的点是一种比较常见的做法。注意这里要用分组的统计量而不是全局统计量否则不同组之间的基线差异会被误删。还有一类数据不能直接画小提琴图离散计数数据比如每个细胞内的斑点数量取值只有 0、1、2、3。KDE 会把离散值滑成平滑的连续曲线出现大量“半整数”的假象。这种数据应该用蜂群图、条形比例图或者直接画原始散点别硬上小提琴图。3. 核心绘制KDE 计算与小提琴主体渲染3.1 理解 KDE 与带宽参数KDE 计算其实不神秘。scipy.stats.gaussian_kde会以每个数据点为中心放一个高斯核然后把这些核叠加起来做归一化。带宽参数bw_method控制核函数的宽度它就像一把尺子尺子短曲线会细致但可能过拟合尺子长曲线光滑但可能把真实的双峰磨掉。用个生活化的类比想象你在沙滩上用脚把脚印抹平。带宽太小脚印之间的边界还在整片沙滩坑坑洼洼带宽太大一脚下去全部抹平原来的起伏全没了。所以这个参数决定了你是否能看清数据里真实存在的“脚印”。默认的bw_methodscott是根据样本量自适应计算的大多数时候能用。但我个人习惯在小提琴图上手动设bw_method0.3左右让曲线更平滑。如果数据是单峰且样本量中等0.250.35 通常合适如果样本量很大n 100可以用 0.150.2 保留更多细节。建议多画几版对比结合直方图确认自己没有把双峰抹平。3.2 小提琴主体镜像填充与归一化先写一个灵活的小函数负责画单个分组的小提琴。import matplotlib.pyplot as plt from matplotlib.patches import Polygon from scipy import stats def violin_group(ax, data, pos, color, bw0.3, width_scale0.45): xs np.linspace(data.min() - 0.1 * np.ptp(data), data.max() 0.1 * np.ptp(data), 200) dens stats.gaussian_kde(data, bw_methodbw)(xs) dens np.clip(dens, 0, None) dens dens / dens.max() * width_scale ax.fill_betweenx(xs, pos - dens, pos dens, facecolorcolor, alpha0.6, lw0.8, edgecolorcolor, zorder2)重点是这两行np.clip(dens, 0, None)把负密度裁掉防止尾巴跑进负值区域dens / dens.max() * width_scale是把每组密度归一化到相同的最大宽度。如果不做归一化峰值高的组会看起来“胖”峰值低的组“瘦”组间无法公平比较形状。width_scale控制整张图中小提琴的最大宽度一般 0.40.5 比较协调太宽会互相挤压太窄则看不清形状。注意我用的是fill_betweenx也就是把数据值放在 y 轴、分组位置放在 x 轴。这是大多数期刊图表的标准排版方向。3.3 箱线叠加、散点抖动与统计标注小提琴主体画完后图还缺几个要素内部的箱线统计量、原始数据散点、组间显著性标注。箱线图可以直接用ax.boxplot设置widths控制箱体宽度showfliersFalse关闭离群点因为小提琴已经展示了分布散点也会承担展示离群点的功能。散点抖动需要注意直接用原始数据点画所有点会重叠成一条竖线什么信息也看不到。所以要在 x 方向加一点随机偏移。def jitter(values, center, seed1): gen np.random.default_rng(seed) return center gen.normal(0, 0.04, sizelen(values))固定随机种子很重要。否则每次运行图都会变后续投稿时图文对不上会很麻烦。散点的s8左右比较合适太小看不到太大会盖住箱线。绘制主流程fig, ax plt.subplots(figsize(3.2, 4.0)) groups df[group].unique() colors [#4C72B0, #DD8452, #55A868] for i, (g, c) in enumerate(zip(groups, colors)): sub df[df[group] g][value] violin_group(ax, sub, posi1, colorc) ax.boxplot(sub, positions[i1], widths0.08, showfliersFalse, zorder3, medianpropsdict(colorblack, lw1.2), patch_artistFalse) ax.scatter(jitter(sub, i1), sub, s8, colorblack, alpha0.5, zorder4)zorder 必须安排好小提琴在底层箱线在中层散点最上层。如果散点被密度填充盖住检查 zorder。显著性标注可以手动画一条横线加星号x1, x2 1, 2 y max(np.max(df[value]) * 1.05, 80) ax.plot([x1, x1, x2, x2], [y, y2, y2, y], colorblack, lw1) ax.text((x1x2)/2, y4, **, hacenter, vabottom, fontsize9)注意 P 值星号要放在方括号里说明检验方法**p 0.01, two-sided Wilcoxon rank-sum test。别只画星号不写检验过程这是学术规范问题。4. Nature 风格细节定制4.1 字体、字号、线宽的整体预设默认的 matplotlib 字体是 DejaVu Sans很多期刊编辑一眼就能看出来。学术出版界最常见的两类字体是 Helvetica 和 Arial如果你所在机构没有购买 Helvetica 版权Arial 是更稳妥的替代。在代码开头统一设置plt.rcParams.update({ font.family: sans-serif, font.sans-serif: [Arial], font.size: 7, axes.linewidth: 0.6, xtick.major.width: 0.6, ytick.major.width: 0.6, xtick.direction: out, ytick.direction: out, })字号方面正文文字 7 pt坐标轴标题可以到 8 pt图例 6.5 pt。不要小于 6 pt否则印刷后没法辨识。物理尺寸上单栏图宽度 8.5 cm 左右双栏图 17 cm 左右高度按比例调整即可。图小字小是顶刊的特点不是越大的字越清晰而是整体协调。4.2 配色方案与色盲友好关于配色我强烈建议别再用默认的tab10。那套颜色对于红绿色盲完全不友好。这里给出一套我实测下来比较稳妥的预设COLOR_PALETTE [#4C72B0, #DD8452, #55A868]三色刚好对应三组色相距离足够大而且都是中等饱和度的“学术色”。如果需要四到六组可以选Set2色板中挑但要注意相邻颜色之间的区分度。对于黑白印刷场景透明度和边框样式也能帮上忙把其中一组改成低透明度、另一组改成实心打印出来后依然能区分。对小提琴主体我习惯填充透明度 0.6边缘线宽 0.8。这样组间重叠的区域会自然产生深色混合不会让人误以为是另一个数据组。4.3 坐标轴、刻度、图例和导出设置Nature 风格的坐标轴是极简的只保留左边和底下的边框去掉上边和右边。顶部和右侧的边框线几乎不会出现在顶刊图里。ax.spines[top].set_visible(False) ax.spines[right].set_visible(False)刻度朝外而不是朝内目的是不干扰数据区域的阅读。tick 长度建议 23 pt。图例如果必要放在右上角或图外。默认的图例边框线非常粗需要去掉ax.legend(frameonFalse, locupper right, fontsize6.5)如果图里已经有分组标签作为 x 轴刻度图例甚至可以不加减少冗余信息。导出时不要用默认的 PNG 就完了。投稿用的矢量图最佳格式是 PDF 或 EPS如果期刊要求位图选 TIF600 dpi 以上LZW 压缩。fig.savefig(violin_nature.pdf, bbox_inchestight)保存 PDF 之后建议用软件打开缩放看一遍确认所有字体都是矢量线条没有奇怪的锯齿。5. 常见问题排查与避坑记录5.1 六类高频问题速查表我把自己和周围同事实际踩过的坑整理成一张表几乎覆盖了画这张图会遇到的大部分问题。现象可能原因解决办法各组小提琴宽窄不一没有做密度归一化统一除以各组最大密度或全局最大密度曲线出现负值区域KDE 拟合值未裁剪加np.clip(dens, 0, None)左右镜像不对称填充时顺序写错检查左右边界点的排列顺序散点被密度图完全盖住zorder 层级不对散点 zorder 设为 4箱线设为 3双峰被磨平带宽过大调小bw_method比如 0.2保存 PDF 后中文字体变方块字体没有嵌入指定font.sans-serif使用系统的中文字体5.2 实战复盘从“能出图”到“能投稿”讲一个典型的修改案例。A 同学拿一段 RNA 表达数据来找我说自己的小提琴图被导师批了。我一看第一版用的是 seaborn 默认样式六组数据每组宽度都不一样颜色是默认的紫色箱线内的小提琴有统一的白色边缘线整体看起来像玩具图明显达不到投稿标准。我们做的修改有三步。第一步把每组密度除以全局最大密度确保六组宽度代表真实的相对密度而不是被各组内部峰值带歪。第二步把颜色全部换成统一灰色系不同组之间用透明度区分。第三步在每组下方标注 n 值在图注里加上统计检验方法。改完之后的图虽然颜色朴素了但信息传递效率反而更高审稿人看不到花哨颜色只能看到数据和统计结果。这个小案例说明学术图表的美感不等于色彩丰富而是信息组织清晰、所有元素都服务于数据本身。每加一个视觉效果都要问自己“这能让读者更懂数据吗”。5.3 什么时候不要用小提琴图不是所有分布比较都适合用小提琴图。如果你只有 5 个样本KDE 形状基本不可信这时候老老实实画箱线图加所有散点大家都能看懂你的结论。如果你的数据是离散计数小提琴图会把离散值磨成连续曲线给人错误的连续印象。如果你关心的只是中位数差异箱线图就够了画小提琴图反而引入过多视觉噪声。另一个常见误用是——把小提琴图画出来时只展示密度却没有标注任何统计量。审稿人盯着图上那两条“腰”想知道中位数在哪结果找不到。这时应该要么叠加箱线要么在每组旁边用粗点标出中位数。6. 进阶扩展更复杂的分组与自动化标注6.1 双因素分组的小提琴图有时候实验设计是两个因素的组合比如“时间点 × 处理方式”。这种情况可以把分组轴设置为Ctrl-0h、Ctrl-24h这样的字符串简单直接也是我在实际项目里最常用的方案。更花哨的方式是嵌套小提琴内圈画第一个因素外圈画第二个因素。我建议新手先别追求这种效果因为嵌套之后组间比较变得很困难除非你有明确的视觉分层需求。嵌套关键代码是内外两层fill_betweenx外层用浅色内层用深色但这种方式只适合两因素且每组样本量很大的场景。6.2 自动把检验结果转成星号手动画星号容易写错组间位置。自动化做法是解析检验的 P 值然后生成字符串from scipy.stats import mannwhitneyu def star(p): if p 0.001: return *** if p 0.01: return ** if p 0.05: return * return ns stat, p mannwhitneyu(group_a, group_b) label star(p) # 用于标注这里有个容易踩的坑mannwhitneyu默认返回单侧 P 值比较两组时最好设置alternativetwo-sided。另外如果组间方差不齐用 Welch t 检验或 Mann-Whitney U 检验如果数据符合正态性且方差齐性直接用 t 检验更常见。选择错误的检验方法虽然画出来的星号差不多但审稿人一旦追问方法细节就会很被动。6.3 动画或交互式小提琴图的备注若你想做交互展示plotly 虽然一行出图但交互式的图形不适合期刊投稿只适合内部探索数据。我个人建议分析阶段可以用交互式快速看分布发表阶段还是走 matplotlib 的静态矢量图。交互式图的鼠标悬浮并不能帮你提升论文的说服力反而可能让合作者误以为某些特征可以在浏览器里验证。结尾最后再分享一个我的习惯每次存图前我都会把 KDE 曲线和原始数据的直方图叠在一起看一遍。如果直方图明明有明显双峰而我的 KDE 参数却把所有峰磨掉了那这张图就是视觉骗子。先确认形状可信再考虑配色好不好看。做图这件事数据诚实永远排在美观前面。如果你也想做一张能投出去的图不用急着把每个参数调到完美。先用默认参数跑通流程再逐项调整带宽、归一化、字体、配色最后导出 PDF 放大检查。这个顺序我反复用了很多年过程最顺返工最少。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑