热图可视化:pheatmap与ComplexHeatmap
热图是表达数据可视化最常用的图表类型。pheatmap 提供三行代码出图的基础方案,ComplexHeatmap 则支持高级布局、行列注释和多图拼接,适合发表场景。本文覆盖两个 R 包的数据标准化、配色调色板、注释配置和导出选项。
实测环境:R 4.4.1,pheatmap 1.0.12,ComplexHeatmap 2.18.0,Debian 13。
1. 数据准备——生信热图的数据长什么样
热图的输入本质就是一张数值矩阵。生信中最常见的场景:
# 模拟一个 RNA-seq 表达矩阵:100个基因 × 10个样本set.seed(42)expr_matrix <- matrix( rnorm(1000, mean = 5, sd = 2), nrow = 100, ncol = 10, dimnames = list( paste0("Gene", 1:100), paste0("Sample", 1:10) ))
# 人为制造一些差异(让热图有层次感)expr_matrix[1:30, 1:5] <- expr_matrix[1:30, 1:5] + 3 # 前30个基因在前5个样本高表达expr_matrix[71:100, 6:10] <- expr_matrix[71:100, 6:10] + 3 # 后30个基因在后5个样本高表达
head(expr_matrix[, 1:5])真实场景中这个矩阵来自 DESeq2 的 normalized counts、limma 的差异基因列表、或者 WGCNA 的模块表达值。几行代码就能从 DESeq2 提取:
# 从 DESeq2 对象提取标准化表达矩阵# dds <- DESeqDataSetFromMatrix(...)# dds <- DESeq(dds)# vsd <- vst(dds, blind = FALSE)# expr_matrix <- assay(vsd)2. pheatmap——三行代码出热图
library(pheatmap)
# 最简单的热图pheatmap(expr_matrix)就这么简单。但默认参数出来的图有这些问题:基因名太多挤成一团、颜色默认红蓝不太友好、没做行标准化看不出相对差异。
2.1 加上标准化和聚类
pheatmap(expr_matrix, scale = "row", # 按行做 Z-score 标准化 cluster_rows = TRUE, # 行聚类 cluster_cols = TRUE, # 列聚类 show_rownames = FALSE, # 基因太多,不显示名字 show_colnames = TRUE, main = "RNA-seq Expression Heatmap")scale = "row" 是生信热图最关键的一个参数。表达式原始值的量级差异很大(基因A表达量 0.5,基因B表达量 5000),不标准化的话热图只能看到少数高表达基因,其他全是暗色。
Z-score 的公式:
其中 是基因 在所有样本中的均值, 是标准差。标准化后每个基因的均值 = 0,标准差 = 1,不同基因之间就”可比”了。
2.2 配色——告别红蓝默认色
红蓝配色在色盲群体中难以区分,而且生信文献里几乎没人用。主流选择:
# 白-橙-红(表达量热图)pheatmap(expr_matrix, scale = "row", color = colorRampPalette(c("white", "orange", "red"))(100), show_rownames = FALSE, main = "White-Orange-Red")
# 蓝-白-红(差异热图,白=不变,蓝=下调,红=上调)pheatmap(expr_matrix, scale = "row", color = colorRampPalette(c("navy", "white", "firebrick3"))(100), show_rownames = FALSE, main = "Blue-White-Red (common in papers)")
# viridis 配色(色盲友好,越来越流行)library(viridis)pheatmap(expr_matrix, scale = "row", color = viridis(100), show_rownames = FALSE, main = "Viridis (colorblind-friendly)")colorRampPalette 生成 100 个渐变色阶,越多越平滑。我一般杂志投哪个就用哪个配色,投 Nature 系列可以用 viridis,投 Cell 系列蓝白红更常见。
2.3 添加注释——这张图里最加分的部分
纯热图只告诉你”这些基因在这些样本里高表达”,加上注释后可以同时展示样本分组、临床信息、突变状态等。
# 样本注释(列注释)annotation_col <- data.frame( Group = factor(rep(c("Control", "Treatment"), each = 5)), Batch = factor(rep(c("Batch1", "Batch2"), times = 5)), row.names = paste0("Sample", 1:10))
# 基因注释(行注释,比如标记哪些是差异基因)annotation_row <- data.frame( GeneClass = factor(c( rep("Oncogene", 30), rep("Tumor Suppressor", 30), rep("Other", 40) )), row.names = paste0("Gene", 1:100))
# 注释配色ann_colors <- list( Group = c(Control = "#4DBBD5", Treatment = "#E64B35"), Batch = c(Batch1 = "#00A087", Batch2 = "#7E6148"), GeneClass = c(Oncogene = "#DC0000", `Tumor Suppressor` = "#3C5488", Other = "#8491B4"))
pheatmap(expr_matrix, scale = "row", color = colorRampPalette(c("navy", "white", "firebrick3"))(100), annotation_col = annotation_col, annotation_row = annotation_row, annotation_colors = ann_colors, show_rownames = FALSE, main = "Expression Heatmap with Annotations", cutree_rows = 3, # 把行聚类切成3个模块 cutree_cols = 2 # 把列聚类切成2组)cutree_rows 和 cutree_cols 会自动在热图上画出聚类分割线——审稿人看到这条线就知道你做了聚类。
2.4 保存图片
# png 格式png("heatmap.png", width = 8, height = 10, units = "in", res = 300)pheatmap(expr_matrix, scale = "row", show_rownames = FALSE)dev.off()
# pdf 格式(矢量,适合投稿)pdf("heatmap.pdf", width = 8, height = 10)pheatmap(expr_matrix, scale = "row", show_rownames = FALSE)dev.off()
# tiff 格式(部分期刊要求)tiff("heatmap.tiff", width = 8, height = 10, units = "in", res = 300)pheatmap(expr_matrix, scale = "row", show_rownames = FALSE)dev.off()注意 pheatmap 的输出是 grid 对象,不能直接用 ggsave。用基础的 png()/pdf()/tiff() + dev.off() 即可。
3. ComplexHeatmap——进阶玩家的选择
pheatmap 能应付 80% 的场景,剩下的 20%——比如拼多列热图、自定义图例位置、oncoprint 等——需要 ComplexHeatmap。我一般简单探索用 pheatmap,要发表的图用 ComplexHeatmap。
3.1 基础用法
library(ComplexHeatmap)library(circlize) # ComplexHeatmap 依赖 circlize 做配色
Heatmap(expr_matrix, name = "Expression", # 图例标题 col = colorRamp2(c(-2, 0, 2), c("navy", "white", "firebrick3")), row_names_gp = gpar(fontsize = 6), column_names_gp = gpar(fontsize = 8), show_row_names = FALSE, cluster_rows = TRUE, cluster_columns = TRUE)colorRamp2 比 colorRampPalette 更精确——你指定断点和对应的颜色,它自动插值。对于 Z-score 标准化数据,c(-2, 0, 2) 覆盖了约 95% 的数据范围。
3.2 添加注释——ComplexHeatmap 的方式
# 列注释column_ha <- HeatmapAnnotation( Group = annotation_col$Group, Batch = annotation_col$Batch, col = list( Group = c("Control" = "#4DBBD5", "Treatment" = "#E64B35"), Batch = c("Batch1" = "#00A087", "Batch2" = "#7E6148") ), annotation_legend_param = list( Group = list(title = "Group"), Batch = list(title = "Batch") ))
# 行注释row_ha <- rowAnnotation( GeneClass = annotation_row$GeneClass, col = list(GeneClass = c( "Oncogene" = "#DC0000", "Tumor Suppressor" = "#3C5488", "Other" = "#8491B4" )))
Heatmap(expr_matrix, name = "Z-score", col = colorRamp2(c(-2, 0, 2), c("navy", "white", "firebrick3")), top_annotation = column_ha, right_annotation = row_ha, show_row_names = FALSE, column_title = "RNA-seq Expression Heatmap", column_title_gp = gpar(fontsize = 14, fontface = "bold"), row_km = 3, # k-means 分成3个基因模块 column_km = 2 # 样本分成2组)ComplexHeatmap 把注释对象化——HeatmapAnnotation 管列注释,rowAnnotation 管行注释,逻辑清晰但代码量确实比 pheatmap 多。
3.3 拼图——ComplexHeatmap 的杀手锏
多组数据并排展示是 ComplexHeatmap 最强的地方:
# 假设你有三个独立的表达矩阵(不同组织或不同处理)set.seed(123)mat1 <- matrix(rnorm(500, 5, 2), 50, 10)mat2 <- matrix(rnorm(500, 5, 2), 50, 10)mat3 <- matrix(rnorm(500, 5, 2), 50, 10)
rownames(mat1) <- rownames(mat2) <- rownames(mat3) <- paste0("Gene", 1:50)
# 分别画三张热图然后用 + 拼起来ht1 <- Heatmap(mat1, name = "Tissue1", col = colorRamp2(c(-2,0,2), c("navy","white","red")), show_row_names = FALSE, column_title = "Normal")ht2 <- Heatmap(mat2, name = "Tissue2", col = colorRamp2(c(-2,0,2), c("navy","white","red")), show_row_names = FALSE, column_title = "Tumor")ht3 <- Heatmap(mat3, name = "Tissue3", col = colorRamp2(c(-2,0,2), c("navy","white","red")), show_row_names = FALSE, column_title = "Metastasis")
# 横向拼接ht1 + ht2 + ht3
# 保存pdf("multi_heatmap.pdf", width = 15, height = 8)draw(ht1 + ht2 + ht3)dev.off()这个功能在需要展示”同一批基因在不同条件下”的表达模式时特别有用。你还可以用 row_order 让三张热图的基因顺序保持一致。
4. 踩坑记录
坑1:pheatmap 报错 “margins too large”
症状:基因名/样本名太多,R 报 figure margins too large。
解决:
# 方法1:调小字体pheatmap(expr_matrix, fontsize_row = 4, fontsize_col = 6)
# 方法2:不显示行名pheatmap(expr_matrix, show_rownames = FALSE)
# 方法3:增大画布(在 png/pdf 前设置)pdf("heatmap.pdf", width = 20, height = 30)pheatmap(expr_matrix)dev.off()坑2:scale=“row” 遇到全是相同值的行
如果某基因在所有样本中表达量完全一样,标准差 = 0,scale("row") 会算出 NaN,热图里的该行是一片灰色。
# 排查row_sds <- apply(expr_matrix, 1, sd)table(row_sds == 0)
# 处理:过滤掉恒定基因,或给标准差加个极小值expr_filtered <- expr_matrix[row_sds > 0.01, ]实际数据中完全恒定的基因很少见(除非是 spike-in),但低表达基因近似恒定值的蛮多。
坑3:pheatmap 和 ComplexHeatmap 的行列顺序不一致
两个包的聚类算法参数默认值不同,pheatmap 默认 clustering_method = "complete",ComplexHeatmap 可以自定义。如果先画了 pheatmap 探索数据,再用 ComplexHeatmap 出图,聚类结果可能不一样。
# 统一聚类方法Heatmap(expr_matrix, clustering_method_rows = "complete", clustering_method_columns = "complete")坑4:ComplexHeatmap 图例标题不显示
ComplexHeatmap 的图例标题 name 参数和 HeatmapAnnotation 里的 title 是两个东西。如果图例没标题,检查:
# Heatmap 的 name 必须设Heatmap(expr_matrix, name = "Expression") # 正确
# HeatmapAnnotation 里每个注释单独设 titleHeatmapAnnotation( Group = ..., annotation_legend_param = list(Group = list(title = "Sample Group")))坑5:colorRamp2 颜色看起来不对
colorRamp2 的颜色插值是线性的,但如果中间断点不对称,颜色过渡会偏:
# 不对称断点导致白色不在中间colorRamp2(c(-3, 0, 1), c("blue", "white", "red"))# 白-红区间只有1个单位,蓝-白有3个单位
# 尽量设对称断点colorRamp2(c(-2, 0, 2), c("blue", "white", "red"))坑6:中文标签在 PDF 里变成方框
生信文章虽然以英文为主,但如果你要给国内报告画中文标签:
# PDF 里显示中文需要额外指定字体library(showtext)showtext_auto()font_add("SimHei", "/usr/share/fonts/truetype/simhei.ttf") # 黑体
# 然后在 Heatmap 里指定Heatmap(expr_matrix, column_title = "基因表达热图", column_title_gp = gpar(fontfamily = "SimHei"), ...)5. 小结
| 需求 | 推荐工具 | 代码量 | 灵活性 |
|---|---|---|---|
| 快速探索数据 | pheatmap | 3行 | ★★☆ |
| 加注释出图 | pheatmap | 15行 | ★★★ |
| 复杂排版拼图 | ComplexHeatmap | 30行+ | ★★★★★ |
| 发表级单图 | 两者都可 | — | — |
| oncoprint/UpSet | ComplexHeatmap | — | ★★★★★ |
我的习惯:探索数据时在 RStudio 里用 pheatmap 快速预览,确定分组和配色方案后,用 ComplexHeatmap 出最终版。colorRamp2 + HeatmapAnnotation 的组合基本能满足 95% 的生信热图需求。
配色小抄:
- 表达量热图:白→橙→红,或 viridis
- 差异分析热图:蓝→白→红
- 相关性热图:白→粉→深红
- 甲基化热图:蓝→黄→红
本文于 2025-09-14 在 Debian 13 + R 4.4.1 上实测完成。pheatmap 1.0.12,ComplexHeatmap 2.18.0。所有代码可直接复制运行。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!