第三章 质量控制:把好细胞关
拿到单细胞数据的第一件事是什么?不是急着跑聚类,而是先做一次彻底的“体检”。
细胞不是完美的——有的死了,有的碎了,有的其实是两个细胞被混在一起当成一个测了。这批“问题细胞”要是混进下游分析,聚类、差异基因、甚至细胞类型注释都会跟着跑偏。我在早期项目里吃过这个亏,上来直接标准化,结果出来一堆奇怪的“亚群”,排查到最后才发现是坏死细胞在捣乱。
所以今天我们把质量控制的三个核心步骤讲透:
- 计算线粒体/核糖体基因比例
- 过滤低质量细胞
- 识别并移除双细胞
为什么线粒体/核糖体比例如此重要
先说线粒体基因。正常活细胞里线粒体基因占比一般不会太高,通常低于20%。如果比例飙到30%、40%,多半是细胞膜破裂了——线粒体里的mRNA漏出来,导致测到的UMI里线粒体read占比异常高。说白了,这些细胞已经“透心凉”,留着也没用。
核糖体基因则是另一回事。核糖体基因(RPS, RPL家族)在大多数细胞里高表达,但如果某类细胞核糖体比例异常低或异常高?我建议还是先打个问号。比例太低可能说明细胞状态不好,转录活性下降;太高又可能有双细胞的嫌疑。我个人习惯在QC里同时看这两个指标,避免一叶障目。
计算线粒体/核糖体比例:用PercentageFeatureSet
Seurat提供了两个很方便的函数:PercentageFeatureSet和AddMetaData。拿线粒体基因来说,所有以“MT-”开头的基因都是线粒体基因——人、小鼠都一样。核糖体基因用“RP[SL]”开头就能匹配到。
# 计算线粒体比例
sce[["percent.mt"]] <- PercentageFeatureSet(sce, pattern = "^MT-")
# 计算核糖体比例
sce[["percent.ribo"]] <- PercentageFeatureSet(sce, pattern = "^RP[SL]")
# 看一眼结果
head(sce[[]])
注意:这个pattern匹配的是基因名称,不是ID。如果你的基因名已经标准化成Symbol格式,直接用就行。我遇到过基因名全大写的物种(比如斑马鱼),当时还特意查了一下它们的命名规则,嗯,建议先调个rownames(sce)[1:10]确认一下。
过滤低质量细胞:阈值怎么定?
拿到percent.mt、nFeature_RNA和nCount_RNA以后,剩下的就是切一刀。但这一刀怎么切?没有绝对标准,取决于你的组织类型和建库质量。
| 指标 | 常见范围 | 说明 |
|---|---|---|
| nFeature_RNA | 200~2500 | 低于200可能是空滴或死细胞;高于2500可能是双细胞 |
| nCount_RNA | 视文库而定,一般500~50000 | 超高或超低都要警惕 |
| percent.mt | <20% | 组织类型差异大(心肌细胞可以放宽到25%) |
| percent.ribo | 通常10%~50% | 极端值提示异常细胞或双细胞 |
我曾经处理过一个肝脏样本,线粒体比例总体偏高,一度怀疑是建库出了问题。后来查文献才知道,肝细胞线粒体本来就多,正常范围可以到25%。所以最好先画个分布图——VlnPlot(sce, features = "percent.mt"),看看数据的真实形状再定阈值。
# 示例:保守过滤
sce <- subset(sce, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 &
percent.mt < 20)
如果你一次想滤掉核糖体比例异常的细胞,也可以加条件:& percent.ribo > 5 & percent.ribo < 50。不过我不建议把所有指标都卡得死死的,结合具体数据做判断更靠谱。
过滤双细胞:千万不能跳过的一步
双细胞(doublets)指的是两个细胞被包裹进同一个液滴或孔里。10X Genomics的标准drop-seq流程中,双细胞率大约1%~4%,样本上样浓度越高双细胞率越高。这些“合体细胞”会在下游分析里形成一群假阳性的中间态,你想想看,它既表达A细胞基因又表达B细胞基因,聚类出来就会卡在两个群之间。我的做法是,凡是需要做稀有亚群分析的,双细胞过滤必须做。
常用工具:DoubletFinder(R包)和scrublet(Python)。这里我用DoubletFinder举例,因为它直接集成Seurat对象。
# 安装(只需一次)
# install.packages("remotes")
# remotes::install_github("chris-mcginnis-ucf/DoubletFinder")
# 先做标准预处理和降维
sce <- NormalizeData(sce)
sce <- FindVariableFeatures(sce)
sce <- ScaleData(sce)
sce <- RunPCA(sce)
sce <- RunUMAP(sce, dims = 1:20)
# 寻找最优pK值(基于BCmvn)
sweep.res <- paramSweep(sce, PCs = 1:20)
sweep.stats <- summarizeSweep(sweep.res, GT = FALSE)
bcmvn <- find.pK(sweep.stats)
# 设置预期双细胞比例(根据10X官方指南)
nExp <- round(0.04 * ncol(sce)) # 假设4%
sce <- doubletFinder(sce, PCs = 1:20, pN = 0.25, pK = 0.005,
nExp = nExp, reuse.pANN = FALSE)
输出结果会多一列类似DF.classifications_0.25_0.005_XXX的元数据,包含"Singlet"和"Doublet"标签。直接按行过滤即可:
# 假设列名为 DF.classifications_0.25_0.005_430
sce <- sce[, sce[[]][["DF.classifications_0.25_0.005_430"]] == "Singlet"]
整体质量控制流程
把上面三步串起来,就是下面这张图:
我一般习惯在过滤低质量细胞之后就马上检查一下VlnPlot确认分布合理,再进行双细胞检测。这样做还能减少计算量,毕竟双细胞检测对异常值比较敏感。
小技巧:如果你用的是10X标准数据,推荐先去10X官网查一下预期双细胞率(根据回收细胞数)。也可以在log2转换后的count矩阵上先用scran::doubletCells快速评估,再决定要不要做DoubletFinder —— 有时候数据质量好,双细胞本身就很少,没必要多花时间。
嗯,质量控制这一章到这基本结束了。记住一个原则:宁可错杀一百,不可放过一个。多过滤几个细胞不会影响大局,但留下一个双细胞或死细胞可能会污染整个亚群。我在一个脑组织样本上就亲眼见过,只因为宽松了线粒体比例阈值,多出一个“假脑膜细胞群”,后来加严过滤后那个群就消失了。所以,别怕滤得狠,更别偷懒跳步。你的下游分析好不好,全看这一步了。