04 火山图、热图与富集可视化
04 火山图、热图与富集可视化
前三章把数据、差异和富集跑完,这一章只做一件事:把结果变成能直接用于论文和汇报的图。每张图都配一个最常见的用途,源码一起放出来。
配套脚本 bulk04_visualization_sci.R 把 DESeq2 + 富集的流程重跑一次(继承自 02 和 03),然后输出 6 张可视化成品:
Rscript scripts/bulk04_visualization_sci.R
一篇论文的 figure 怎么排
具体画图之前,先想清楚每张图在论文里要承担的角色。一篇典型的 RNA-seq 文章 main figure 大约 4-6 张图,结构通常是:
| Figure | 内容 | 本章对应 |
|---|---|---|
| Fig 1 | 样本概览 + QC — PCA、距离热图、库大小 | 02 章的图 1-3 |
| Fig 2 | 差异分析整体 — 火山图 + top DE 热图 | 本章 图 1-2 |
| Fig 3 | 关键基因深入 — 已知通路基因表达分布 | 本章 图 3 |
| Fig 4 | 功能富集 — KEGG / GO bubble + cnetplot | 本章 图 4-5 |
| Fig 5 | 跨数据集验证 | 看项目 |
一张图回答一个问题,不要堆 8 个 panel 的 supplementary 怪物。每张图问自己:"读者看完这张图能讲清楚什么?" 讲不清楚就拆开。
图 1:火山图

火山图是每篇转录组文章的标配 —— 横轴是 log2 fold change,纵轴是 -log10(padj)。每个点是一个基因,颜色表示它在哪个象限。两条虚线标出 padj = 0.05 和 |LFC| = 1 的阈值。
这张图的关键是标注:默认标全部显著基因会完全盖掉点云。脚本里只标 top 10 up + top 10 down(按 padj 排序),用 ggrepel 自动避让。
核心代码:
ggplot(vol, aes(x = log2FoldChange, y = -log10(padj), color = sig)) +
geom_point(size = 0.6, alpha = 0.5) +
geom_text_repel(data = label_df, aes(label = SYMBOL), ...) +
geom_vline(xintercept = c(-1, 1), linetype = "dashed") +
geom_hline(yintercept = -log10(0.05), linetype = "dashed")
改 sig 列的阈值、head(10) 的数量、颜色都很方便。
图 2:Top DE 基因热图

取 padj 最小的 40 个基因,在 VST 归一化后做 row z-score,画热图。列注释标出了处理状态和细胞系。
这张图在报告里的作用是给火山图"补一刀" —— 火山图只能看统计显著性,热图能直接看到"这些基因在每个样本里到底是什么表达水平"。列聚类会把处理样本和对照样本各自聚到一起;行聚类会把上调和下调基因分开。
用 pheatmap:
mat_z <- t(scale(t(mat))) # row z-score
pheatmap(mat_z,
annotation_col = anno_col,
color = colorRampPalette(c("blue", "white", "red"))(100),
breaks = seq(-3, 3, length.out = 101))
breaks 参数很重要:限制颜色范围能避免个别极端基因把色阶拉歪。[-3, 3] 是 z-score 的常用范围。
图 3:精选基因的表达分布

选几个已知的糖皮质激素响应基因(DUSP1 / FKBP5 / ANGPTL4 / KLF15 / SPARCL1 / ZBTB16)画 boxplot + jitter,y 轴是 log10 的 normalized counts。
这种"已知通路验证"的图在讨论部分特别有用:文献说这些基因是地塞米松响应基因,我们的数据里也确实看到了这个响应,能直接把自己的数据和已有知识对接上。
ggplot(count_long, aes(x = dex, y = count, fill = dex)) +
geom_boxplot() + geom_jitter() +
facet_wrap(~ symbol, scales = "free_y") + scale_y_log10()
图 4:KEGG 富集气泡图

比 dotplot(ekegg) 的默认版本更有信息量:点大小是 gene count,颜色是 -log10(p.adj),横轴是 gene ratio。一张图同时显示"通路覆盖度"、"显著性"、"通路里命中了多少基因"三个维度。
这个 bubble 是纯 ggplot 写的,不依赖 enrichplot,完全可以移植到任何富集结果(enrichGO、enrichPathway、GSEA 都行)。
ggplot(ekegg_df,
aes(x = GeneRatio_num, y = reorder(Description, GeneRatio_num),
size = Count, color = -log10(p.adjust))) +
geom_point() +
scale_color_gradient(low = "blue", high = "red") +
scale_size_continuous(range = c(3, 9))
图 5:cnetplot 基因-通路网络

enrichplot::cnetplot 把 GO term 和驱动这些 term 的基因画成二部图:浅灰色大圆点是 term,彩色小点是基因,颜色按 log2FC 着色。
这张图的价值是一眼看到"哪些基因被多条通路共享" —— 共享基因往往是生物学上最关键的 master regulator。讨论部分里可以直接点出"图里 A 基因被 3 条通路共享,说明它很可能是这次处理的上游枢纽"。
图 6:GSEA NES forest 图

GSEA 的瀑布图能表达排序,但多个 term 一起看、带显著性的信息,forest 图更合适。横轴是 NES,纵轴是 term 名字,点大小是 -log10(p.adj),颜色区分激活 / 抑制。
相比 waterfall,这张图额外能读出 NES 到 0 的距离(segment 线段)和 显著性强度(点大小),信息密度更高。
常见坑
- 颜色:火山图用蓝 / 红 / 灰三色是惯例,别换成 viridis 这种连续色;读者会困惑"到底哪边是上调"
- 标注:标签超过 30 个就没法读;能标 top 10-20 就够了
- 基因 ID:热图 / 火山图要标 SYMBOL 不是 Ensembl。一定要在最后可视化之前把 SYMBOL 映射上
- 坐标轴范围:火山图如果有极端 padj(比如 1e-300),用
pmax(padj, 1e-300)封顶避免压扁整张图 - 导出:论文图建议
dpi = 300直接存 PNG;不要存 JPG(有压缩伪影);有时需要 TIFF / PDF 向量图,见ggsave()的device参数
补充几条容易踩的坑:
坑:热图行做 z-score 但没限制 breaks
scale(t(mat)) 出来的 z-score 范围可能 [-10, 30],pheatmap 默认会把整个范围映射到色阶 — 结果 99% 的基因颜色都接近白色,几个极端基因把图"拉歪"。breaks = seq(-3, 3, length.out = 101) 是常用上限,超出范围的值压到端点。
坑:火山图 padj = 0 的基因显示为 Inf
部分 DE 工具会输出 padj = 0(数值下溢),-log10(0) = Inf 让 ggplot 报错或丢点。pmax(padj, 1e-300) 封顶再画。
坑:cnetplot 的基因数太多挤成一团
cnetplot(showCategory = 5) 默认显示前 5 条 term,如果 term 之间共享基因多,几十个基因挤成一坨可读性归零。显式 showCategory = 3 + node_label = "category" 只标 term 不标基因,更清晰。
坑:ggsave PDF 字体丢失
ggsave(file = "fig.pdf") 在某些字体下会输出"被吃掉部分字体"的 PDF,投稿时被拒。用 device = cairo_pdf 强制嵌入字体,或者直接存 png 300dpi。
坑:facet_wrap 的 free_y 让 boxplot 跨基因不可比
facet_wrap(~ symbol, scales = "free_y") 让每个基因的 y 轴独立 — 视觉上"差异都看上去差不多大"。展示绝对幅度差异时用 scales = "fixed",让读者能看出"基因 A 的 fold 比基因 B 大多少"。
下载资源
下一步
bulk RNA-seq 专栏目前到这一章为止。后面会补:
- 多时间点或剂量响应的差异分析(
LRTvsWald) - 批次效应更复杂的场景(RUVseq、SVA)
- 面向发表的结果组织(fgsea + MSigDB Hallmarks + Reactome)
- 单细胞 vs bulk 的方法学对照
接着深入:
- 05 多时间点与 LRT 检验 — 设计里有时间序列 / 剂量梯度时,本章的火山图 + 热图都要换思路
- 08 可复现分析报告与结果交付 — 这些图怎么组织进 R Markdown 报告,怎么交付给合作者
横向延伸:
- FigCode 火山图 — 在线把你的 DE 表替换 demo 数据出图
- FigCode 热图 — 复杂热图(多列 annotation、自定义聚类)的样板代码