
做科研图表复现这事我一直觉得最忌讳的就是只盯着别人的图“照猫画虎”不知道图层背后为什么要这么叠。Nature Communications 里那类分组小提琴图乍看就是几个“胖葫芦”并排摆在一起可真要自己动手从数据一步步画出来你会发现里面牵扯到数据结构、统计展示、期刊审美、甚至输出分辨率一堆细节。这篇博文我就拿一个典型的 NC 图表做靶子完整拆解怎么用 R ggplot2 复现一张可以投出去的分组小提琴图——从读图思路到数据整理从基础图层到统计标记再到我在实际操作里踩过的那些坑。这套东西更适合谁看如果你是做生信、医学统计、或者任何需要拿“分组对比”讲故事的科研人员那这篇文章基本能帮你省掉一两个星期的摸索时间。哪怕你现在只用过 Excel 画图只要会一点点 R 的基础语法跟着思路走完一遍也能做出像模像样的 NC 风插图。1. 这个图要复现的到底是什么先读懂图表本身很多人拿到一张文献里的图第一反应就是“我要把这个代码搞到手”而不是先问一句“这个图在说什么”。这是复现工作最大的误区。你连图里的信息层级都没理清楚就算拿到原作者代码换个数据一样画废。1.1 小提琴图 vs 箱线图不只是“好看”先明确一个基本概念。箱线图大家都熟它展示的是五数概括——最小值、下四分位数、中位数、上四分位数、最大值外加离群点。它的优点是稳健、简洁但代价是丢失了数据分布的“中间形态”。比如两组数据一组是双峰分布一组是均匀分布它们的箱线图可能长得几乎一模一样但真实的数据结构完全不同。小提琴图本质上就是“箱线图 核密度曲线”的合体把数据的概率密度曲线对称地画在两侧中间再叠一个精简的箱线图。这样你既能看出中位数和四分位区间又能直接看到数据在哪些区间聚集、哪里稀疏、是不是存在多峰。在 Nature Communications 这类期刊里审稿人和读者都更吃这一套因为他们不光想看到“有没有差异”还想看到“差异到底是怎么分布的”。分组小提琴图就是在小提琴图的基础上把 x 轴再拆出一个分组维度。举个例子你要比较两种药物处理下、四个时间点的某个细胞因子表达量。这时候 x 轴放时间点fill 映射药物类型每个时间点下就有两把小提琴并排一眼看过去既能看到时间趋势又能看到同一时间点上两种药物的差异。这比“一张图里塞八个箱线图”要清爽得多。提示判断一张图需不需要用分组小提琴图有个简单标准——你是不是同时关心“两个维度交叉后的分布形态”如果只关心均值差异箱线图就够了如果数据量大、分布复杂、还想展示样本量那就上小提琴图。1.2 分组小提琴图到底在展示什么关系以 NC 论文里常见的一个场景为例纵轴是某个基因的表达量log2 归一化后横轴是“对照组 / 处理组 / 回复组”fill 颜色代表“患者是否响应”。这个图想回答的问题有三个不同组别之间表达量的中位数有没有显著变化每一组内部的样本分布是集中还是离散是否存在离群亚群两组之间的差异是整体平移还是仅仅尾部少数样本在拉高均值这三个问题普通箱线图只能回答第一个散点图能回答第二个但很难承载大样本分组小提琴图则是三个问题一起答。所以你在复现之前一定要先搞清楚原图作者用这个图想论证什么那个核心信息是什么后面你怎么配色、怎么加统计标记、怎么排布子图都是围绕这个核心信息服务的。另外一个很容易忽略的点是样本量。小提琴图的缺点是样本量少的时候核密度估计会非常不稳定画出来跟一根根火柴棍似的很难看也容易误导读者。这也是为什么实际复现时绝大多数人会在小提琴图里叠加散点也就是所谓的“蜂群图”或“抖动散点”每个点代表一个真实样本——既保留原始数据透明度又弥补小提琴图在少量样本下的失真问题。这在 NC 里几乎是标配。2. 数据准备与工具选型复现之前先把地基打牢我见过太多人代码跑不通最后发现不是函数用错了而是数据结构从一开始就是乱的。绘图代码只是最后一公里前面九十九公里的数据整理如果没做好后面全是连锁反应。2.1 数据结构长格式 vs 宽格式ggplot2 有一个铁律它只能吃“长格式数据”long data。所谓长格式就是每一行是一个观测样本每一列是一个变量。以刚才那个“基因表达量”的例子来说数据框应该是这样的sample_idgroupresponseexpressionS01controlR6.23S02controlNR5.11S03treatmentR8.02............每一行代表一个患者的某个样本group列说明它属于哪个实验分组response列说明它是不是响应者expression列是我们要画到 y 轴上的数值。很多新手拿到手的数据是宽格式的比如不同列的列名是“control_R”“control_NR”“treatment_R”这种。这种格式给 ggplot2 用轻则报错重则画出完全错误的图。所以复现的第一步永远是检查你的数据是不是长格式。不是的话用tidyr::pivot_longer()把它转过来。library(tidyr) # 假设宽格式数据长这样 # sample_id, control_R, control_NR, treatment_R, treatment_NR df_long - df_wide %% pivot_longer( cols starts_with(control):starts_with(treatment), names_to c(group, response), names_sep _, values_to expression )这里names_sep _是个容易出错的地方如果列名里有多余下划线或者大写小写不统一拆分就会出问题。我的习惯是在整理原始数据时列名只用小写字母和下划线且下划线只出现一次否则后面所有处理都是在给自己埋雷。2.2 工具选择R ggplot2 还是 Python seaborn复现 NC 图表我强烈建议首选 R ggplot2。原因有三点一是 ggplot2 的图层语法逻辑和期刊图表的信息层级天然匹配你想在图上叠箱线图、叠散点、加统计线都是“往画布上堆图层”的操作非常直观二是 R 生态里统计检验的工具太全了ggpubr、rstatix这些包直接帮你把显著性标记画上去省掉手动计算 p 值再手动标注的麻烦三是 Nature 系期刊的图表风格半开主题、极简网格、克制的配色基本就是 ggplot2 主题定制出来的你很难在 Python 里找到这么顺手的“一键风格”。不是说 Python 不行。如果你的整个分析流程都在 Python 里用 seaborn 画sns.violinplot也完全能复现大致效果特别是splitTrue这个参数可以画出漂亮的“半分小提琴图”。但如果你要精细控制每个图层最后还要拼多图、调主题R 的灵活度更胜一筹。我自己是两套工具都用但最终出图基本都落到 R 上。标题里既然点名了 Nature Communications我就默认你要走“期刊级复现”这条路那工具选定为 R ggplot2 是性价比最高的选择。版本方面用 R 4.2 以上ggplot2 3.4 以上两个包就能覆盖大部分需求。部分进阶功能需要ggpubr、patchwork、ggsignif后面会具体提到。3. 用 ggplot2 一步一步画出分组小提琴图3.1 基础图层搭建假设数据已经整理成了长格式df_long我们要画的图是x 轴 处理分组fill 是否响应y 轴 表达量。先写出最基础的代码library(ggplot2) p_base - ggplot(df_long, aes(x group, y expression, fill response)) geom_violin( position position_dodge(0.8), width 0.6, alpha 0.7, scale width ) theme_classic(base_size 14)这里有两个参数值得单独拿出来说。第一个是position_dodge(0.8)。它控制同一 x 位置下两组小提琴之间左右错开的幅度。0.8 的意思是小提琴的总宽度占 x 轴单位宽度的 80%彼此之间留一点缝。这个值太小两组会叠在一起太大两个小提琴离得太远视觉上像两棵孤立的树。NC 的图里常见的 dodge 值在 0.7~0.9 之间我一般取 0.8。第二个是scale width。这是个特别关键但特别容易被忽略的参数。默认情况下ggplot2 中不同组的小提琴宽度是根据该组样本量来的样本量大的组小提琴更宽。这个默认行为在工作时往往不是我们想要的——你想比较的是“分布形态”而不是“样本量差异”在视觉上的喧宾夺主。设成scale width之后所有组的小提琴宽度统一极大值、极小值的组不会出现“一个大葫芦带一个小葫芦”的失衡感。这是我建议所有人复现 NC 图时第一件要做的事。alpha 0.7是给小提琴加一点透明度。因为在后续步骤里我们会在里面叠箱线图和散点太实的填充色会挡住后面的内容。透明度这一点文献里通常直接用实色但实际出图时半透明反而更高级也方便读者看到重叠在内部的点。3.2 叠加箱线图与散点信息密度的艺术基础小提琴画完之后图还“空”。NC 的图通常不会只放一把光秃秃的小提琴它会夹一层迷你箱线图再撒上原始样本点。箱线图直接用geom_boxplot叠上去但有一点必须注意它的position_dodge宽度必须和小提琴的完全一致否则箱线图会跑到小提琴外面去。p_box - p_base geom_boxplot( width 0.1, position position_dodge(0.8), outlier.shape NA, coef 0, alpha 0.8 )这里width 0.1控制箱线图“腰身”的粗细太宽会盖住小提琴的密度轮廓太窄又看不清箱体。coef 0的意思是不要画离群点因为我们已经要叠散点了离群点交给散点去展示箱线图只管五数概括就行。散点我用geom_jitter或者geom_point 手动抖动。注意散点的 dodge 宽度同样要和小提琴保持一致否则所有点都会挤在同一条垂线上完全看不出分布p_dot - p_box geom_jitter( position position_jitterdodge( jitter.width 0.15, dodge.width 0.8 ), size 1.2, alpha 0.6 )N 大的时候比如每组上百个样本直接叠散点会让图变成一坨墨迹。这时候有两种处理一是把点的透明度调到 0.3 以下让密集区域自然变深二是改用ggbeeswarm包里的geom_beeswarm让点像蜂群一样自动排布不重叠。NC 论文中那种每个点清晰可辨的效果大多是用 beeswarm 或者geom_quasirandom做出来的比纯 jitter 好看得多。3.3 分面分组维度再多一层时怎么办有些时候分组变量不止两个。比如研究里有“性别”这一层混杂因素你想分别看男性和女性里的处理效应。这时候硬塞进同一个坐标系会非常拥挤正确的做法是用facet_wrap分面。p_wrap - p_dot facet_wrap(~ sex, nrow 1)在 NC 图中你经常能看到一行两三个小图并排的排版每个小图就是同一个图在不同亚组下的切片。这种方式的好处是既保证了图形元素的一致性y 轴范围相同便于横向对比又避免了信息过载。用facet_wrap之后还有个细节默认情况下每个面都会带上自己的坐标轴刻度这在并排对比时其实没太大必要还占空间。我一般会加一句scales fixed默认就是 fixed同时把 x 轴标签只保留在每个面板的最下方p_wrap - p_wrap theme( strip.background element_blank(), strip.text element_text(size 13, face bold) )这样上下留白少了图更紧凑也更符合期刊那种干干净净的气质。3.4 配色与主题定制向 Nature 排版靠拢Nature Communications 的图风格说难听点叫“性冷淡”说好听点叫“克制”低饱和度的配色、无背景网格线、字体大小统一、标注精炼。复现的时候这几点一个都不能落下。配色方面我建议别用默认的 ggplot2 调色板那个偏花哨改用比较稳的配色方案。比如library(RColorBrewer) my_colors - c(#E64B35, #4DBBD5, #00A087, #3C5488)前两个是常用的红蓝对比适合“处理 vs 对照”“响应 vs 不响应”这类二分组。如果你要做多分组对比也可以直接用scale_fill_manual(values my_colors)手动指定。这里有一个经验双色对比尽量选“色相差异大、饱和度不高”的颜色红配蓝是最稳妥的红配绿在色盲读者眼里可能会翻车。p_final - p_wrap scale_fill_manual(values my_colors[1:2], labels c(Non-responder, Responder)) labs( x Treatment group, y Relative expression (log2), fill Response ) theme_classic(base_size 14) theme( legend.position top, legend.key.size unit(0.6, cm), axis.line element_line(color black, linewidth 0.5), axis.ticks element_line(color black, linewidth 0.5) )这里theme_classic是天然适合 NC 风的基础主题没有网格线只有两条坐标轴。我在此基础上加了黑色的轴线避免默认的灰色在打印后发虚。注意配色尽量不要用那些“荧光色”印刷出来会偏色偏得厉害。编辑和审稿人对颜色的偏好往往是“能区分、不刺眼”。你可以在一个阳光充足的屏幕上把图缩到 80% 再放大回 100%看看区分度还够不够明显——这是我检验配色的土办法实测很有效。4. 进阶统计检验、分面小图与期刊出图规范一张图画得再好看没有统计检验的支撑在 NC 这种期刊面前就等于“裸奔”。复现文献图表统计标记这块几乎是必选项。4.1 添加显著性标记最常见的需求是比较“处理组 vs 对照组”在特定分组下的差异然后把 p 值用星号或者数字标到图上。做法通常是用ggpubr::stat_compare_means也可以用ggsignif::geom_signif自己手动指定比较组。library(ggpubr) p_stat - p_final stat_compare_means( method wilcox.test, comparisons list(c(control, treatment)), label p.signif, label.y.npc 0.95 )这里的方法要谨慎选择如果你的数据大致正态且各组方差齐性可以用 t 检验如果像绝大多数组学数据那样偏态分布用 wilcoxon 秩和检验更稳。NC 的图里常见标注形式有p 0.003、p 0.001、或者直接用星号体系* p 0.05** p 0.01*** p 0.001。如果你的图里同时有好几组比较我建议用星号因为数字多了排版会乱而且编辑更喜欢“简洁”。如果你是按照response分组还想在每个group内比较 responder 和 non-responder 的差异那就需要换一种写法p_stat - p_final stat_compare_means( method wilcox.test, aes(label ..p.signif..), comparisons list(c(R, NR)), label.y.npc 0.9 )stat_compare_means默认会在每个面板即每个group里独立做分组检验非常省事。但要注意label.y.npc这个参数它是基于“面板内 y 轴范围比例”定位标签的设成 0.95 会放在靠近顶部的位置。如果画完发现标签跟数据点叠在一起微调这个数值就行。4.2 样本量标注与误差线期刊审稿人特别看重“你的结论是不是建在小样本上”。所以很多 NC 图会在 x 轴刻度标签下面或者小提琴底部标明每组样本量 n。做法很简单就是拼一个文本层n_label - df_long %% group_by(group, response) %% summarise(n n(), .groups drop) n_label$y - min(df_long$expression) - (max(df_long$expression) - min(df_long$expression)) * 0.08然后把这个n_label映射上去。不过更干净的做法是直接改 x 轴的标签把“Control (n20)”直接写进刻度标签里new_labels - c( Control (n20), Treatment (n22) ) p_final - p_final scale_x_discrete(labels new_labels)这个方案的缺点是如果同一个 x 分组下还有response两个子组那这里的 “n” 就会是合计样本量容易误导。所以我实际出图时更倾向于把 n 标注成子图内的文字注释或者放在图例里。图例里面的写法是p_final scale_fill_manual( values my_colors, labels c(Non-responder (n12), Responder (n16)) )这种做法干净、直接而且不挤占绘图区域是我最推荐的方案。4.3 输出矢量图与分辨率设置你辛辛苦苦画好的图如果在最后导出的时候用了个 72 dpi 的 JPG 发给了导师那前面所有工作都白费了。期刊投稿对图片的最低要求通常是 300 dpi但最佳实践永远是存矢量图PDF 或 SVG。ggsave( Figure_1_violin.pdf, plot p_final, width 7, height 5, units in, dpi 300, device pdf )这里device pdf会保证里面的文字、线条全部变成矢量元素放大多少倍都不糊。如果你投稿系统要求 TIFF那就得先输出一个高分辨率位图ggsave( Figure_1_violin.tiff, plot p_final, width 7, height 5, units in, dpi 600, compression lzw )TIFF 用 600 dpi 是个稳妥的保险值因为有的期刊在印刷时会按 600 dpi 重新采样纯 300 dpi 的图印出来有时候会边缘发虚特别是那些细线条。用compression lzw是为了做无损压缩文件体积小很多但画质不受影响。字体方面如果你用的是中文系统且 R 里没有配置好字体导出 PDF 时容易出现字体缺失或乱码。稳妥做法是出图时用系统自带的英文默认字体R 默认通常能处理中文字体一定要在投稿前确认嵌入了。我的习惯是终稿一律把字体切成Arial或Helvetica这类无衬线字体统一又干净p_final - p_final theme(text element_text(family Arial))5. 复盘与避坑复现过程中最常见的几个问题下面这部分是纯经验总结。我当年第一次复现 NC 的小提琴图前前后后折腾了一个多星期现在回头看至少有一半的时间都花在了一些文档里根本不会写的奇怪 bug 上。5.1 常见问题速查表现象原因解决方法箱线图漂在小提琴外侧geom_boxplot的 dodge 宽度和小提琴不一致确保两者的position_dodge宽度完全一致小提琴图一片糊看不出密度形态样本量太大核密度带宽选择不当调adjust 0.5或bw SJ或改用geom_quasirandom展示点两组小提琴宽度悬殊默认scale area宽度由样本量决定改成scale width散点全部堆在一条线上geom_jitter的抖动宽度太小jitter.width调到 0.15~0.3且dodge.width必须匹配p 值标签重叠、位置飘出画面label.y.npc设定不合理缩小数值或改用label.y直接指定绝对坐标导出的 PDF 打开后中文字体乱码字体未嵌入换用 Arial 或嵌入字体后再导出同一个 x 分组下的两组小提琴间距太窄position_dodge(0.8)小于实际需要的值调大 dodge或者把 x 轴映射改成interaction(group, response)分面后各子图坐标范围不一致对比时误读需要独立尺度或者固定尺度按需设scales free但注意对比公平性这个表不是给出来好看的是每一行都是我或周围的人真实踩过的。尤其那个“箱线图漂移”的问题新手几乎必然遇到。其实原理很简单ggplot2 的position_dodge并不会自动同步多个图层之间的躲避宽度你不显式设置箱线图就会按自己的默认宽度0.75躲和小提琴不一致自然错位。所以写代码的时候养成把所有图层的position_dodge参数都显式写出来的习惯能救你命。5.2 一些值得砸进脑子里的心得第一数据容错率比代码容错率重要得多。我复现时吃过最大的亏不是画图而是整理数据的时候把某个样本的组别标错了一个字母结果小提琴图里多出了一个“幽灵群”。后来我养成了一个习惯画图之前先跑一行table(df_long$group, df_long$response)看看交叉频数和预期是否一致。这行代码 1 秒钟就能跑完能挡住 90% 的数据错漏。第二不要过度美化。有的朋友画完图觉得太平淡硬要加渐变填充、加阴影、加 3D 效果。NC 的风格从来都不是炫技而是“信息尽可能清晰”。你加了再多装饰审稿人看的还是数据本身。我记得我导师说的一句话“图表是数据的翻译官不是数据的化妆师。”这话糙理不糙现在也是我审稿时的第一观感来源。第三图例位置很有讲究。NC 的图例通常放在顶部或右侧我自己的偏好是顶部居中。legend.position top配合legend.justification center就可以实现。图例放顶部有个好处横向分面的时候每个小图的宽度可以更大数据的主体展示区不会被图例挤压。第四如果你要画的是那种“半分小提琴”即一组数据只画半边而另一半留给对照组可以用geom_split_violin的现成实现比如see包里的geom_violin配合split TRUE。这种图在 sample 对照设计里特别常见但注意它的数据格式要求更严格x 轴必须是两两配对的分组变量。复现前先确认你的数据是不是这种结构。最后再说一个使用patchwork拼图的小技巧。NC 的正式图表往往是 A、B、C 三个子图拼成一张 Figure小提琴图可能只是其中的 Panel A。我会用patchwork包做拼接library(patchwork) combined - p_stat p_heatmap p_curve plot_annotation(tag_levels A) theme(plot.tag element_text(size 18, face bold))tag_levels A就是自动生成左上角的大写字母标记。这个包在最开始用的时候容易犯一个错忘记在所有子图后面加来统一主题导致三个图的字体大小不一致拼起来非常难看。统一主题的写法就是上面这种把所有theme统一到后面拼出来的图才有一体感。我自己在实际操作里还有一个执念每次把拼好的图导出前都会用眼睛在“缩小到 50%”和“放大到 200%”两个缩放级别各看一遍。缩小看整体布局是否失衡放大看细节线条是否发虚、点是否糊成一团。这一步看起来很笨但能提前发现很多让编辑皱眉的问题。做完这些检查一张能拿去投稿的分组小提琴图才算真正落地。后面你再复现别的图其实都是同一个套路——先把图读明白再把数据捋顺然后一层图层地往上搭最后死磕输出细节。这条路走一遍后面就通了。