火山图:把差异分析结果画成一张经得起审稿的图
跟完这篇,您能把一张差异分析结果表,一步步画成读得懂、经得起审稿的火山图:两个轴各放什么、阈值怎么定、上下调怎么配色、关键基因怎么标,最后导出成投稿能用的图片文件——中间常见的几个坑也一并避开。
火山图(volcano plot)是差异表达分析最常见的一张图。它把成千上万个基因压进一张散点图里,一眼就能看出「哪些基因变了、变了多少、有多可信」。工具不复杂,但要画对,得先想清楚两个轴。
火山图的两个轴
火山图是一张散点图,每个点是一个基因。两个轴分别回答两个问题:
| 轴 | 变量 | 回答的问题 | 越往哪边 |
|---|---|---|---|
| 横轴 X | log2FoldChange(log2 倍数变化) | 变了多少、往哪个方向变 | 越往右上调越强,越往左下调越强 |
| 纵轴 Y | -log10(padj)(校正 p 值取负对数) | 这个变化有多可信 | 越往上越显著 |
两点需要说清楚:
- 横轴用 log2 而不是原始倍数,是为了让「上调 4 倍」和「下调 4 倍」在图上对称(分别落在 +2 和 −2),零点表示没有变化。
- 纵轴取
-log10,是因为 p 值越小越显著,直接画 p 值会把最显著的点压到最底下。取负对数之后,padj = 0.01变成 2、padj = 0.001变成 3,越显著的点越高,符合直觉。
纵轴用的是校正后的 p 值(
padj,也叫FDR、adjusted p-value),不是原始 p 值。成千上万个基因同时检验会带来大量假阳性,必须做多重检验校正(DESeq2 默认用 Benjamini-Hochberg 法)。画火山图请优先用padj。
准备一张差异结果表
火山图的输入就是您跑完差异分析得到的那张结果表,至少要有两列:倍数变化和校正 p 值。不同工具的列名不一样,先对照一下:
| 工具 | 倍数变化列 | 校正 p 值列 |
|---|---|---|
DESeq2(results()) | log2FoldChange | padj |
edgeR(topTags()) | logFC | FDR |
limma(topTable()) | logFC | adj.P.Val |
本文按 DESeq2 的列名往下写。若您用的是 edgeR 或 limma,把代码里的列名换成对应的即可。先把结果表读进来,并把基因名单独放一列(后面标注要用):
# 若结果已存成 CSV(DESeq2 / edgeR / limma 导出的都行)
res <- read.csv("deg_results.csv", row.names = 1)
res$gene <- rownames(res) # 把基因名放到单独一列
# 若刚跑完 DESeq2,也可以直接转成数据框:
# res <- as.data.frame(results(dds))
# res$gene <- rownames(res)
数据从哪来、结果表各列什么意思,见 用 DESeq2 做差异表达分析。
给每个基因贴上下调标签
判断一个基因「值得关注」,通常同时看两件事:显著(padj 足够小)且变化幅度足够大。所以先定两个阈值,再按阈值把每个基因分成三类。
library(dplyr)
p_cut <- 0.05 # 显著性阈值:常用惯例,按数据调,不是铁律
lfc_cut <- 1 # log2FC 阈值:常用惯例,按研究问题松紧
res <- res %>%
mutate(change = case_when(
padj < p_cut & log2FoldChange > lfc_cut ~ "Up", # 显著上调
padj < p_cut & log2FoldChange < -lfc_cut ~ "Down", # 显著下调
TRUE ~ "NS" # 其余算不显著
))
table(res$change) # 看看三类各有多少个
这两个阈值也决定了待会儿三条虚线画在哪:
- 一条水平线在显著性阈值处:
y = -log10(0.05) ≈ 1.30; - 两条竖直线在倍数变化阈值处:
x = -1和x = 1(即上调或下调 2 倍以上)。
阈值是惯例,不是定律。
padj < 0.05与|log2FC| > 1是文献里最常见的默认,但都不是硬标准:样本量大、信噪比高时可以更严(如padj < 0.01);探索阶段、差异本就微弱时可以放宽 log2FC。关键是在图注或正文里写清您用的阈值,让读者知道线画在哪。padj为NA的基因(DESeq2 独立过滤后置空的)在这里会自然落进NS,正是我们想要的。
画出带配色和阈值线的火山图
配色的作用是把三类点一眼分开:显著上调、显著下调、其余不显著。通行惯例是红上、蓝下、灰不显著。
| 类别 | 含义 | 常用颜色 |
|---|---|---|
Up | 显著上调 | 红色 |
Down | 显著下调 | 蓝色 |
NS | 未达阈值 | 灰色 |
library(ggplot2)
cols <- c(Up = "#E64B35", Down = "#3182BD", NS = "grey80")
p <- ggplot(res, aes(x = log2FoldChange, y = -log10(padj), color = change)) +
geom_point(alpha = 0.6, size = 1.2) +
scale_color_manual(values = cols) +
geom_hline(yintercept = -log10(p_cut), linetype = "dashed", color = "grey40") +
geom_vline(xintercept = c(-lfc_cut, lfc_cut), linetype = "dashed", color = "grey40") +
labs(x = "log2 fold change", y = "-log10(padj)", color = NULL) +
theme_bw()
p
几个让图更干净的小处理:
- 让灰点退到后面做背景,红蓝的信号点才跳得出来:给
NS更低的透明度或更小的点即可。 - 点很多时降低透明度(
alpha = 0.4左右),避免中间黑成一团、看不出密度。 - 红蓝别用最刺眼的纯红纯蓝,稍微降一点饱和度更耐看,也更接近期刊常见配色。
- 画图时若看到
Removed N rows containing missing values的提示,多半是padj = NA的基因被丢掉了——它们本就归入不显著,不影响主图。
标注要讲的关键基因
火山图上不需要、也不应该标出每一个基因的名字——只标少数几个真正要讲的。标注用 ggrepel 包,它会自动把文字错开、避免重叠。挑选方式常见两种,按需选一种:
- 按生物学意义手动选:您研究里关心的那几个基因,直接列名字最稳妥。
- 按统计量自动选 Top N:例如取「显著且 log2FC 最大」的前若干个。
library(ggrepel)
# 方式一:手动指定要标的基因
genes_to_label <- c("TP53", "MYC", "CDKN1A")
lab <- dplyr::filter(res, gene %in% genes_to_label)
# 方式二:自动取显著变化里最靠边的 10 个
# lab <- res %>% filter(change != "NS") %>% slice_max(abs(log2FoldChange), n = 10)
p_lab <- p +
geom_text_repel(
data = lab,
aes(label = gene),
size = 3, max.overlaps = 20,
box.padding = 0.4, min.segment.length = 0,
show.legend = FALSE # 避免图例里多出一个「a」
)
p_lab
标注的分寸:宁少勿多。一张图标 5~15 个名字通常就够讲清故事;标几十上百个,文字会糊成一片,反而什么都看不清。
导出成出版级图片
最后把图存成文件。投稿优先存矢量图(PDF / SVG,放大不糊);预览或贴进文档用位图(PNG,记得给足分辨率)。
ggsave("volcano.pdf", p_lab, width = 6, height = 5) # 矢量图,投稿首选
ggsave("volcano.png", p_lab, width = 6, height = 5, dpi = 300) # 位图,预览或贴进文档
width / height 单位默认是英寸,按目标版面调;位图的 dpi 一般给到 300 以上才够清晰。
想更省事,可以用 Bioconductor 的
EnhancedVolcano包:一行EnhancedVolcano(res, lab = res$gene, x = "log2FoldChange", y = "padj")就能出一张自带阈值线和标注的火山图。代价是细节不如手写 ggplot 灵活,微调配色、字号时仍要翻它的参数。
常见坑与自查
出图前对照这张表快速自查一遍:
| 错误 | 后果 | 怎么避免 |
|---|---|---|
| 两个轴放反(把 log2FC 放纵轴) | 图彻底读不对,方向和显著性混在一起 | 记牢:横轴 log2FC、纵轴 −log10(padj) |
| 忘了取负对数(纵轴直接放 padj) | 最显著的点被压到最底部,图上下颠倒 | 纵轴永远是 -log10(padj) |
| 用原始 p 值而非 padj | 假阳性没被控制,显著数被高估 | 优先用校正后的 padj / FDR |
| 不画、不标阈值线 | 读者不知道「显著」的界在哪 | 画出虚线,并在图注写清阈值 |
| 一次标几十个基因 | 文字重叠成团,喧宾夺主 | 只标要讲的几个,5~15 个为宜 |
没处理 padj = NA 或 = 0 | 点消失或飞到无穷高 | NA 归入不显著或先剔除;padj = 0 时可设一个下限再取对数 |
关于
padj = 0:个别极显著的基因padj会被记为 0,-log10(0)是无穷大,点会飞出画面。稳妥做法是给它设个下限,例如res$padj[res$padj == 0] <- min(res$padj[res$padj > 0]) / 10,再取对数。关于NA:DESeq2 会对表达量过低的基因做独立过滤,把它们的padj置为NA,画图时应剔除或归入不显著,不要当成缺失去插补。
在平台上跑一遍
不想写 R 代码也能得到同样的图:在 百沐一下 传入一张差异分析结果表(含 log2FoldChange 与 padj 两列),用一句话说清「画火山图、阈值 padj<0.05 且 |log2FC|>1、标出这几个基因」,就会给您一张出版级火山图,连同可复现的 R 代码一起交付,配色和阈值都能对话式再调。操作细节见 智能绘图使用指南。
相关
- 用 DESeq2 做差异表达分析 —— 火山图的数据从哪来
- 做完差异分析,下一步富集分析 —— 把显著基因清单接着往下分析
- 统计方法与阈值怎么选 —— padj 和 log2FC 的界该定在哪
- 智能绘图使用指南 —— 在平台上不写代码画同一张图