资讯动态

R语言ggplot2绘制带显著性标注的堆积箱线图实战

发布时间:2026/10/14 4:36:18 来源:尧图企业网站定制
帮课题组处理数据这几年被问到最多的一类图就是能不能在这个箱线图上面把显著性标出来一开始我以为只是加个星号的事真正上手才发现科研绘图里大家口口相传的堆积箱线图其实是一套组合表达——把数据分布、散点原始值、组间比较的显著性标注整合在同一张图里。这篇就把我用R语言做这类带显著性差异标注的堆积箱线图stacked boxplot with label的完整思路、操作细节和踩坑记录都写出来适合刚接触R的科研新手也适合想优化图表表达的老手。1. 图形拆解与设计逻辑先想清楚这张图到底要表达什么1.1 堆积不是堆柱子而是三层信息叠加很多刚接触R绘图的人会把堆积图理解成百分比堆叠柱状图但科研语境里的堆积箱线图并不是那个意思。它指的是在一张图上把多种数据呈现方式叠加在一起形成信息层次第一层箱线图boxplot或小提琴图violin plot展示数据的中位数、四分位距、分布形状第二层原始数据点散点或抖动点让每个样本的实测值直接落在图上避免箱线图掩盖样本量和小众分布第三层显著性标记星号、字母、括号线段标注组间比较的统计检验结果。这三层信息的组合价值在于审稿人和读者不需要在正文里翻找统计表看图就能同时确认数据长什么样和差异显不显著。我之前做过一次组学对比数据的可视化对照组和处理组样本量都只有6个如果只画箱线图中位数看起来差不多把散点叠上去之后两组数据分离度一目了然再加一条显著性标记线结论就非常直观。1.2 为什么推荐用ggplot2体系R语言里做这类图主流路径就是基于ggplot2的语法体系。原因有三个第一ggplot2的图层layer机制天然适合叠加这种需求。先画箱线再加散点再叠加标记线每一步都是往图上增加一个几何对象逻辑清楚代码好改。你不需要像base绘图那样手动计算坐标、逐段画线。第二统计标注有成熟扩展包。ggsignif和ggpubr都专门为组间比较标注设计提供了现成的显著性检验和标记函数比自己手写p值计算和画线高效得多。第三主题和配色可定制程度高投不同的期刊时能快速切换风格。我之前帮一个师弟调整配色从默认的蓝红改成目标期刊要求的灰度系只改了两行代码。1.3 这种图适合什么场景带显著性标签的堆积箱线图最常用于生物学、医学、农学等领域的组间比较分析典型场景包括对照组和处理组的指标差异展示如基因表达量、生理生化指标时间点或浓度梯度分组的结果对比多样本多个细胞系、多个品种、多个批次的测定值对比。如果你是做纯数值模拟或者工程数据其实不太需要这种图——因为你的分组往往不是生物学重复显著性检验的前提不一定满足。所以动笔之前先确认自己的数据结构和统计设计别为了好看硬画。2. 数据准备与统计检验前置画图之前先把数据结构和检验方法定清楚2.1 数据格式必须整理成长格式ggplot2做分组绘图时最顺手的永远是长格式数据long format。也就是每一行是一个观测样本包含三列样本ID、分组列、数值列。示例# 模拟一个处理组 vs 对照组的数据集 set.seed(123) data - data.frame( group rep(c(Control, Treatment), each 30), value c(rnorm(30, 10, 2), rnorm(30, 14, 2.5)) ) head(data)group value 1 Control 8.879049 2 Control 9.539645 3 Control 13.117417 ...如果你手里的数据是宽格式每一列是一个分组用tidyr::pivot_longer()转成长格式library(tidyr) long_data - wide_data %% pivot_longer(everything(), names_to group, values_to value)这一步非常重要。我见过太多人直接在ggplot里塞数据透视表最后pivot操作绕来绕去代码变得不可读。数据整理干净后面所有绘图和统计都顺畅。2.2 分组顺序因子水平的坑必须提前排如果不主动设置因子顺序ggplot会按字母顺序排列分组Control和Treatment虽然有逻辑关系但图上可能先画Control没问题换个场景比如低剂量、中剂量、高剂量字母顺序就乱套了。需要显式指定因子水平data$group - factor(data$group, levels c(Control, Low, Medium, High))这个顺序同时决定了图例顺序、箱线图排列顺序、显著性标记的摆放顺序。养成习惯数据准备阶段就把因子顺序锁定不要在绘图阶段反复调。2.3 提前确定统计检验方法显著性标注要标什么取决于你怎么做统计。常见的几类情况两组比较两组独立样本先做方差齐性检验如var.test()或car::leveneTest()方差齐用t检验方差不齐用Welch修正t检验数据不服从正态分布就用Wilcoxon秩和检验。R里的检验函数很直接# t检验 t.test(value ~ group, data data, var.equal TRUE) # Wilcoxon秩和检验 wilcox.test(value ~ group, data data)多组比较多组三组以上不能两两反复做t检验这会放大一类错误。正确做法是先做方差分析ANOVA或Kruskal-Wallis检验再做事后多重比较post-hoc。常用方案参数检验aov()TukeyHSD()非参数检验kruskal.test()dunn.test()需要dunn.test包或FSA::dunnTest()。# 单因素方差分析 fit - aov(value ~ group, data long_data) TukeyHSD(fit)事后比较结果会返回每两组之间的p值这些p值才是你画在图上星号或字母的依据。配对/重复测量数据如果同一批样本在不同时间点或不同条件下测了多次需要用配对检验或重复测量方差分析。画图时别忘了在散点层用线段把同一个样本的点连起来。这个细节画没画代表你是否真的理解了实验设计。2.4 多重比较校正别忽略这一步做多组两两比较时p值需要校正常见方法有Bonferroni和FDRBH法。校正后p值变大图上标注的显著性也可能从**变成*甚至ns。这是正常的也是正确的。p_values - c(0.03, 0.008, 0.012, 0.15) p.adjust(p_values, method fdr)我习惯在投稿前把校正前后的p值都看一遍心里有数。不然图上传到系统后被审稿人追问N是否校正过会很被动。3. 核心绘图实现从箱线图到散点叠加一步步搭出层次感3.1 基础箱线图先画一个干净的箱线图确认分组、颜色、坐标轴标题等基础元素没问题。library(ggplot2) p - ggplot(data, aes(x group, y value, fill group)) geom_boxplot(width 0.6, outlier.shape NA, alpha 0.7) labs(x Group, y Expression Level) theme_classic() print(p)几个参数说明一下width 0.6控制箱体宽度分组多个时调窄更协调outlier.shape NA先隐藏离群点后面会用散点层统一呈现所有点避免离群点重复绘制alpha 0.7设置填充透明度配合后续散点层看得更清楚。如果数据分布呈现明显的多峰或偏态可以考虑用geom_violin()先画小提琴图再叠加箱线图能同时看到分布的核密度形状和中位数四分位范围。很多高影响因子的论文偏好这种组合。ggplot(data, aes(x group, y value, fill group)) geom_violin(width 0.8, alpha 0.5) geom_boxplot(width 0.15, outlier.shape NA, fill white) theme_classic()3.2 叠加原始数据散点加了箱线图之后一定要考虑样本量。样本量小每组少于10个所有点都画出来用geom_jitter()加随机抖动样本量中等每组10~50个画点但设置透明度用geom_jitter(alpha 0.4)样本量很大每组几百个画点会非常拥挤通常不画原始点改为箱线图误差棒。p - p geom_jitter( width 0.15, size 2, alpha 0.6, color grey30 )width0.15表示水平方向的抖动范围太小了点会重叠在一起太大了会让数据点跑出箱体范围太多。经验值一般设置在0.1~0.2之间看你的分组数量调整。如果有多组且组内又要区分亚组比如不同批次可以用position_jitterdodge()让点自动避开箱体。3.3 自定义配色避免默认配色千篇一律ggplot默认配色在屏幕上看着还行印刷之后往往太艳。我一般用自定义填充色custom_colors - c( Control #4DBBD5, Low #00A087, Medium #E64B35, High #3C5488 ) p - p scale_fill_manual(values custom_colors)也可以直接用现成的配色包比如ggsci系列里的期刊配色、RColorBrewer的Set2等。关键原则图和文字之间要有足够区分度不要用深色底配深色字也不要大红大绿同时出现。3.4 边框、轴线、字体大小投稿级别的细节期刊对图的要求一般是字体直接用theme_bw()或theme_classic()配base_size 14坐标轴标题加粗轴刻度线往外交替显示图例要放图内还是图外根据期刊要求和图宽度决定。p theme_classic(base_size 14) theme( axis.title.x element_text(face bold, size 14), axis.title.y element_text(face bold, size 14), axis.text element_text(color black, size 12), legend.position top, legend.title element_blank() )如果你需要对接某些期刊的模板最好在投稿前把RGB色值换成CMYK色值。R导出PDF时其实会保留颜色信息但在线上预览阶段很多系统会自动转成RGB最终印刷可能略有色差。这不影响评审但高质量图最好提前处理。4. 显著性标注的三种实现方式包函数、自动标注、手动精调4.1 用ggsignif包快速加标记线和星号ggsignif是专门做组间显著性标注的扩展包用法简单核心函数geom_signif()。library(ggsignif) p geom_signif( comparisons list(c(Control, Treatment)), test t.test, map_signif_level TRUE, y_position 18, tip_length 0.03 )comparisons用列表指定要比较的分组对test指定检验函数这里用t.test也可以是wilcox.testmap_signif_level TRUE把p值自动映射成星号***、**、*、nsy_position标记括号线段放置的最大高度需要根据数据范围调整。多个比较可以写多组comparisonsp geom_signif( comparisons list( c(Control, Low), c(Control, High), c(Low, Medium) ), test t.test, map_signif_level TRUE, y_position c(18, 22, 26), step_increase 0.1 )4.2 用ggpubr包自动计算p值和标签ggpubr里的stat_compare_means()也非常常用它用起来更自动library(ggpubr) p stat_compare_means( comparisons list(c(Control, Treatment)), method t.test, label p.signif, label.y 18 )它的主要参数method选择统计方法t.test、wilcox.test、anova、kruskal.testlabel控制显示内容p.signif显示星号p.format显示具体p值也可以p.adj显示校正后p值label.y手动指定标签的纵坐标位置bracket.size括号线段的粗细。stat_compare_means()内部会自动调用检验函数并提取p值很方便但它会重新做一次检验。这里有个细节最好保持和论文统计方法一致不要图上一个方法、正文统计另一个方法。有一年我帮人改稿图里用了Wilcoxon正文写的t检验被发现后打回来重跑所有数据教训很深刻。4.3 手动标注方案彻底掌控每一个标签位置扩展包虽然方便但遇到不对称分组、特殊样式需求比如字母标注法时还是得手动控制。手动标注的核心是自己算好每组的检验p值然后往图上画标记线和文字。思路是这样的先用ggpubr::compare_means()批量算好各组p值library(ggpubr) stat_res - compare_means( value ~ group, data data, method t.test, p.adjust.method fdr ) print(stat_res)然后把结果整理好自己有心理预期再用annotate()或geom_text()画标注p annotate(segment, x 1, xend 2, y 18, yend 18, linewidth 0.6) annotate(segment, x 1, xend 1, y 17.5, yend 18, linewidth 0.6) annotate(segment, x 2, xend 2, y 17.5, yend 18, linewidth 0.6) annotate(text, x 1.5, y 18.5, label ***, size 5)手动标注虽然麻烦但是胜在精确想画成什么样就画成什么样。字母标记法如果你的分组多到四五个用星号会画不开很多期刊改用字母标记法a、b、c。基本思路是事后多重比较结果出来后同一字母内差异不显著不同字母间差异显著。字母一般标在每个箱体的正上方。# 假设计算出每组字母标签 label_data - data.frame( group c(Control, Low, Medium, High), label c(a, a, b, c), y c(16, 17, 18, 19) ) p geom_text( data label_data, aes(x group, y y, label label), size 5, vjust 0 )这里字母y坐标需要手动微调通常放在箱线图最高点的上方。批量处理时可以根据每组最大值自动算max_values - aggregate(value ~ group, data, max)但不同组箱体高度差距很大时自动出来的字母位置可能互相遮挡还是建议肉眼检查一轮。4.4 一个避坑点星号显示的阈值要提前对齐map_signif_level TRUE默认的星号规则是p值范围显示p 0.001***p 0.01**p 0.05*p 0.05ns如果你的数据里有p值刚好在0.051附近图上会显示ns但你可能觉得趋势很接近显著想单独标注。这种情况下不要硬改显示规则老老实实把具体p值显示出来并注明检验方法。我个人建议除非审稿人明确要求否则不要在图上自己定义一套边缘显著的特殊符号。另外设置y_position时一定要给标签和星号留足空间否则就等着导出后文字被截断。一般做法是用ggplot_build(p)$data提取箱线图四分位数再按最大值的一定比例自动偏移max_val - max(data$value) y_start - max_val * 1.1如果数据里存在离群点outlier且你在箱线图里还画了离群点符号那y的起点应该从离群点最大值往上加而不是从箱体上边缘往上加。5. 常见问题与排查技巧实录这一节全是实操现场5.1 ns总是出现在图上但我明明算出p值很显著这个问题的根源几乎都是检验方法选择不一致。stat_compare_means()和geom_signif()内部默认的检验方法可能与你t.test(value ~ group, data)拿到的p值不一致。比如geom_signif()的test参数没传时默认走wilcox.test而你正文统计用的是t检验。两个p值自然不同图上呈现的显著性自然和你期望的不一致。解决办法很简单把test参数显式写清楚p geom_signif( comparisons list(c(Control, Treatment)), test t.test, map_signif_level TRUE )5.2 显著性标记线超出画布边界导出的图被截断这是最常见的排版问题。原因在于y_position没设置或者设置得太低。处理方式有两种修改y_position将标记线整体上移调整坐标范围的动态预留不要硬性ylim()截断用coord_cartesian(ylim c(0, 25))让图形区域扩展但数据不被删掉。p - p coord_cartesian(ylim c(0, max_val * 1.3))coord_cartesian()只会缩放视野不会裁剪数据比直接改ylim安全很多。之前我遇到过一次特别棘手的情况四组数据三对比较每组最大值几乎贴到上限。我算好y_position之后发现第三层标记线高度逼近25但图表区只有22导出的PNG图顶部直接被切掉。最后是用scale_y_continuous(expand expansion(mult c(0.05, 0.3)))在顶部预留了30%的扩展空间解决的。5.3 分组顺序和图例顺序不一致图例顺序由因子顺序控制一定要在数据转化时同步确认legend_order - c(Control, Low, Medium, High) data$group - factor(data$group, levels legend_order) scale_fill_manual(values custom_colors, breaks legend_order)breaks参数是为了保险万一颜色向量命名混乱至少图例顺序可控。5.4 数据点重叠严重看不出分布样本量一大geom_jitter()的抖动就会变得混乱。这时可以改用geom_dotplot()或者ggbeeswarm包的geom_beeswarm()后者用蜂群算法让点尽量不重叠。library(ggbeeswarm) p - p geom_beeswarm(size 2, alpha 0.6)还是乱就考虑分面facet展示一行一个组降低重合密度。5.5 配对数据没有连线配对设计同一病人治疗前后、同一培养皿处理前后必须把同一个样本的点用线段连起来。核心是用aes(group id)告诉ggplot哪些点属于同一个样本ggplot(data, aes(x timepoint, y value, group id)) geom_boxplot(aes(fill timepoint), width 0.4) geom_point(size 2) geom_line(aes(group id), color grey50, alpha 0.6)注意group id这个映射必须放在ggplot()里才能让点和线都知道按id分组。放在geom_line()也行但如果图里同时有箱线和点线放在顶层最稳妥。5.6 标注条重叠、遮挡箱体三组以上比较多重标记时标记线可能在顶部挤成一团。解法用step_increase参数让每条标记线逐级上抬或者用分面的方式每组单独占一个小图把显著性标记放到每个panel内。p geom_signif( comparisons list( c(Control, Low), c(Control, Medium), c(Low, Medium) ), step_increase 0.12 )5.7 导出图片的清晰度和尺寸控制投稿级的图片推荐先导PDF再转TIF或者直接用高分辨率PNG。我用ggsave()时固定三个参数ggsave(boxplot_labeled.pdf, p, width 7, height 5, units in, dpi 300)只改dpi不改变宽高比例是最稳的做法。有些时候图在RStudio预览里很完美导出后用PDF阅读器放大到150%才发现文字发虚这是因为没有指定device。如果用device pdf文字是矢量格式不会虚用device png300dpi以上基本没问题。5.8 常见问题速查表问题可能原因处理方式图上显示ns但你想显示星号检验方法不一致显式指定test参数标记线被裁剪y_position不够调y_position或coord_cartesian图例顺序混乱因子水平未设factor()指定levels散点重叠严重样本量大用geom_beeswarm或分面箱线图越界离群点干扰手动指定outlier处理策略字母标注位置重叠高度设置太紧凑按组最大值加偏移人工微调6. 完整可复现代码模板直接替换数据就能用最后整理一份完整代码从数据整理到导出一步到位。数据部分请替换成你自己的工作表。# 加载包 library(ggplot2) library(ggsignif) library(ggpubr) # 模拟数据三组处理 set.seed(2025) data - data.frame( id paste0(S, 1:60), group rep(c(Control, Low, High), each 20), value c( rnorm(20, 8, 1.5), rnorm(20, 10, 1.8), rnorm(20, 13, 2.0) ) ) # 设定分组顺序 data$group - factor(data$group, levels c(Control, Low, High)) # 自定义配色 my_colors - c(Control #4DBBD5, Low #00A087, High #E64B35) # 画图 p - ggplot(data, aes(x group, y value, fill group)) geom_boxplot(width 0.6, outlier.shape NA, alpha 0.6) geom_jitter(width 0.15, size 1.8, alpha 0.5, color grey30) geom_signif( comparisons list( c(Control, Low), c(Low, High), c(Control, High) ), test t.test, map_signif_level TRUE, step_increase 0.1 ) scale_fill_manual(values my_colors) labs(x Treatment, y Value) theme_classic(base_size 14) theme( axis.text element_text(color black), legend.position none ) coord_cartesian(ylim c(0, max(data$value) * 1.5)) print(p) # 导出 ggsave(stacked_boxplot_with_labels.pdf, p, width 6, height 4.5, units in, dpi 300) ggsave(stacked_boxplot_with_labels.png, p, width 6, height 4.5, units in, dpi 600)如果你的数据超过三组comparisons列表会变长step_increase建议调到0.12~0.15之间否则顶部高度不够用。如果你的分组内样本量差异很大箱线图宽度会造成视觉误导可以用geom_boxplot(width 0.5)配合aes(width ...)按样本量调整但那样操作比较繁琐一般8:1以内的样本量差异直接画问题不大。7. 图中的语意细节别忽略误差棒、图注和样本量标注7.1 显示n值科研图特别讲究样本量透明化。在图的副标题或图注里明确写每组n20会让审稿人少一个挑刺角度。也可以在x轴刻度标签上用Control (n20)这种方式直接写清楚new_labels - c( Control (n20), Low (n20), High (n20) ) p scale_x_discrete(labels new_labels)7.2 标注单位和统计信息y轴必须带单位比如Expression Level (FPKM)、Concentration (mg/L)。图的figure legend里说明统计方法、误差棒含义标准误还是标准差、显著性规则。这些都是审稿人默认期待的东西缺一项都会给返修增加工作量。7.3 三角形标记与渐变色的使用边界有的论文喜欢用上升箭头或色块渐变表示数值大小这类视觉元素适合热图不适合箱线图。箱线图本身的数据几何属性已经非常强烈再加过多的视觉符号会让图变成信息烟花。我自己的原则是一个图层负责一个信息维度箱线管分布、点管样本、标记线管统计其他装饰能省就省。8. 一个经验之谈画图前先想清楚图要放进论文的哪个位置这个建议听起来像废话但真的能省很多返工时间。如果图是放在结果部分的第一张图往往需要承载全局数据分布概览那就要把全部分组放上去如果是作为补充材料那只需要画关键比较组如果是给PPT汇报用那配色可以更鲜明、字体更大完全不用考虑期刊印刷限制。我通常的做法是先保存一个完整版再针对目标期刊的排版尺寸另存一个精简版。完整版带上所有分组和所有显著性标签精简版只保留正文提到的核心对比。这样调整起来比每轮返工重新画图快得多。另外不同期刊对图宽度的要求不同单栏图一般6~9厘米跨栏图一般14~19厘米。你不必提前精确到毫米但至少确认最终导出尺寸的宽高比不要让图被拉伸变形。ggsave里同时指定宽度和高度就能固定比例别只改一个参数。最后提醒一句统计检验的细节方法、样本量、校正方式一定记在代码注释或实验记录里。半年前画的图半年后投稿时被问你这是什么检验是常事。代码注释写得越清楚返修时越轻松。我自己现在画到显著性标注这一步都会多花两分钟检查三个地方一是标记线高度是否超出画布二是星号规则是否和正文一致三是分组顺序是否和表格一致。这三处检查完图基本就能顺利投稿。希望这篇能帮你少走一些弯路。

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

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

免费获取报价 →
↑