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

热图聚类:注释条与配色全解

跟完这篇,您能亲手画出一张读得懂、不会误导人的表达热图:知道该不该做 z-score、行列怎么聚、如何在上方加分组与临床注释条、如何选一套 0 居中的发散配色、最后把这些参数拼成一条命令一次出图,并且清楚聚类结果哪些能信、哪些是「聚出来的假象」。

热图本身只是把数值映射成颜色的方格。真正决定它好不好用、会不会骗人的,是四个常被忽略的选择:要不要标准化、按什么聚类、注释条怎么配、配色断点怎么定。下面按「照着做」的顺序逐个拆开,用 pheatmap 演示核心逻辑(上手最快),复杂注释再补一段 ComplexHeatmap

准备输入:一张归一化表达矩阵

热图的输入是一张表达矩阵 mat:行是基因、列是样本,值是已经归一化过的表达量,例如 DESeq2 的 vstrlog 转换矩阵,或 log2(TPM+1)。别把原始 count 直接铺成热图。

它长这样:

Ctrl_1Ctrl_2Treat_1Treat_2
GeneA8.27.93.13.4
GeneB2.42.69.08.7
GeneC5.15.35.04.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")      # 联动方式

实操建议:

  1. 常先只聚行、列按已知分组手动排。样本本来就有分组(如处理 vs 对照),把它们按分组固定顺序、不参与聚类,图更好读;只让基因聚类,看每组内部的模式。
  2. 关掉某一维聚类cluster_cols = FALSE(列不聚)或 cluster_rows = FALSE(行不聚)。
  3. 距离和联动没有唯一正确答案,换一组参数色块会变。这不是 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_colrow.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)

别被聚类误导:聚类总能聚出东西

最后一节最重要,因为它关乎结论对不对。

层次聚类对任何数据都会给出一棵树、切出几块色块——哪怕数据完全是随机噪声。 图上出现整齐的色块,非常容易让人觉得「看,样本自然分成了两类」,但这可能只是算法的产物,不是数据里真有的结构。

守住这几条,能挡住大部分误读:

  1. 别拿聚类当分组证据的「独立验证」。如果您先挑了组间差异基因、再用这些基因聚类,样本当然会按组分开——这是循环论证,不是发现。这种图只能用来「展示」差异,不能用来「证明」分组成立。
  2. 换参数看稳不稳。把距离从 euclidean 换成 correlation、联动从 complete 换成 ward.D2,如果主要色块结构大体还在,说明它相对稳健;如果一换就散,就别对它下强结论。
  3. 树高(dendrogram 的分叉高度)有信息。分叉点越高,说明两簇差异越大。两个簇在很低的高度才分开,意味着它们其实很像,别硬说成两类。
  4. 注释条是照妖镜。加上真实分组注释条后,如果聚出来的色块和分组对不上,别急着改参数「凑」到对齐——那往往说明差异没那么干净,这本身就是要如实报告的结果。
  5. 看的是模式,不是单格。热图适合展示整体趋势,不适合精读某一个基因在某一个样本的具体数值——那种精度请回到原始表格或箱线图。

一句话:热图是很好的探索和展示工具,但它天生「爱讲故事」,别把它讲的每个故事都当真。 把它和统计检验(如 DESeq2 差异分析)互相印证,才踏实。

在平台上跑一遍

上面这套流程——按行 z-score、选聚类距离与联动、加分组与临床注释条、配 0 居中的发散色、导出出版级图——在「百沐一下」上不用写一行代码也能完成:上传您的表达矩阵和样本分组表,用一句话说清「按行 z-score,蓝白红发散配色截断到 ±2,顶部标注分组和分期,只聚基因不聚样本」,智能绘图会替您选好参数并出图,您再对话式微调配色和注释即可。

https://app.baimuyixia.com 传数据即可开始,或先看 智能绘图使用指南 了解出图流程。

相关