跳到正文
百沐一下百沐一下
本节目录

箱线图 + 散点抖动:展示分布

跟完这篇,您能画出一张既概括分布、又不掩盖真实数据的分组比较图:读懂箱线图的每一条线在说什么、为什么要在箱子上叠一层散点抖动、显著性标注怎么加才不误导,以及什么时候该换成小提琴图。

比较几组数值(如某基因在肿瘤组与正常组的表达、几种处理下的某项指标),最常见的一张图就是箱线图(boxplot)。它用五个数把一组数据的分布压成一个箱子,几组并排,高低一目了然。但箱线图有个天生的毛病:它把每一个样本都藏了起来。所以现在的通行做法是——箱线图打底,散点抖动叠上去,既要概括、也要诚实。

箱线图的每一部分在说什么

箱线图画的是「五数概括」(five-number summary)。看懂下面这几个部件,才不会读错图。

部件对应的量含义
箱子中间的横线中位数(median,Q2)一半数据在它上、一半在它下
箱子的下边下四分位数(Q1,25% 分位)有 25% 的数据比它小
箱子的上边上四分位数(Q3,75% 分位)有 75% 的数据比它小
箱子的高度四分位距(IQR = Q3 − Q1)中间 50% 数据的跨度,衡量离散程度
上下的「须」延伸到 1.5 × IQR 内最远的点覆盖大部分「正常范围」的数据
须之外的点离群点(outlier)距箱子超过 1.5 × IQR 的观测

两点容易被忽略,先说清楚:

  1. 箱线图里的横线是中位数,不是平均数。 中位数对离群值不敏感,所以箱线图天然抗极端值。如果您想强调平均数,得另外用点单独标出来,别把中位线错读成均值。
  2. 「须」的长度不是最大最小值。 ggplot2、以及绝大多数统计软件默认用的是 Tukey 定义:须只延伸到「距箱子不超过 1.5 × IQR」范围内最远的那个真实数据点,再远的单独画成离群点。所以须的端点几乎总是落在某个实际观测上,而不是理论边界。

「1.5 × IQR」是判定离群点的常用惯例,不是铁律。它来自 Tukey 的经典建议,被大多数软件设成默认,但离群与否终究要结合专业判断——一个「离群点」可能是录入错误,也可能是最有意思的真实个体,别一律删掉。

先画一张标准箱线图

假设您有一张长表 df:一列是分组 group(因子),一列是数值 value。这是绘图最顺手的格式,如果您手上是宽表,先转成长表(tidyr::pivot_longer)。

library(ggplot2)

ggplot(df, aes(x = group, y = value)) +
  geom_boxplot(outlier.shape = NA) +   # 先把离群点关掉,下一步用散点统一显示
  labs(x = NULL, y = "表达量") +
  theme_bw()

这里有个关键的小动作:outlier.shape = NA。因为下一步要叠散点,所有点都会由散点层画出来;若不关掉,箱线图自带的离群点会和散点重叠,同一个观测被画两遍。这是叠图时最常见的一个小坑。

为什么要叠上散点抖动

箱线图把一组数据压成五个数,代价是把每个样本藏了起来。而藏起来的东西,往往正是要命的:

  • 看不出样本量。 一个箱子背后是 5 个样本还是 500 个,画出来一模一样。但 n=5 的比较和 n=500 的比较,可信度天差地别。
  • 看不出真实形状。 一组「双峰」数据(其实是两个亚群)和一组集中在中间的数据,可能画出几乎相同的箱子。箱线图假设数据是单峰的,一旦不是,它就会骗人。
  • 看不出数据是不是「空的」。 极端情况下,箱子中间大片区域一个点都没有,全靠上下两小撮撑着——箱线图完全看不出来。

解决办法很简单:把每一个原始数据点画上去。这样读者能一眼看到 n 有多大、点是怎么散布的、有没有可疑的空洞或聚堆。这不是装饰,而是一种诚实——让图承认「我背后到底有多少证据」。

在样本量不大(比如每组几个到几十个)的科研数据里,「箱线图 + 散点」几乎应当是默认做法。审稿人越来越倾向于要求看到原始点,尤其是当每组样本很少时。

加上散点抖动

直接把点画上去会有个问题:同一组的点 x 坐标相同,会叠成一条竖线,重合的点根本数不清。抖动(jitter) 就是给每个点的水平位置加一点随机偏移,把它们摊开。

library(ggplot2)

ggplot(df, aes(x = group, y = value)) +
  geom_boxplot(outlier.shape = NA, width = 0.5) +
  geom_jitter(width = 0.15, height = 0,      # 只横向抖动,纵向绝不能抖
              alpha = 0.6, size = 1.8) +
  labs(x = NULL, y = "表达量") +
  theme_bw()

几个参数值得逐一说明:

参数作用建议
width横向抖动的幅度0.1~0.2 常够用,别宽到两组的点混在一起
height = 0纵向抖动幅度必须设为 0
alpha点的透明度点多时调低(如 0.4~0.6),看得出密度
size点的大小点多就调小,避免糊成一团

这里最重要的一条规则:纵向绝对不能抖动(height = 0。纵轴是您真正的数值,一旦纵向抖动,点的高低就不再是它的真实取值,图直接读错。横向抖动只是为了摊开重叠,不影响任何数值——这条线千万别踩。

如果点特别多、抖动后还是糊成一片,可以换 ggbeeswarm 包的「蜂群图」(beeswarm),它按密度把点整齐地排开、不重叠,比纯随机抖动更好看也更好读。

library(ggbeeswarm)

ggplot(df, aes(x = group, y = value)) +
  geom_boxplot(outlier.shape = NA, width = 0.5) +
  geom_quasirandom(width = 0.2, alpha = 0.6) +   # 按密度铺开,不随机重叠
  theme_bw()

怎么加显著性标注

组间「差没差」不能靠眼睛看箱子高低,得配一个统计检验,再把结果标在图上。ggpubr 包的 stat_compare_means() 能把检验和标注一步做完。

两组比较,指定要比的那一对:

library(ggpubr)

ggplot(df, aes(x = group, y = value)) +
  geom_boxplot(outlier.shape = NA) +
  geom_jitter(width = 0.15, height = 0, alpha = 0.6) +
  stat_compare_means(
    comparisons = list(c("Tumor", "Normal")),
    method = "wilcox.test"           # 明确写清用什么检验
  ) +
  theme_bw()

多组比较,先给一个整体检验,再列出要两两比的组合:

my_comparisons <- list(c("A", "B"), c("A", "C"), c("B", "C"))

ggplot(df, aes(x = group, y = value)) +
  geom_boxplot(outlier.shape = NA) +
  geom_jitter(width = 0.15, height = 0, alpha = 0.5) +
  stat_compare_means(method = "kruskal.test") +                  # 整体检验(放在图上方)
  stat_compare_means(comparisons = my_comparisons,
                     method = "wilcox.test") +                    # 两两比较
  theme_bw()

标注要标得诚实,有三条红线:

  1. 检验方法要和数据匹配,别用默认糊弄。 两组正态、方差齐用 t.test;不满足或样本少、偏态,用非参数的 wilcox.test(Mann-Whitney U)。多组对应 anovakruskal.teststat_compare_means 有默认方法,但请显式写出 method =,别让读者猜。到底怎么选,见 怎么选统计检验
  2. 多次两两比较要校正 p 值。 三组就有三对、四组有六对,比得越多,光靠运气「碰」出显著的概率越高。做多重比较时应对 p 值做校正(如 Benjamini-Hochberg、Bonferroni),别把一堆没校正的 * 直接贴上去。原理见 多重检验校正
  3. 星号不是幅度。 *** 只说明「差异更不像是偶然」,不代表差得更多。一个统计上极显著、但实际只差一点点的结果,在生物学上可能毫无意义。星号旁边,最好让读者也能从散点看出实际差多大。

图上的 * 到底代表多小的 p 值,不同期刊约定不完全一样(常见的一种是 * 对应 p<0.05** 对应 p<0.01*** 对应 p<0.001)。务必在图注里写清您的对应关系和所用检验,别让读者靠猜。

什么时候改用小提琴图

散点叠箱线图很好,但当每组样本非常多(成百上千)时,点会多到抖动也摊不开,糊成一坨。这时候小提琴图(violin plot)更合适。

小提琴图画的是数据的密度分布:哪个数值区间点多,小提琴就在那里鼓得胖;哪里点少就瘦。它比箱线图多告诉您一件关键的事——分布的形状。前面说箱线图会把「双峰」藏起来,而小提琴图会直接鼓出两个包,一眼看穿。

常见做法是把三者叠起来,各取所长:

library(ggplot2)

ggplot(df, aes(x = group, y = value)) +
  geom_violin(trim = FALSE, fill = "grey90") +          # 外层:分布形状
  geom_boxplot(width = 0.15, outlier.shape = NA) +      # 内层:五数概括
  geom_jitter(width = 0.08, height = 0, alpha = 0.3) +  # 点:真实数据(量大时可省)
  theme_bw()

怎么选,一张表说清:

情况更合适的选择原因
每组样本少(几个~几十个)箱线图 + 散点抖动点画得下,直接给读者看原始数据最诚实
每组样本很多(几百以上)小提琴图(可内嵌小箱线)点已摊不开,密度曲线更清楚
怀疑分布是双峰 / 强偏态小提琴图能看出形状,箱线图会掩盖
只想快速比几组高低箱线图最简洁、最好读

一个反过来的提醒:样本太少时别用小提琴图。 小提琴的形状是由核密度估计「平滑」出来的,只有几个点时,它会把噪声脑补成一个圆润好看、其实并不存在的形状,反而骗人。少数据老老实实用箱线加散点。

geom_violin 默认 trim = TRUE,会把小提琴在数据的最小/最大值处截断;设 trim = FALSE 则让密度曲线自然延伸出去。两种都常见,按您想强调的是「实际范围」还是「平滑形状」来选即可。

常见错误

错误后果怎么避免
散点纵向也抖动点的高低不再是真实数值,图读错geom_jitterheight = 0
忘了关箱线图自带离群点离群点被画两遍、重叠geom_boxplot(outlier.shape = NA)
把中位线当平均数读结论说偏记牢箱线图中间是中位数;要均值另标
多组两两比较不校正 p 值假阳性,* 贴多了做多重检验校正,见相应教程
图注不写检验方法和 * 含义读者无法判断可信度图注写清所用检验与星号对应的 p 值
样本极少还画小提琴平滑出并不存在的形状少数据用箱线 + 散点

在平台上跑一遍

不想写 R 代码也能得到这张图:在 百沐一下 传入一张含分组列和数值列的表,用一句话说清「按组画箱线图、叠散点抖动、Tumor 与 Normal 两组做 Wilcoxon 检验并标显著性」,就会给您一张出版级图,连同可复现的 R 代码一起交付,检验方法、配色、要不要换小提琴都能对话式再调。出图流程见 智能绘图使用指南,选检验可参考 智能统计

相关