☰
inferCNV热图只显示observed?两步设置让0-9亚群标签清晰呈现
2026/10/10 4:06:00 网站建设 项目流程

跑单细胞inferCNV分析的时候,最期待的就是那张红蓝热图能清楚地按照0、1、2……9号上皮细胞亚群各自聚成一坨,结果图一出来,热图下方的dendrogram(分层聚类树)上孤零零挂着一个“observed”标签,0-9号亚群的具体分组信息完全没显示。这个问题在群里被问过无数次,我自己也实打实踩过,乍一看像绘图bug,实际上就是inferCNV对“分组信息”的传递方式和你想的不太一样。

这篇文章把这个问题彻底聊透:observed标签到底从哪来、为什么你的亚群标签没被画上去、注释文件和表达矩阵哪一步容易出错、plot_cnv参数怎么设、以及实在不行怎么用自定义热图把0-9号分组硬画出来。适合正在跑inferCNV但被热图标注卡住的人,也适合想把热图做得精致一点、能直接放进论文里的进阶玩家。

1. 搞清楚inferCNV里的observed到底从哪来

1.1 observed是“非参考细胞”的统称

inferCNV的核心逻辑其实很简单:把细胞分成两大类,一类是参考组(reference,通常是正常细胞),另一类是观察组(observation,也就是你想分析拷贝数变异的那些肿瘤细胞或上皮细胞)。在构建对象的时候,ref_group_names这个参数指定了哪些分组属于参考组,注释文件里其他所有分组,统统会被inferCNV当成观察组,统一叫observed。

所以“observed”并不是某一个具体亚群的名字,而是一个集合名词。如果你的注释文件里确实写了0、1、2……9号上皮亚群,它们全部属于observed这个大集合。问题就在于:inferCNV默认在画热图时不一定会把你这个大集合内部的细分标签逐个画出来,很多时候它干脆只给你一个大标签observed,让你误以为亚群信息丢了。

理解了这一层,再看“热图下方只显示observed”这个现象,就会明白它其实是inferCNV在告诉你:这些细胞都是观察组,至于观察组内部怎么分,那是另一码事,不会自动显示。

1.2 为什么0-9号分组没有自动出现

很多人想当然地认为:只要注释文件里有0-9号分组,跑完inferCNV热图就应该自动带上这些分组标签。实际情况完全不是这样。

inferCNV从注释文件读到的分组信息,主要用于两个地方:一是在run阶段按注释分组去做聚类分析和统计,二是在绘图阶段决定注释条怎么上色。但绘图阶段的注释条是否显示你给的标签,取决于绘图函数是否被明确告知“请按照注释分组来展示”。如果你用了默认绘图参数,或者压根没开启按注释分组的功能,inferCNV就会把所有观察组细胞当作一个整体,只输出一个observed标签。

另外还有一种非常隐蔽的情况:注释文件本身就只有“observed”一个分组。比如有些人在用Seurat做完聚类后,导出注释文件时只导了细胞barcode和一个统一的“observed”,并没有把自己0-9号的亚群标签写进去。这种时候无论你怎么调绘图参数,都不可能显示出0-9号分组,因为数据源里压根没有这些信息。

1.3 热图注释条与dendrogram的分工

要彻底理解这个问题的解决思路,需要先搞清楚热图上各种元素各自干什么。inferCNV热图的主体是CNV信号矩阵,行通常是基因,列是细胞,行列都做了层次聚类,于是有了dendrogram。聚类树反映的是细胞间CNV信号的相似程度,它只负责告诉你哪些细胞长得像,不会自动帮你把细胞分组。

真正承担“告诉读者每个细胞属于哪一组”这个任务的是热图顶部的注释条(annotation bar)。注释条是一行彩色条形,每一小段对应一个细胞,颜色代表它所属的分组。inferCNV的cluster_by_group参数打开以后,注释条才会按注释文件的group上色,并在图例里列出0、1、2……9这些标签。聚类树和注释条配合起来,你才能在图上既看到0-9亚群的亲缘关系,又看到每个细胞的归属。

所以,“热图下方只有observed”这个现象的实质是:你既没有让inferCNV按注释分组来排列和上色,也没有给inferCNV一个正确的分组注释数据。搞清楚这一点,后面所有的解决方案就顺理成章了。

2. 先动手排查:80%的问题出在注释文件与数据匹配

2.1 注释文件的正确格式

inferCNV对注释文件格式有严格要求:两列,第一列是细胞barcode,第二列是分组名称,默认用Tab分隔,建议不要带表头。一个正常的注释文件长这样:

AAACCTGCATGCCTA-1 0 AAACCTGGTAACGCA-1 1 AAACCTGGCACGTGC-1 2 AAGGTTACCAAGCTA-1 3 ...

第一列必须和表达矩阵的列名严格一致,第二列的内容可以是数字、字母、字符串,比如“0”“1”“2”或者“C0”“C1”,甚至“epi_0”。inferCNV会原样读入这些值作为分组标签。如果你的注释文件里这一列全是“observed”,那自然不可能显示出0-9号亚群。

这里最容易出问题的细节是表头和分隔符。有些教程给的示例带了表头,有些没有,两种写法在某些inferCNV版本里都能跑,但如果你用Excel另存为CSV,分隔符变成了逗号,inferCNV默认按Tab读就会把整列读成一个大字符串,轻则报错,重则把分组信息读成一坨乱码。所以我的习惯是:注释文件一律用write.table以Tab分隔输出,不要手动在Excel里编辑。

2.2 Excel打开barcode后-1变日期的坑

这是单细胞分析里最经典的隐形坑,没有之一。10X平台的细胞barcode自带“-1”后缀,比如“AAACCTGCATGCCTA-1”。一旦你用Excel打开过这个文件,哪怕只是看一眼再保存,Excel就会自作主张把“-1”识别成日期格式,结果是barcode变成“AAACCTGCATGCCTA”或者一串日期数字。

等你再用R读入这个注释文件,就会发现barcode跟表达矩阵的列名对不上了,inferCNV在匹配细胞时会丢掉大量细胞,或者干脆把所有细胞统统归成一个组。最后画热图时dendrogram下面只挂一个observed,看起来莫名其妙,实际上是你注释文件里的barcode早就被Excel搅乱了。

我自己踩过这个坑之后,立了一条规矩:跟barcode相关的文件,绝对不经过Excel中转。读取的时候用read.table并指定colClasses = c("character", "character"),确保barcode不会被转成数字或因子。即便要在R里修正barcode,也直接字符串操作完成,写出去时用quote = FALSE,彻底杜绝Excel的隐性格式污染。

2.3 用R快速验证分组有没有被inferCNV真正读进去

在重新跑分析之前,先用R把注释文件和表达矩阵检查一遍,比盲调参数高效一百倍。以下几步强烈建议做成固定流程:

# 第一步:检查注释文件的内容和分组 ann <- read.table("annotations.txt", header = FALSE, stringsAsFactors = FALSE) colnames(ann) <- c("barcode", "group") table(ann$group)

如果table(ann$group)的结果里除了“observed”之外没有任何0-9的数字,那问题就出在注释文件本身,赶紧回去重新导出注释。如果能看到0-9,说明注释文件是好的,问题出在绘图参数或者数据匹配。

# 第二步:检查barcode是否和表达矩阵列名一致 counts <- read.table("count_matrix.txt", header = TRUE, row.names = 1, check.names = FALSE) sum(ann$barcode %in% colnames(counts))

这个匹配数理论上应该等于注释文件里的细胞总数。如果少了很多,优先检查barcode是否被Excel改过格式,其次检查表达矩阵的列名是不是带了额外前缀或后缀。匹配不上时,inferCNV会直接报错或者忽略掉那些细胞,热图的分组信息自然就乱了。

3. 让dendrogram正确显示0-9号亚群:两个参数就能解决

3.1 cluster_by_group=TRUE:按你的注释显示分组

如果你的注释文件里已经有0-9号上皮亚群,并且你希望热图完全按照你自己的亚群划分来展示,那关键参数就是plot_cnv里的cluster_by_group。这个参数的作用是:告诉inferCNV在画图时使用注释文件里定义好的分组来排列细胞、上色注释条、显示分组标签。

实际代码不复杂,关键位置就在最后一个函数调用里:

plot_cnv(infercnv_obj, out_dir = "plot_dir", cluster_by_group = TRUE, output_filename = "infercnv_heatmap_with_groups.pdf")

打开output_filename输出的PDF,你会在热图注释条位置看到0-9号亚群的颜色条,图例里会列出0、1、2……9这些标签,dendrogram也会按照你自己的分组结构来组织。这个问题往往一下子就解决了。

需要特别说明的是,cluster_by_group这个名字在run()阶段也有对应参数,叫cluster_by_groups,多个s。run()里的cluster_by_groups=TRUE会让分析阶段保留按注释分组的信息,plot_cnv()里的cluster_by_group=TRUE则让绘图阶段使用这些分组信息。两个地方都建议显式写清楚,不要依赖默认值。

3.2 k_obs_groups=10:让inferCNV自动聚成10组

另一种情况:你手头没有可靠的注释分组,只想让inferCNV自己把observed细胞通过层次聚类分成10个组,看看结构怎么样。这时候用k_obs_groups参数:

plot_cnv(infercnv_obj, out_dir = "plot_dir", k_obs_groups = 10, output_filename = "infercnv_auto10.pdf")

k_obs_groups=10的意思是:把所有的observed细胞重新做一次层次聚类,按距离切成10份,然后在热图上画出分组边界和标签。这种方式的优点是省心,不需要任何额外注释,inferCNV完全根据CNV信号的相似性自动分组。缺点也很明显:自动分出来的0-9组跟你之前Seurat聚类得到的0-9号上皮亚群不保证一致,除非你的上皮细胞CNV特征非常清晰,否则两者经常对不上。

3.3 两种方式的区别与选择建议

很多人在这一步搞混,结果浪费了大量时间。我直接给结论:

  • 如果注释文件里已经有你精心定义好的0-9号上皮亚群,就用cluster_by_group = TRUE。这是最推荐的做法,因为你的亚群往往结合了marker基因、生物学知识,比纯算法聚类结果更有意义。
  • 如果你只是探索性分析,没有现成的亚群划分,才用k_obs_groups = 10。注意这里的0-9是inferCNV自己现算出来的,不代表你的生物学分组。
  • 两者同时打开并且值不对应时,会出现注释条标签和分割线打架的情况。所以不要盲目同时传,先想清楚你想要哪种效果。

另外要说明一个点:k_obs_groups默认是1,如果不改它、也不开cluster_by_group,热图上就只有一个observed标签——这恰恰就是你看到的那个现象。所以看到“只有observed”时,第一反应应该是回头看看自己这两个参数到底传了什么。

3.4 plot_cnv完整运行参数示例

把上面两种方式合成一个可配置的示例,方便直接抄:

library(infercnv) # 场景A:使用已有注释分组,显示0-9号亚群 plot_cnv(infercnv_obj, out_dir = "plots_A", cluster_by_group = TRUE, output_filename = "plot_A_by_annotation.pdf") # 场景B:没有注释分组,让inferCNV自动分10组 plot_cnv(infercnv_obj, out_dir = "plots_B", k_obs_groups = 10, output_filename = "plot_B_auto10.pdf")

注意out_dir目录必须存在,inferCNV不会自动创建多层目录。如果plot_cnv报目录不存在的错误,先dir.create("plots_A", recursive = TRUE)。

4. 实操复盘:从分析开始就把分组信息传递到底

4.1 从Seurat结果到inferCNV对象的构建

很多时候分组标签显示不出来,不是绘图阶段的问题,而是数据准备阶段就把分组信息弄丢了。我分享一下自己稳定的操作流程。假设在一个Seurat对象里已经完成了上皮细胞鉴定,Idents就是0-9号亚群:

# 取出要分析的上皮亚群 epi <- subset(seu, idents = c("0","1","2","3","4","5","6","7","8","9")) # 导出count矩阵,注意行名是基因,列名是barcode counts <- as.matrix(GetAssayData(epi, slot = "counts")) write.table(counts, file = "count_matrix.txt", sep = "\t", quote = FALSE) # 导出注释文件,两列,无表头 ann_df <- data.frame( barcode = colnames(epi), group = as.character(Idents(epi)), stringsAsFactors = FALSE ) # 按group排序,便于后续观察 ann_df <- ann_df[order(ann_df$group), ] write.table(ann_df, file = "annotations.txt", sep = "\t", row.names = FALSE, col.names = FALSE, quote = FALSE)

这个流程里最值得留意的是col.names = FALSE和quote = FALSE。前者去掉表头避免inferCNV把barcode那行当成数据,后者防止字符串被引号包裹导致匹配失败。这两处没注意,后面就有你折腾的。

还需要准备基因排序文件,格式是三列:基因名、染色体、起始位置。可以直接用注释好的GTF文件提取,也可以从inferCNV官方教程里下载现成的人类基因排序文件。这个文件的质量直接影响分析结果,但跟本文的分组标签问题关系不大,不再展开。

4.2 run()中的cluster_by_groups参数

有些人习惯把run()里的cluster_by_groups设成FALSE,理由是希望inferCNV完全从头对细胞做聚类,不依赖先验分组。这个思路在特定场景下没问题,但它有一个副作用:后续plot_cnv要想按注释分组画图,可用的分组结构就没有被完整保留。

run()阶段的cluster_by_groups=TRUE(注意这里是groups),意思是让inferCNV在计算过程中使用注释文件中的分组信息作为先验,对细胞进行有指导的聚类分析。对大多数肿瘤异质性研究来说,这个设置更合理,因为它避免了一部分细胞因CNV信号太弱被聚类到莫名其妙的位置。我建议除非你有明确理由,否则这个参数保持TRUE。

调用示例:

infercnv_obj <- infercnv::run( infercnv_obj, cutoff = 0.1, # 10X平台常用0.1,Smart-seq2建议1 out_dir = "infercnv_result", cluster_by_groups = TRUE, # 按注释分组参与聚类 denoise = TRUE, # 去噪 HMM = TRUE, # 跑HMM预测 num_threads = 4 )

如果跑完分析之后再回头检查,发现注释分组没有被正确使用,重跑的成本很高。所以建议在正式跑大型数据集之前,先用一个小子集测一遍流程,确认注释文件和参数都正常,再丢全量数据。

4.3 验证infercnv对象内部的分组索引

inferCNV跑完之后,对象内部其实已经存好了分组信息。可以通过检查对象槽位来确认你的0-9号分组有没有被正常读入。不同版本的槽位名可能略有差异,但一般可以通过slotNames(infercnv_obj)查看。

# 查看所有槽位 slotNames(infercnv_obj) # 查看观察组的分组索引 table(names(infercnv_obj@observation_grouped_cell_indices))

如果observation_grouped_cell_indices这个槽位里能看到0、1、2……9这些名字,并且每个名字对应一组细胞索引,说明分组信息在对象里是完整的,问题基本就出在绘图参数上。如果这个槽位只有一个observed或者只有一组索引,那说明注释文件读入就出了问题,得回到第2部分排查。

4.4 自定义热图方案:当默认出图满足不了你

plot_cnv的默认热图样式相对固定,有些时候你想把分组标签放得更明显、想加上别的临床注释条、想调整配色,这时候就得绕开默认绘图,自己从infercnv对象里提取数据来画。

inferCNV经过分析后,归一化好的CNV矩阵存在@expr.data里。可以直接拿这个矩阵配合你之前的注释信息做自定义热图。先看一个pheatmap的简单版本:

library(pheatmap) cnv_mtx <- infercnv_obj@expr.data anno_col <- data.frame( cluster = ann_df$group[match(colnames(cnv_mtx), ann_df$barcode)] ) rownames(anno_col) <- colnames(cnv_mtx) pheatmap(cnv_mtx, annotation_col = anno_col, show_colnames = FALSE, show_rownames = FALSE, cluster_cols = TRUE, color = colorRampPalette(c("navy", "white", "firebrick3"))(100), breaks = seq(0.5, 1.5, length.out = 101), cutree_cols = 10)

pheatmap的好处是简单、几行代码就能出图,注释条会直接显示0-9的因子水平。缺点是聚类树的样式比较朴素,对超大矩阵的性能也一般。

如果追求更好的排版和更灵活的注释,推荐ComplexHeatmap:

library(ComplexHeatmap) library(circlize) # 给每个亚群分配颜色 cluster_colors <- c( "0" = "#E41A1C", "1" = "#377EB8", "2" = "#4DAF4A", "3" = "#984EA3", "4" = "#FF7F00", "5" = "#FFFF33", "6" = "#A65628", "7" = "#F781BF", "8" = "#999999", "9" = "#66C2A5" ) col_ann <- HeatmapAnnotation( cluster = anno_col$cluster, col = list(cluster = cluster_colors) ) Heatmap(cnv_mtx, top_annotation = col_ann, column_split = anno_col$cluster, # 强制按亚群分块 cluster_columns = TRUE, # 仍显示聚类树 show_row_names = FALSE, show_column_names = FALSE, col = colorRamp2(c(0.5, 1, 1.5), c("blue", "white", "red")), name = "CNV")

column_split = anno_col$cluster这一行是点睛之笔,它会在聚类树上直接按你定义的0-9号亚群把细胞切开。这样即使默认热图的标签显示有问题,你自己画出来的图也一定会清晰地标出每个亚群。这种方法对最终发表论文特别友好,因为你可以完全控制颜色、字体、布局。

5. 常见问题速查表与避坑心得

5.1 常见问题速查表

现象可能原因解决办法
热图只显示observed,没有0-9plot_cnv未传cluster_by_group或k_obs_groups显式设置cluster_by_group=TRUE或k_obs_groups=10
注释文件里就只有observed导出注释时丢了亚群标签回到Seurat重新导出含0-9分组的注释文件
barcode对不上,分组错乱Excel打开过注释文件,-1被转成日期重新生成注释,用read.table以character读入检查
run()里cluster_by_groups=FALSE分组信息未被使用改为TRUE重新运行
有标签但跟期望的0-9对不上k_obs_groups是inferCNV自动聚类结果用cluster_by_group=TRUE按自己的注释展示
图例有0-9但热图注释条颜色杂乱细胞barcode排列与注释顺序不一致用match()按矩阵列名重排注释,不要直接cbind
自定义热图报错:行列名不匹配anno_col的行名和矩阵列名不一致用match(colnames(cnv_mtx), ann_df$barcode)索引

5.2 几个实践中的细节技巧

第一个细节是导表达矩阵时不要用scale过的数据。inferCNV分析的是原始count矩阵,内部会自己做归一化和log化。如果你把Seurat的@scale.data导进去,结果会非常奇怪,热图的分组结构也会被破坏。

第二个细节是在跑大型数据集之前先做小规模测试。取每个亚群几十个细胞先跑一遍流程,确认注释匹配、参数设置、绘图输出都没有问题。否则几十万个细胞丢进去跑几个小时,最后发现只是注释文件少了个分组,那个挫败感我太熟悉了。

第三个细节是输出PDF而非PNG。默认的PNG在细胞数多的时候注释条和标签几乎是糊的,PDF是矢量图,可以无限放大,想看哪个亚群就放大看哪个。如果你在服务器上用无界面模式跑,记得把output_filename里的后缀写成pdf,或者单独再存一份PDF。

第四个细节,也可能是最容易被忽略的一点:如果你把cluster_by_group=TRUE打开了,但注释文件里分组名的顺序是乱的,inferCNV会按factor的水平排序显示标签。想控制标签顺序,可以在R里先把group列转成factor,指定levels为0到9。这样dendrogram下方的标签顺序就完全可控了。

第五个细节是内存。自定义热图时如果细胞数太多,pheatmap和ComplexHeatmap都会非常吃内存。一个几万细胞的热图,单是聚类就可能让笔记本风扇狂转。遇到这种情况,可以先按亚群做细胞抽样,或者把相同亚群内的细胞合并成伪细胞(pseudo-cell)再画图,视觉上既清爽又不失代表性。

这些坑我基本都在真实项目里踩过。从“只有observed一个标签”到最终能清楚看到0-9号亚群各占一席,差的往往不是复杂的代码,而是对inferCNV注释传递逻辑的准确理解。先检查注释文件,再确认参数,最后再用自定义绘图兜底,这套流程走完,基本能覆盖绝大多数情况。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询