☰
R语言ggplot2绘制Nature风格小提琴图与箱线图组合完整教程
2026/10/10 20:50:48 网站建设 项目流程

搞科研的人应该都有同感:翻 Nature 论文的时候,看到 Figure 里那排“小提琴图+箱线图”组合,觉得干净、清楚、信息量足,心里默默想着“也就那样嘛,画个图而已”。等真自己动手,在 R 里复现出来,才发现问题一个接一个——小提琴宽窄不一、箱线图被完全埋住、队形歪歪扭扭、配色土得不敢提交。今天这一篇就把这个组合从头到尾拆干净,从数据整理、图层顺序、核心参数到最终导出,完整复现一张能放进论文配图的 Nature 风格图表。主工具是 R 的 ggplot2,这也是科研图表复现里公认最顺手、上限最高的路线。如果你平时只用 Excel 画图,也没关系,每一步我都会讲清“为什么这么写”,你看完能直接拿去改自己的数据。

1. 先搞清楚:这个组合凭什么成为论文配图的“默认选项”

1.1 两种图分工不同,合在一起才信息密度拉满

先说箱线图。箱线图展示的是五个数字的统计摘要:最小值、下四分位数、中位数、上四分位数、最大值,外加孤立的离群点。它最大的优点是一眼就能读出分布的位置和离散程度,缺点是它把整个分布压缩成了几个分位数,数据的“形状”信息基本丢了。

小提琴图刚好补上这块短板。它本质上是在数据上做核密度估计,然后把密度曲线左右镜像,形成一个对称形状,像一把倒扣的小提琴。宽度代表数据堆叠的密集程度,越鼓的地方说明越多样本落在那个区间。单峰、双峰、偏态、厚尾,这些箱线图完全表达不出来的特征,小提琴图一目了然。

这两种图合在一起,效果就是“照片”加“标尺”:小提琴负责展示分布长什么样,箱线图负责给关键位置提供精确刻度。我印象很深的一次分析里,两组数据的中位数几乎一样,箱线图画出来基本是复制粘贴;可换成小提琴图之后,其中一组明显是双峰分布,两个亚群的存在立刻暴露出来。这种差别在论文里恰恰是决定结论走向的信息。Nature 这类期刊喜欢这个组合,就是因为它能在同样一块版面里塞进最多的分布细节。

1.2 不是所有场景都适合,选错组合等于画蛇添足

组合图虽好,但要分清使用场景。从我自己的经验看,组别数量在 2 到 8 组、每组连续型数值样本量在 10 到 100 之间,是小提琴图加箱线图的舒适区。样本量少于 5 时,核密度估计结果很不稳定,小提琴形状会出现莫名其妙的毛刺,这种时候直接画点图加箱线图更诚实。组别超过 10 组时,横轴空间被压扁,小提琴之间互相挤压,读者也会失去耐心。

还有一类情况不太适合:计数数据、比例数据、或明显属于离散分布的数据,用这个组合容易产生误导。计数数据的“分布”往往集中在少数几个整数值上,核密度图会画出一些不存在的中间值,等于人为制造平滑假象。这种数据我一般改用点阵图或直接按原始分布展示更稳妥。

1.3 技术路线:为什么我首选 R 加 ggplot2

复现这类图表,技术选型其实是个小分水岭。主流选项无非 R 的 ggplot2 和 Python 的 seaborn 加 matplotlib 两种。我在实际项目里更偏向 R,核心原因是图层语法和主题定制能力更顺手。

ggplot2 的优势在三个地方。第一,图层语法非常直白,加散点、加箱线、加文本标注,本质都是往上叠一层,复盘和改动都方便。第二,主题系统灵活,轴线、网格、字体、颜色都能精确控制,这正好是复现 Nature 图表最需要的“微调能力”。第三,R 的统计检验生态成熟,t.test、wilcox.test、多重比较校正这些操作就在同一个语言环境里,算完 p 值直接标到图上。

Python 不是不能做,seaborn 里violinplot加boxplot也能快速堆出类似效果。但我在几个项目里实测下来,越往后做细节定制,越觉得 seaborn 的“快速但粗放”会拖后腿——尤其是显著性标记的定位和主题细节,改起来比 ggplot2 费劲。所以这篇文章的主线我全部用 R 写,Python 党看到思路后自己平移过去也完全没有障碍。

2. 数据整理:复现图表前最容易翻车的一步,别急着开画

2.1 长格式和宽格式,这两者必须分清楚

ggplot2 的设计核心是“长格式数据”,也就是每一行是一个观测值。做组间比较时,最简单的长格式表格长这样:

groupvalue
Control10.2
Control11.7
Treatment113.1
Treatment112.4

对应的宽格式则是把每个组各占一列,比如group1,group2,group3各一列数值。不少刚上手的人把 Excel 里整理好的宽表格直接丢给 ggplot,结果画出来要么报错要么完全不对。原因很简单:ggplot 的美学映射里,x 轴对应组别变量,y 轴对应数值变量,这天然要求两列构造。

如果你的原始数据是宽格式,用 tidyr 包的pivot_longer()转换就行:

library(tidyr) data_long <- data_wide %>% pivot_longer(cols = starts_with("group"), names_to = "group", values_to = "value")

转换完记得检查str(data_long)和head(data_long),确认组别列是因子、数值列是数值型。这一步看着基础,实际项目里八成的问题都出在这。

2.2 因子顺序决定横轴顺序,别默认让字母表替你排

另一个非常容易翻车的点是组别在横轴上的顺序。默认情况下,factor的顺序按字母排:Control、Treatment1、Treatment2 这种还好,可一旦组名是 A、B、C 而你实际想按实验流程排 C、A、B,就直接错位了。

解决办法是显式用factor()指定水平顺序:

data_long$group <- factor(data_long$group, levels = c("Control", "Treatment1", "Treatment2"))

这一步不仅要提前做,而且要在画图前确认。图例里的顺序、颜色分配的顺序,全部由这个因子水平顺序决定。你后面配色的色板顺序如果和它不一致,会看到颜色张冠李戴,排查起来特别头疼。

2.3 数据清洗与离群值:先别急着删,离群值也是信息

我见过太多人拿到数据,看到几个离群值,第一反应就是删掉再画图。这在某些情况下没问题,但更多时候是危险的。离群值可能是实验里真实存在的重要信号,也可能是数据录入错误。正确做法是先检查原始记录,确认不是录入问题后再决定是否保留。

小提琴图和箱线图本身就是展示离群值的好工具——箱线图把离群点单独画出来,小提琴图则会把它们体现在长长的拖尾里。如果你的数据量非常大,比如单组上千个样本,叠加散点会糊成一团,这时可以只保留小提琴图加箱线图,不再叠点。另一种思路是做分面,按批次或重复拆到多个小图里,既保留细节又避免单图过载。

3. 核心绘图:用 ggplot2 画出干净的“Nature 风”组合图

3.1 先准备一套可复现的模拟数据

为了方便演示,我先造一份模拟数据。真实使用时,把data.csv读进来替换这部分即可。

library(ggplot2) library(dplyr) set.seed(42) data <- data.frame( group = rep(c("Control", "Treatment1", "Treatment2"), each = 50), value = c( rnorm(50, 10, 2), rnorm(50, 13, 2.5), rnorm(50, 9, 1.8) ) ) data$group <- factor(data$group, levels = c("Control", "Treatment1", "Treatment2"))

这份数据共 150 行,三个组别,每组 50 个观测。数值上故意让三组的均值、离散度都有差异,方便看出图形效果。

3.2 基础图层:先画小提琴,再往上叠箱线图

核心代码其实只有两行几何对象:

p <- ggplot(data, aes(x = group, y = value, fill = group)) + geom_violin(trim = FALSE, width = 0.7, scale = "width") + geom_boxplot(width = 0.12, outlier.shape = NA, fill = "white") + theme_classic(base_size = 14) print(p)

关键点在于geom_violin的三个参数:

  • trim = FALSE:默认trim = TRUE会把核密度估计的尾部截断在数据最值处,画出来的小提琴底部是平的一条线,非常丑。改成FALSE后,曲线会自然平滑收尾到零密度,更接近 Nature 论文里那种流畅感。
  • scale = "width":让所有组的小提琴图宽度一致。这样组与组之间的比较重心落在形状差异上,视觉上更整齐。scale = "area"则会让面积随样本量变化,scale = "count"会让宽度随样本量变化。论文复现里我绝大多数时候用"width"。
  • width = 0.7:控制单组小提琴占横轴组距的比例。数值越大图形越宽,0.7 左右是比较均衡的选择。

geom_boxplot里同样有几个细节要留意:

  • width = 0.12:箱线图宽度占整组宽度的比例。太宽会把小提琴中部完全盖住;太窄则读不清分位数。0.1 到 0.15 这个区间是我实测下来最舒适的。
  • fill = "white":白底箱线图从彩色小提琴里“跳”出来,视觉上清爽,这也是 Nature 配图最常见的处理。
  • outlier.shape = NA:因为后面会叠原始点,如果不关掉离群点,同一个点在小提琴图和箱线图里重复标记,很乱。

3.3 要不要叠加原始点?两种思路看场景

在小提琴图加箱线图的组合里,是否再叠一层原始数据点,取决于样本量和论文想要传达的细节程度。

样本量不大、每组在几十个以内时,我非常推荐加geom_jitter:

p <- p + geom_jitter(width = 0.15, size = 1.5, alpha = 0.4)

这里width = 0.15代表点在横轴方向的随机抖动幅度,避免大量点重合在一起;alpha = 0.4做半透明处理,重叠越多颜色越深,相当于把密度信息又映射了一层。这一层放在geom_violin之前、箱线图之前还是之后,要按需调整。我一般先画geom_jitter再画小提琴,这样半透明点会“渗透”出来,不会盖住箱线图。

另一种方案是用geom_dotplot(binaxis = "y", stackdir = "center"),把每个点排成整齐的“列兵阵”,适合样本量偏小、想展示每一位原始数据的场景。它的缺点是遇到大量重复值时堆叠高度失控,调试成本高。

如果单组样本量超过 200,我建议放弃叠加点图层。黑压压的一片不仅破坏干净感,还会把小提琴的轮廓彻底淹没。这种情况下只保留小提琴加箱线图就足够了,必要时在文字里报告具体样本量。

3.4 配色与主题:把“论文质感”落实到最后一步

theme_classic()提供了一个很好的起点:白色背景、两条轴线、无网格线。但距离 Nature 那种“定制感”还需要微调配色和字体。

配色上,我最常用的是自定义手动配色:

my_colors <- c("#4472C4", "#ED7D31", "#A5A5A5") p <- p + scale_fill_manual(values = my_colors)

这三个颜色是蓝、橙、灰的经典组合,在 Scientific American 和不少 Nature 子刊里都常见。如果你想要更稳的学术站点风格,可以用scale_fill_brewer(palette = "Set2")这类现成方案,但要注意调色板顺序与因子水平顺序一致。

主题定制方面,我通常这样处理:

p <- p + theme_classic(base_size = 14) + theme( text = element_text(family = "Arial"), axis.title = element_text(size = 11), axis.text = element_text(size = 10), axis.ticks.length = unit(-0.15, "cm"), plot.margin = margin(t = 10, r = 10, b = 10, l = 10), legend.position = "none" )

legend.position = "none"的意思是:如果组别在横轴上已经写明了,图例就是多余的,干脆关掉。axis.ticks.length若设为负值,刻度线会朝内,这种细节在很多期刊成品图里都有,亲测能让图显得精致不少。

4. 图上非要有结论:显著性标记怎么加才不乱

4.1 先算统计量,再考虑画图

在往图上加星号之前,先把统计检验做完。两组比较时,数据大体对称且近似正态,用t.test();分布偏态明显或样本量很小,用wilcox.test()这种非参数检验更稳妥。我的习惯是先跑一下 Shapiro-Wilk 正态性检验,或者直接看小提琴图的形状来辅助判断。

多组比较时,事情稍微复杂一些。通常论文配图里标注的是主要比较的两组,而不是所有两两组合都标一遍。如果做了多组两两比较,务必要考虑多重比较校正,用p.adjust()把 p 值转为校正后的数值再判断星号。不校正而直接标三五个星号,审稿人大概率会质疑。

4.2 用 annotate 手动标注,位置完全由你把控

Nature 图里最常见的显著性标注形态是两组顶部画一条横线,横线正上方标星号。手动实现其实不复杂:

# 先算一个合适的标注高度 y_max <- max(data$value) y_bracket <- y_max + 0.05 * diff(range(data$value)) p <- p + annotate("segment", x = 1, xend = 2, y = y_bracket, yend = y_bracket) + annotate("segment", x = 1, xend = 1, y = y_bracket, yend = y_max + 0.02 * diff(range(data$value))) + annotate("segment", x = 2, xend = 2, y = y_bracket, yend = y_max + 0.02 * diff(range(data$value))) + annotate("text", x = 1.5, y = y_bracket + 0.02 * diff(range(data$value)), label = "**", size = 5)

这段代码的思路是:先算出整组数据的最大值,再留出 5% 左右的余量作为括号高度,然后用三个annotate("segment")画一条横线和两条竖线,最后放一个星号文本。好处是坐标可以精确控制,不会因为数据更新而跑到图外。坏处是每次换数据都要重新算高度。如果嫌麻烦,可以自己写一个小函数,输入两个组名和 p 值,自动算出合适高度再标注,一劳永逸。

ggpubr::stat_compare_means()这种自动标注工具我也用过,适合快速出图看趋势,但它的标注位置经常跟设计稿打架,微调起来不如手动annotate灵活。复现 Nature 图表时,尤其是多组布局,我基本都回归手动标注。

4.3 多组比较时的布局思路:避免“天女散花”

当图里有 4 个组、需要标 3 组比较时,最容易出现的问题是标注线互相交叉、高度挤在一起。我的经验是两个原则。第一,优先从左侧开始标,比较线的高度逐次递增,尽量让组间比较的视觉关系沿着横轴从左往右展开。第二,如果比较线必须有高低差,高一点的比较线不要压在低一点的图例元素上。胸外科手术级别的严谨当然用不上,但至少要让读者一眼看清哪两个组在比。

还有一个常被忽略的细节:显著性标记的 y 高度必须高于该组所有数据点包括离群点,否则星号会被点盖住。我一般取该组最大值再往上加半个四分位距,或直接按全组长度的 5% 抬升。

5. 常见问题与排查实录:这些都是我踩过的坑

5.1 小提琴图底部像被“砍了一刀”

这个现象十有八九是trim参数没改。默认geom_violin()的trim = TRUE,会把核密度估计的尾部截断到数据的最小值、最大值为止,于是底部看起来是一条平线,像是图被削了一刀。改成trim = FALSE后,曲线会自然收尾,平滑度立刻不一样。

还有一种可能:数据里存在大量刚好等于最小值的样本,密度在边界处急剧上升,即使trim = FALSE,也会看到底端有一条高密度“贴地”延伸。这种情况我会选择在统计描述里单独说明地板效应,而不是强行美化图形。

5.2 箱线图被小提琴完全“吞掉”

先从图层顺序排查。geom_violin必须写在geom_boxplot之前,后画的图层永远覆盖先画的。其次看箱线图的fill有没有设置。如果两者的 fill 一样,箱线图会融入小提琴里看不出来。经典解法就是fill = "white"或fill = NA。

最后检查width。箱线图width = 0.12这种比较薄的比例才合适,如果你写成0.5,在宽小提琴下面必定被淹没。

5.3 各组样本量差好几倍,画出来比例失真

样本量 10 对 100 的小提琴图,如果还用scale = "width",两组宽度一样,读者看不出样本量的差异。这时有两种思路。一是改成scale = "count",让小提琴宽度直接与样本量挂钩,图面能直观体现数据“底数”的不同,缺点是美观度下降。二是保持scale = "width",在图注里写明 n 值。Nature 图里常见的是第二种,因为审稿人更关心分布形状的比较,而不是让小提琴的胖瘦替代样本量表。

5.4 保存出来的图发虚、字太小、糊成一团

复制论文原图风格时,很多人栽在导出设置上。ggsave()的核心尺寸要按目标期刊栏宽来设定。Nature 单栏图片宽度约 89 毫米,双栏约 183 毫米。换算成英寸,就是 3.5 英寸和 7.2 英寸左右。

ggsave("figure_violin.pdf", p, width = 3.5, height = 3, dpi = 300) ggsave("figure_violin.png", p, width = 3.5, height = 3, dpi = 600)

印刷级图片我一般优先导 PDF 矢量格式,文字和图形在任何缩放级别下都保持清晰;PNG 则用来预览和投审稿系统,dpi 至少 300,做位图导出时 600 更稳妥。字号方面,RStudio 预览里看着合适的字号,导出到 3.5 英寸宽后往往偏小。经验值是主题里base_size设在 10 到 12,输出 3.5 英寸宽时对应大约 8 到 9 号字体,基本符合期刊要求。

5.5 中文标签显示成方块

中文的用户名和坐标轴文字在 Windows 上经常遇挫。element_text(family = "Arial")这种西文字体对中文无效,必须指定中文字体,比如"SimHei"、"PingFang SC"或"Microsoft YaHei"。如果期刊要求全英文,其实最省事的方案就是把组别标签和轴标题全部改成英文,彻底绕开字体问题,也更贴合 Nature 这类国际期刊的语言环境。

6. 一套可以直接抄的完整代码

最后把前面所有内容整理成一份完整脚本,数据替换成你自己的即可。

library(ggplot2) library(dplyr) # 读入长格式数据,示例结构:两列 group / value data <- read.csv("your_data.csv", stringsAsFactors = FALSE) data$group <- factor(data$group, levels = c("Control", "Treatment1", "Treatment2")) my_colors <- c("#4472C4", "#ED7D31", "#A5A5A5") p <- ggplot(data, aes(x = group, y = value, fill = group)) + geom_jitter(width = 0.15, size = 1, alpha = 0.4) + geom_violin(trim = FALSE, width = 0.7, scale = "width") + geom_boxplot(width = 0.12, outlier.shape = NA, fill = "white") + scale_fill_manual(values = my_colors) + theme_classic(base_size = 12) + theme( text = element_text(family = "Arial"), legend.position = "none", axis.title = element_text(size = 11), axis.text = element_text(size = 10) ) + labs(x = NULL, y = "Expression Level") ggsave("figure_violin_box.pdf", p, width = 3.5, height = 3) ggsave("figure_violin_box.png", p, width = 3.5, height = 3, dpi = 600)

跑完这份代码,你得到的就是一张白底、黑轴线、三组彩色小提琴、白底箱线图叠加半透明原始点的组合图,基本贴合 Nature 子刊常见的“素净风格”。

我自己复现这种图已经不知道多少回了,最大的体会是:论文图表丑,通常不是审美问题,而是图层顺序、参数细节和数据格式这三件事没理顺。把这个组合图彻底吃透之后,你能迁移到密度图、分面小提琴图、堆叠箱线图等各种变体,因为它们底层都是同一套逻辑。后续你还可以在这个基础上继续加统计标记、改配色方案、调整分面布局,上限很高。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询