热图聚类:注释条与配色全解
跟完这篇,您能亲手画出一张读得懂、不会误导人的表达热图:知道该不该做 z-score、行列怎么聚、如何在上方加分组与临床注释条、如何选一套 0 居中的发散配色、最后把这些参数拼成一条命令一次出图,并且清楚聚类结果哪些能信、哪些是「聚出来的假象」。
热图本身只是把数值映射成颜色的方格。真正决定它好不好用、会不会骗人的,是四个常被忽略的选择:要不要标准化、按什么聚类、注释条怎么配、配色断点怎么定。下面按「照着做」的顺序逐个拆开,用 pheatmap 演示核心逻辑(上手最快),复杂注释再补一段 ComplexHeatmap。
准备输入:一张归一化表达矩阵
热图的输入是一张表达矩阵 mat:行是基因、列是样本,值是已经归一化过的表达量,例如 DESeq2 的 vst/rlog 转换矩阵,或 log2(TPM+1)。别把原始 count 直接铺成热图。
它长这样:
| Ctrl_1 | Ctrl_2 | Treat_1 | Treat_2 | |
|---|---|---|---|---|
| GeneA | 8.2 | 7.9 | 3.1 | 3.4 |
| GeneB | 2.4 | 2.6 | 9.0 | 8.7 |
| GeneC | 5.1 | 5.3 | 5.0 | 4.8 |
画之前先看一眼形状,确认行是基因、列是样本、没有 NA:
dim(mat) # 行数(基因)× 列数(样本)
mat[1:3, 1:3] # 瞄一眼左上角
range(mat) # 值域,确认是归一化后的量而非原始 count
如果您还没走到「有一张归一化矩阵」这一步,先看 表达矩阵怎么准备;矩阵里的基因通常是先选出来的差异基因或 marker,怎么挑见 DESeq2 差异分析。
要不要 scale:几乎总要按行做 z-score
这是画热图第一个、也是最关键的决定。
直接把原始表达值铺成热图,问题是高表达基因会主导整张图的颜色。一个管家基因表达量是普通基因的几十倍,它那一行会亮得刺眼,而您真正关心的、在组间有差异的中低表达基因,全挤在颜色条的一小段里,几乎看不出变化。
解决办法是按行做 z-score 标准化:每个基因单独减去自己这一行的均值、再除以标准差。这样每一行都变成「相对自己的高低」,图上比较的是「这个基因在哪些样本里偏高、哪些偏低」,而不是「哪个基因绝对值大」。
library(pheatmap)
# scale = "row" 让 pheatmap 自动按行做 z-score
pheatmap(mat, scale = "row")
scale 三个取值该怎么选:
scale 取值 | 含义 | 什么时候用 |
|---|---|---|
"row" | 每行减均值、除标准差(z-score) | 看基因表达「模式」——绝大多数差异/marker 热图,常规默认 |
"none" | 不标准化,直接映射原值 | 矩阵本身就有意义:相关系数、富集打分、已经标准化过的值 |
"column" | 按列(样本)标准化 | 少见,一般不用于表达谱 |
按行 scale 是热图看表达模式时的常用惯例,不是铁律:只有当您确实想展示绝对水平(如一张相关系数矩阵、一张富集打分表),才用 "none"——那些值本身就有意义,不该再被行内标准化抹平。z-score 之后,一行的值有正有负、以 0 为中心,这正好为后面的发散配色埋下伏笔。
注意:
scale只改变颜色映射,不改变您喂进去的数据本身。若您先自己算好了 z-score 再传入,就把scale设为"none",别让 pheatmap 重复标准化。
行列聚类怎么选:距离与联动方式
聚类做的事,是把「模式相近」的行(或列)排到一起,让热图呈现出色块结构。两个参数决定它怎么聚:距离度量和联动方式(linkage)。
距离度量:怎么算「两行有多像」
| 距离度量 | 看的是 | 适合 |
|---|---|---|
euclidean(pheatmap 默认) | 数值绝对接近程度 | z-score 后的数据,通用够用 |
correlation(1 − 皮尔逊相关) | 走势/趋势是否一致 | 关心「一起涨落、模式相同」的基因 |
在已经 z-score 过的数据上,欧氏距离和相关性其实高度相关,多数时候 euclidean 就够;当您明确关心「趋势一致」而不在意整体高低时,换 correlation。
联动方式:一簇和另一簇之间的距离怎么定
| 联动方式 | 特点 | 说明 |
|---|---|---|
complete(pheatmap 默认) | 簇紧凑、大小均衡 | 最常用 |
average | 折中 | 常见 |
ward.D2 | 倾向大小相近的簇,色块更规整 | 好看 ≠ 有生物学意义,别把整齐当结论 |
pheatmap(mat,
scale = "row",
clustering_distance_rows = "correlation", # 按趋势聚基因
clustering_distance_cols = "euclidean", # 样本按整体距离
clustering_method = "ward.D2") # 联动方式
实操建议:
- 常先只聚行、列按已知分组手动排。样本本来就有分组(如处理 vs 对照),把它们按分组固定顺序、不参与聚类,图更好读;只让基因聚类,看每组内部的模式。
- 关掉某一维聚类:
cluster_cols = FALSE(列不聚)或cluster_rows = FALSE(行不聚)。 - 距离和联动没有唯一正确答案,换一组参数色块会变。这不是 bug,而是提醒您:聚类结构对参数敏感,结论别下太满(详见倒数第二节)。
加注释条:分组与临床信息
注释条是热图顶上/侧边那几行彩色小格,用来标注每个样本的分组、分期、亚型等信息。它让读者一眼看出「聚在一起的样本是不是同一组」,是热图信息量的关键来源。
pheatmap 用 annotation_col(给列/样本加)和 annotation_row(给行/基因加)。传入一个 data.frame,它的行名必须和矩阵的列名一一对应——pheatmap 按名字匹配,顺序不同也能对上。
# 样本注释表:行名 = 样本名,列 = 各类注释
anno_col <- data.frame(
Group = c("Ctrl", "Ctrl", "Ctrl", "Treat", "Treat", "Treat"),
Stage = c("I", "II", "I", "III", "II", "III"),
row.names = colnames(mat)
)
# 自定义每类注释的配色(可选,不给则自动配色)
anno_colors <- list(
Group = c(Ctrl = "#4C72B0", Treat = "#C44E52"),
Stage = c(I = "#DDDDDD", II = "#999999", III = "#333333")
)
pheatmap(mat,
scale = "row",
annotation_col = anno_col,
annotation_colors = anno_colors,
show_rownames = FALSE) # 基因太多时关掉行名
配注释条的两条要点:
| 注释类型 | 例子 | 建议配色 |
|---|---|---|
| 离散/分类 | 分组、分期、亚型 | 区分度高的分类色(每类一个纯色) |
| 连续/数值 | 年龄、某表达指标、打分 | 单向渐变色(浅 → 深),别用分类色 |
- 对齐靠行名,不是靠顺序:
anno_col的row.names要和colnames(mat)完全一致。 - 注释别堆太多,两三行足矣。挤十几行既看不清、也会喧宾夺主。
若您需要更灵活的注释(多个注释块、条形/点图注释、行列同时复杂注释),换 ComplexHeatmap:
library(ComplexHeatmap)
# 顶部注释:分组色条 + 一个连续指标的柱状注释
top_anno <- HeatmapAnnotation(
Group = anno_col$Group,
Score = anno_barplot(runif(ncol(mat))), # 举例:每样本一个数值
col = list(Group = c(Ctrl = "#4C72B0", Treat = "#C44E52"))
)
Heatmap(t(scale(t(mat))), # 先按行 z-score 再画
top_annotation = top_anno,
show_row_names = FALSE,
name = "z-score")
ComplexHeatmap不像 pheatmap 有内置scale参数,需要您自己先标准化。t(scale(t(mat)))是「转置 → 按列 scale → 转回」的常用写法,等价于按行做 z-score。
配色与断点:发散配色、0 居中,别用彩虹
做完行 z-score,每格的值是「高于/低于该基因平均」,天然分正负两个方向。配色就该顺着这个语义来。
用发散配色(diverging),让 0 落在中性色上。典型是「蓝 — 白 — 红」:负值蓝、0 白、正值红。这样颜色本身就传达了方向,读者不用查图例也知道红 = 偏高、蓝 = 偏低。
library(RColorBrewer)
# 发散配色:蓝→白→红(RdBu 反转,让红落在高值端)
my_col <- colorRampPalette(rev(brewer.pal(11, "RdBu")))(100)
# 断点对称,把白色钉在 0 上
lim <- 2 # z-score 截断到 ±2,压掉极端值
breaks <- seq(-lim, lim, length.out = 101) # n 个颜色需要 n+1 个断点
pheatmap(mat,
scale = "row",
color = my_col,
breaks = breaks)
两个关键点:
- 断点要对称、以 0 为中心(这里 −2 到 +2)。如果不手动设
breaks,颜色范围会跟着数据最小/最大值走,一旦正负不对称,白色就不在 0 上,红蓝深浅失去可比性。 - 截断极端值能防止个别离群格子把整条色带「拉暗」,让主体对比更清楚。±2 只是常用惯例,按数据分布调整(分布宽就放到 ±2.5~3),不是铁律;截断只影响显示、不改数据,但要在图注里说明。
避坑:
- 别用彩虹色(rainbow/jet)。它在感知上不均匀——相同的数值差,在不同色段看起来变化幅度不同,容易读出并不存在的边界;且对红绿色盲不友好。发散或单向渐变配色在这两点上都更稳。
- 单向数据用单向配色。如果您画的不是 z-score 而是「都为正」的量(如富集
-log10(p)、丰度),就别用发散配色硬凑 0 中心,改用单向渐变(如白 → 红)。
把上面拼成一张完整图
现在把标准化、聚类、注释、配色四步合成一条命令,一次画完并导出出版级 PDF:
library(pheatmap)
library(RColorBrewer)
# 1. 样本注释表:行名必须等于 colnames(mat)
anno_col <- data.frame(
Group = c("Ctrl", "Ctrl", "Ctrl", "Treat", "Treat", "Treat"),
Stage = c("I", "II", "I", "III", "II", "III"),
row.names = colnames(mat)
)
anno_colors <- list(
Group = c(Ctrl = "#4C72B0", Treat = "#C44E52"),
Stage = c(I = "#DDDDDD", II = "#999999", III = "#333333")
)
# 2. 发散色带 + 对称断点(截断到 ±2)
my_col <- colorRampPalette(rev(brewer.pal(11, "RdBu")))(100)
breaks <- seq(-2, 2, length.out = 101)
# 3. 一次画完
pheatmap(mat,
scale = "row", # 按行 z-score
cluster_rows = TRUE, # 聚基因
cluster_cols = FALSE, # 样本按分组固定顺序,不聚
clustering_distance_rows = "correlation",
clustering_method = "ward.D2",
color = my_col,
breaks = breaks,
annotation_col = anno_col,
annotation_colors = anno_colors,
show_rownames = FALSE,
filename = "heatmap.pdf") # 直接导出 PDF
常用参数速查:
| 参数 | 作用 | 常用取值 |
|---|---|---|
scale | 标准化方向 | "row" |
cluster_rows / cluster_cols | 是否聚类该维 | TRUE / FALSE |
clustering_distance_rows | 行距离度量 | "euclidean" / "correlation" |
clustering_method | 联动方式 | "complete" / "ward.D2" |
annotation_col | 样本注释表 | data.frame,行名 = 样本名 |
annotation_colors | 注释配色 | list |
color + breaks | 主色带与断点 | 发散色 + 对称 seq(-2, 2, ...) |
show_rownames | 显示行名 | FALSE(基因多时) |
filename | 导出图片 | "heatmap.pdf"(也支持 png/tiff) |
别被聚类误导:聚类总能聚出东西
最后一节最重要,因为它关乎结论对不对。
层次聚类对任何数据都会给出一棵树、切出几块色块——哪怕数据完全是随机噪声。 图上出现整齐的色块,非常容易让人觉得「看,样本自然分成了两类」,但这可能只是算法的产物,不是数据里真有的结构。
守住这几条,能挡住大部分误读:
- 别拿聚类当分组证据的「独立验证」。如果您先挑了组间差异基因、再用这些基因聚类,样本当然会按组分开——这是循环论证,不是发现。这种图只能用来「展示」差异,不能用来「证明」分组成立。
- 换参数看稳不稳。把距离从
euclidean换成correlation、联动从complete换成ward.D2,如果主要色块结构大体还在,说明它相对稳健;如果一换就散,就别对它下强结论。 - 树高(dendrogram 的分叉高度)有信息。分叉点越高,说明两簇差异越大。两个簇在很低的高度才分开,意味着它们其实很像,别硬说成两类。
- 注释条是照妖镜。加上真实分组注释条后,如果聚出来的色块和分组对不上,别急着改参数「凑」到对齐——那往往说明差异没那么干净,这本身就是要如实报告的结果。
- 看的是模式,不是单格。热图适合展示整体趋势,不适合精读某一个基因在某一个样本的具体数值——那种精度请回到原始表格或箱线图。
一句话:热图是很好的探索和展示工具,但它天生「爱讲故事」,别把它讲的每个故事都当真。 把它和统计检验(如 DESeq2 差异分析)互相印证,才踏实。
在平台上跑一遍
上面这套流程——按行 z-score、选聚类距离与联动、加分组与临床注释条、配 0 居中的发散色、导出出版级图——在「百沐一下」上不用写一行代码也能完成:上传您的表达矩阵和样本分组表,用一句话说清「按行 z-score,蓝白红发散配色截断到 ±2,顶部标注分组和分期,只聚基因不聚样本」,智能绘图会替您选好参数并出图,您再对话式微调配色和注释即可。
到 https://app.baimuyixia.com 传数据即可开始,或先看 智能绘图使用指南 了解出图流程。
相关
- 表达矩阵怎么准备:热图的输入从哪来、怎么归一化。
- DESeq2 差异分析:先挑出要画进热图的差异基因。
- 火山图怎么画:与热图搭配展示差异结果的另一张图。
- 富集分析:把热图里的基因簇进一步做功能解读。