
前些天整理单细胞项目结果又被审稿人问了一句“你的热图除了颜色深浅还能看出什么”。这句话戳到我了。单细胞转录组分析里热图几乎是标配但绝大多数人画出来的热图就是一个“表达量颜色块”既看不出细胞亚群的差异结构也讲不出基因模块的变化趋势更别说把差异基因、富集通路、拟时轨迹这些东西和表达模式串在一起。所以这次我想聊的不是“怎么用pheatmap画一张热图”而是从单细胞基因表达可视化的核心逻辑出发把热图当作一张可以承载多维信息的主图来改造。本文整理的是我在实际项目里反复调过的方案和踩过的坑主题就叫“单细胞基因可视化之热图的根本改造2”——如果你也受够了那种“千图一面”的普通热图这份笔记应该能帮上忙。1. 热图改造的核心思路为什么普通热图不够用1.1 单细胞数据的热图本质上是在展示“分组结构”很多教程会把热图简单理解成“展示基因表达高低”其实这个理解太浅了。单细胞转录组项目里的热图不管画哪种背后都藏着一个核心需求用基因表达模式来定义或者验证细胞的identity。换句话说热图不是给人看颜色深浅的而是让人一眼看出“这群细胞和那群细胞的分子特征差异在哪”。这就引出了普通热图的两个根本问题。第一行和列的排列顺序往往没有生物学意义或者只有字母序比如基因名从A到Z这样的热图看完了只能得到“基因A高表达、基因B低表达”这种零散信息。第二热图旁边缺少上下文信息——细胞是哪来的、属于哪个亚群、关键Marker基因的表达丰度分布、通路活性高低、差异是否显著这些信息如果全部挤在主图里就会变成一团彩色噪声。所以“根本改造”的第一件事就是改变热图的信息组织方式行基因/特征按表达模式或功能模块聚类排序列细胞/样本按细胞亚群或实验分组排序同时把各种注释信息放到热图外侧。这样做的目的不是说主图不重要了而是让主图和注释一起构成“可读”的证据链。1.2 三个维度从“颜色矩阵”升级到“结构视图”这里我要重点说下搜索热词里反复出现的“三个维度的热图”。单细胞热图的数据矩阵天然有三个维度——细胞、基因、表达值。普通热图其实只用了二维用行、列的位置分别对应基因和细胞用颜色深浅对应表达值。这没错但问题是它把所有信息强行摊平到一张二维色块上丢失了“上下文”。我理解的“三维热图”不是真的去画3D立体柱状图那种图看着炫酷实际很难读出精确的数值关系而是给热图叠加三层结构信息第一层主热图矩阵展示基因×细胞的表达模式通常经过Z-score归一化第二层细胞侧的结构注释包括亚群来源、样本批次、细胞周期评分、拟时序位置第三层基因侧的生物意义注释比如是否属于某个通路、是否是Marker基因、差异显著性标记。这三层信息叠在一起热图就从“表达量色块”变成了“细胞状态结构视图”——谁和谁像、哪类基因在哪群细胞里特异地活跃一目了然。这也是我在实际项目中尝试过效果最明显的改造方向。1.3 改造前后的信息密度对比我以前做单细胞项目时热图基本是这样画的用Seurat的DoHeatmap函数出一个初版行是Top差异基因列是细胞按cluster排序颜色是红蓝渐变。然后把这个图丢到PPT里旁边再放一个小提琴图、一个气泡图。问题在于单个热图承载不了逻辑关系审稿人或者老板看到热图之后往往还得再问一句“这些基因在功能上有什么联系”。所以到了“改造2”阶段我给自己定了三条硬性标准热图必须能单独讲清楚一个生物学结论比如“这簇基因负责炎症响应特异地在巨噬细胞亚群中上调”热图必须能叠加统计信息差异基因的显著性标记、通路富集的分组背板;热图必须能在不参考其他图的情况下让读者看懂细胞亚群和基因模块的对应关系。为了达到这三条我用ComplexHeatmap重写了热图绘制流程并且把所有公共注释信息亚群颜色、基因分组、给富集通路、差异显著性都变成了“可配置的组件”。这样同一个基础热图稍改参数就能复用到不同项目中。2. 热图根本改造的基础设施聚类、归一化与配色决策2.1 聚类不是“点一下就行”的按钮画热图的时候行和列都会被聚类但默认聚类的坑非常深。很多教程就直接pheatmap(mat)结果出来之后行、列顺序由欧氏距离和ward.D2决定。这在单细胞数据里往往不是最优选择。举个例子单细胞表达矩阵稀疏性极高存在大量零值。如果直接拿原始的count矩阵或者log1p后的矩阵算距离零值过多会导致样本之间“因为都是零而显得相似”这种相似性掩盖了真正的生物学差异。所以我的习惯是先对基因做Z-score归一化按行再用归一化后的矩阵做聚类。Z-score的意义在于把不同基因的绝对表达量拉到一个可比尺度上。基因A的平均表达量是5、基因B的平均表达量是50如果没有归一化它们出现在同一张热图里时基因B会把基因A的差异“压扁”。Z-score之后每个基因的表达分布均值是0、标准差是1热图上的颜色只反映“相对上下调”不反映绝对表达量这才能把调控模式凸显出来。具体在R里怎么做我放一段常用代码# 假设 mat 是基因×细胞矩阵行基因列细胞 mat.raw - as.matrix(GetAssayData(sce, layer data)) # 仅保留感兴趣基因例如Top差异基因或Marker基因 mat.sub - mat.raw[genes_interest, ] # 按行基因做Z-score归一化 mat.z - t(scale(t(mat.sub)))注意scale是按列操作的所以要先把矩阵转置Z-score后再转置回来。这个顺序搞反了结果会变成“按细胞归一化”那整个热图的模式就会彻底变味。聚类距离的选择上我倾向于用euclidean配ward.D2或者pearson相关距离。对于单细胞数据Pearson相关距离的好处是它不关心绝对表达量只关心表达“模式”是否一致。也就是说两个基因一个是高表达一个是低表达但它们在所有细胞里的变化趋势是同涨同跌的Pearson距离会认为它们很接近。这在看共调控基因模块时非常有用。2.2 颜色映射的坑红色代表高表达蓝色代表低表达没那么简单热图的颜色配置很多新手以为只是个审美问题。实际上颜色映射决定了读者对“差异”的感知强度选不好会误导解读。最常见的是红蓝渐变RdBu或红绿渐变。在单细胞文献里高表达通常用红色、低表达用蓝色这种习惯跟直觉一致。问题出在中值颜色白色或浅灰色上。如果Z-score之后的数据大概在-2到2之间分布那么中值颜色对应的就是0附近即表达量没有显著偏离均值。但如果你用默认的渐变色阶最大值和最小值被拉伸到色阶两端那么大部分普通基因的Z-score值集中在-0.5到0.5之间显示出来基本是一个颜色热图的层次感就没了。我的做法是手动控制颜色断点。用circlize::colorRamp2指定几个关键值对应的颜色而不是依赖连续渐变自动映射。比如library(circlize) col_fun - colorRamp2( c(-2, -1, 0, 1, 2), c(#2166ac, #92c5de, #f7f7f7, #f4a582, #b2182b) )这样颜色集中在-2到2之间超出这个区间的极端值会被锁定为最深色不会让色阶“溢出”。如果数据里有某个基因在某些细胞里Z-score达到了5不截断的话整个热图都会被这一个极值带偏。另外千万别小看色盲友好配色。生物学论文审稿里红绿配色是重灾区——红绿色盲是最常见的色盲类型。我自己在改造代码时统一换成了蓝-白-红系或者viridis系实测下来既好看又安全。2.3 行列注释的布局信息放对位置图才讲得清楚热图改造里最立竿见影的一步是给热图加上注释条annotation bar。注释条的本质是在热图旁边补充“主图表达矩阵里看不出来的分组信息”。行注释gene side可以放的信息包括基因所属通路、基因类型如离子通道、转录因子、细胞因子、差异分析中的显著方向和log2FC大小。列注释cell side可以放的信息包括细胞亚群cluster、样本批次、供体ID、细胞周期评分、拟时序分支。在ComplexHeatmap里列注释放在顶部或底部行注释放在左侧或右侧。我的建议是细胞身份类信息如cluster放顶部因为顶部最容易看基因功能类信息放右侧因为它是对基因的补充说明不抢占主视觉。library(ComplexHeatmap) library(ggplot2) col_ha - HeatmapAnnotation( cluster sce$seurat_clusters, sample sce$sample_id, col list( cluster cluster_colors, sample sample_colors ), annotation_height unit(c(4, 4), mm), show_annotation_name TRUE ) row_ha - rowAnnotation( pathway gene_pathway_factor, significant gene_sig, col list( pathway pathway_colors, significant c(up #d73027, down #313695, ns #969696) ), annotation_width unit(c(6, 4), mm) )这样搭好框架后行和列的方向都可以自由切换注释和主图对齐问题由ComplexHeatmap内部处理很少出现“注释条和热图错位”的情况。这是pheatmap做不到的——pheatmap的注释列在边上但排版可控性差很多。3. 实操基于ComplexHeatmap实现三个维度的热图改造3.1 从pheatmap迁移到ComplexHeatmap的流程很多人的热图入门工具是pheatmap。它简单、够用但一旦要做“三个维度的热图”pheatmap就力不从心了。pheatmap主要痛点注释只能放“列”或“行”中的一个层面无法同时精确控制多个注释图例无法拆分行/列的多层级分组无法在热图内部叠加显著性标记或分组边框。迁移到ComplexHeatmap有一段小陡坡但它值得学。核心逻辑是“热图就是一个对象任何注释、拆分、标记都是往这个对象上叠图层”。我用一个案例来说明完整迁移过程。假设我们有一个单细胞数据集经过Seurat标准流程处理得到了细胞亚群、Marker基因列表、差异基因列表和通路富集结果。现在要画一张Top差异基因热图同时展示每个细胞来自哪个亚群顶部注释每个细胞来自哪个样本顶部注释第二层每个基因是否属于目标通路右侧注释行按Cluster差异基因分组并用色块标出基因归属的差异群组表达矩阵Z-score归一化。代码骨架如下library(Seurat) library(ComplexHeatmap) library(dplyr) # 1. 提取数据矩阵仅保留Marker基因 Idents(sce) - seurat_clusters markers - FindAllMarkers(sce, only.pos TRUE, min.pct 0.25, logfc.threshold 0.5) top_markers - markers %% group_by(cluster) %% slice_max(n 10, order_by avg_log2FC) mat - GetAssayData(sce, layer data)[unique(top_markers$gene), ] mat.z - t(scale(t(mat))) # 2. 细胞注释 col_ha - HeatmapAnnotation( cluster as.character(sce$seurat_clusters), sample as.character(sce$sample_id), col list( cluster setNames(ggsci::pal_lancet()(9), levels(sce$seurat_clusters)), sample setNames(ggsci::pal_npg()(6), unique(sce$sample_id)) ) ) # 3. 行分组按基因属于哪个cluster的marker来分组 gene_cluster - top_markers$cluster names(gene_cluster) - top_markers$gene gene_cluster - gene_cluster[rownames(mat.z)] # 4. 画热图 ht - Heatmap( mat.z, name Z-score, col col_fun, top_annotation col_ha, cluster_columns TRUE, cluster_rows FALSE, row_split gene_cluster, row_title_rot 0, row_gap unit(2, mm), cluster_row_slices TRUE, show_row_names TRUE, row_names_gp gpar(fontsize 8), column_title Single-cell Heatmap ) draw(ht, merge_legend TRUE)关键点在于row_split gene_cluster。这会按照基因所属的cluster把热图从行方向切成若干块每个块顶部会有一个“cluster X markers”的标题。同时cluster_rows FALSE意思是每个块内部的基因顺序保持差异基因列表的排序不重新聚类。这样做的原因是Marker基因列表通常按log2FC降序排列块内的顺序本身就是“最特异基因在前”保留这个顺序比重新聚类更能体现基因的rank意义。3.2 聚类热图加趋势图的组合画卷法单独一张热图再好看能承载的信息还是有限。我在“改造2”里用得最多的一种组合方案是中间画聚类热图左侧画基因模块的表达趋势图右侧画富集条目。这套组合直接呼应了搜索热词里的“聚类热图趋势图富集条目”。思路是这样的先用层次聚类或者K-means把基因分成若干模块module。然后热图部分展示每个模块基因的表达模式趋势图部分按细胞亚群计算每个模块的平均表达量画出折线图或者箱线图展示这个模块在不同亚群中的动态变化富集条目部分对每个模块的基因做GO/KEGG富集把最显著的通路名放在热图右侧并以颜色或者字号标注p值。实现上用ComplexHeatmap的rowAnnotation加自定义panel函数或者用ComplexHeatmap::anno_*系列。最简单的方式是用anno_line画趋势线# 先计算每个cluster的平均模块得分 module_score - function(mat, module_genes, sce) { sub - mat[intersect(module_genes, rownames(mat)), ] if (is.null(dim(sub))) return(NULL) # 每个cluster列取平均值 cluster_mean - t(apply(sub, 1, function(x) { tapply(x, sce$seurat_clusters, mean) })) # 模块整体趋势再求基因平均 apply(cluster_mean, 2, mean) } trend_list - lapply(module_list, function(gn) { module_score(mat, gn, sce) }) # 画趋势图注释 trend_ha - rowAnnotation( trend anno_lines( trend_list, add_points TRUE, ylim c(0, max(unlist(trend_list))), gp gpar(col module_colors), pt_gp gpar(col module_colors, cex 0.5), axis TRUE ) )注意anno_lines的参数结构对象不太直观第一次用容易把trend_list的格式传错。这里要求的是一个列表列表里每个元素对应一行基因的数值向量如果每个模块有多行基因需要先按行计算模块平均表达再传入。另外模块数量多、每个模块基因数量差异大时趋势图Y轴上限最好手动定避免某些模块的极端高表达把其他模块压扁。富集条目部分通常不需要画在热图上而是在热图右边用文字注释粘贴最显著通路的名称。ComplexHeatmap的anno_text或者rowAnnotation里的text参数都能实现。我的习惯是每个模块最多显示2~3条通路名否则右侧注释区会被文字堆满。3.3 差异基因环形热图的实战细节搜索热词里的“差异基因环形热图”也是一个值得讲的结构。环形热图的本质是把热图从左到右的矩形布局改成圆形布局。这么做不是为了炫技——当基因数量很大行数超过200甚至500时矩形热图的高宽比变得非常不协调横向拉得很宽、纵向挤得很密行名根本显示不全。环形热图能把更多基因塞进同样面积的画布里同时中部空白区域还可以叠加更多信息。在R里画环形热图用的还是ComplexHeatmap只不过把Heatmap放进draw时设置circular TRUE。以下是关键参数ht_circ - Heatmap( mat.z, name Z-score, col col_fun, circular TRUE, cluster_rows TRUE, cluster_columns FALSE, show_row_dend TRUE, show_column_names FALSE, use_annotation_legend FALSE, row_names_gp gpar(fontsize 6), top_annotation HeatmapAnnotation( cluster sce$seurat_clusters, col list(cluster cluster_colors) ) ) draw(ht_circ, merge_legend TRUE)环形热图的坑在于行方向的标签旋转和位置控制特别不直观row_names_gp字号小了看不清大了互相重叠而且行名默认是沿着圆的径向排布如果基因名过长会在圆的外侧“飞出去”。我的处理方法是只显示关注的一小部分基因名比如每个Top模块的Top3基因其余不显示。另外一个实用技巧环形热图中间的空白区域非常适合放一个“表达总览”的小图比如各细胞亚群的基因表达雷达图或平均表达量柱状图。ComplexHeatmap里可以通过draw之后用grid在圆心位置添加内容实现但这个操作比较进阶普通应用能控制好外圈布局就够了。4. 常见问题与排查技巧实录4.1 行列顺序不是自己想要的怎么办这是热图改造里最常被问的问题。有人画出来的热图列的顺序不是按亚群编号排列的而是按聚类树结构排列的导致同一群细胞没有连在一起图形很乱还有人希望行的顺序严格按照自己提供的基因列表顺序但ComplexHeatmap默认会重新聚类。解决方案有两个。第一cluster_columns FALSE然后手动设置column_order为细胞顺序的向量第二cluster_rows FALSE让行顺序严格保留输入矩阵的行名顺序。比如# 手动指定列顺序先按cluster排序再按组内表达量均值排序 cell_order - order(sce$seurat_clusters, colMeans(mat.z)) Heatmap( mat.z, cluster_columns FALSE, column_order cell_order )提示手动指定列顺序之前一定要确认cell_order的长度和矩阵列数一致而且顺序向量里的每个元素都能在矩阵列名中找到。不然draw的时候会直接报错。4.2 Z-score之后出现NaN或者Inf值单细胞表达矩阵里如果一个基因在所有细胞里的表达都是0那么scale之后标准差为0Z-score的结果就会出现NaN。这些NaN在热图上会被当作缺失值显示成灰色非常难看。我用两个办法处理一是过滤直接剔除全零或近乎全零的基因二是填充把NaN值替换为0意思是“没有偏离均值”。代码里我一般这样处理# 过滤全零行 keep - apply(mat.raw, 1, function(x) var(x) 0) mat.raw - mat.raw[keep, ] # 少量NaN兜底 mat.z[is.na(mat.z)] - 0这个兜底有生物学上的合理性某基因在所有细胞里恒定不变它在热图里应该显示为白色Z-score0而不是缺失的灰色。灰块会让人误以为检测失败。4.3 注释条的颜色和热图图例重复造成混乱ComplexHeatmap的图例默认会自动合并。当你有多个注释比如cluster有9个水平、sample有6个水平基因分组有4类再加上Z-score的连续色阶图例会一大堆。处理办法merge_legend TRUE把注释图例和主图例合并排列用heatmap_legend_param和annotation_legend_param分别控制各图例位置如果注释图例太多可以考虑只保留cluster注释的图例sample和pathway注释的图例用show_legend FALSE隐藏然后在图题或图注里文字说明。4.4 热图保存时字体丢失或显示模糊R里直接ggsave或pdf保存热图有时会出现中文字体变方块、英文字体大小不一致的问题。ComplexHeatmap的字体由gpar(fontfamily ...)控制。如果要出版级别的图我建议保存为PDF或者300dpi以上的TIFFpdf(heatmap_final.pdf, width 8, height 10) draw(ht, merge_legend TRUE) dev.off()不要直接用png保存低分辨率再插入Word或PPT里拉大那样文字边缘一定发虚。另外如果期刊要求矢量图PDF是首选如果要求位图至少width3000, height4000, res300。4.5 大规模矩阵画图卡顿上千行乘上数万列的热图直接绘制内存占用会非常夸张。三个可以优化的方向抽样细胞每组亚群随机抽50~100个细胞画热图表型趋势不会变速度提升10倍以上只画Top基因筛选每个亚群前10~20个Marker基因而不是把所有差异基因都画进去使用Heatmap的raster TRUE参数把热图栅格化为位图保存大幅降低渲染压力。Heatmap( mat.z, raster TRUE, raster_device png, raster_quality 2 )注意raster TRUE会把热图主体部分保存为位图导出矢量PDF时能大幅减小文件体积但栅格化之后如果缩放大到某比例图表面可能看出像素点。期刊图建议关闭raster先用抽样策略减规模。5. 工具链选型与改造路线图5.1 为什么我最终选择ComplexHeatmap市面上画热图的工具不少R里有pheatmap、ComplexHeatmap、ggplot2的geom_tilePython里有seaborn的clustermap、plotly的热力图、matplotlib的imshow。从我自己的项目实践来看如果目标是做“三个维度的热图”这类结构化信息密度极高的可视化ComplexHeatmap是当前最合适的选择。原因有三个它对热图的拆分支持最好。row_split、column_split能把热图切成任意复杂度实现分组而不重排数据它把注释当成“独立组件”。注释和主图是平行关系可以非常灵活地变换位置、控制颜色、叠加图例它支持自定义图形嵌入。只要你会写R的grid绘图逻辑几乎任何内容都能嵌入热图的注释区域。相比之下seaborn的clustermap美观但定制空间有限复杂注释全靠手工拼图。ggplot2的geom_tile灵活但性能差几万列的数据它根本扛不住。如果你的项目规模很大ComplexHeatmap确实能省心很多。5.2 “改造2”的完整落地路线图根据我在两个真实单细胞项目里的实践总结一条可复制的落地路线第一步整理数据。从Seurat或Scanpy对象中导出表达矩阵、细胞元数据、Marker基因列表。这个阶段的关键是保持基因名和细胞ID的格式统一避免后面join时出现匹配不上。第二步归一化。按行做Z-score过滤低变异基因确认无NaN。第三步定注释面板。列注释至少包含cluster和sample行注释至少包含基因所属模块。配色方案提前定好全局用同一套颜色。第四步画基础热图。先不追求复杂注释把主热图的行列顺序跑通。第五步迭代加注释。加趋势图、富集条目、显著性标记每次只加一个元素确认图片没有信息重叠再继续。第六步导出与检查。用PDF导出后放大到200%检查字体和色块边界重点看行名有没有重叠、色阶对比是否明显。最后再分享一个小经验热图改造不要一口气追求把所有信息都画进去。信息过载比信息不足更可怕一张图上塞了五个维度的注释人眼反而抓不住重点。最好的做法是做一个“主图配套注释”的系列图——主图承载表达模式扩展图承载富集结论两者配合讲完一个完整的生物学故事。这也正是“根本改造2”这个标题里“根本”二字的含义不是给热图换个皮肤而是把表达矩阵从数据展示工具变成生物学结论的推导工具。