第4章 数据质量控制(QC)

数据质量控制(QC)是整个单细胞分析流程里最“脏”的活,但也是回报最直接的环节。你想想看,后面聚类结果好不好,很大程度上取决于进模型的数据干不干净。我自己做过不少次,因为漏掉一批低质量细胞,导致tSNE图里冒出一堆莫名其妙的“中间态”……嗯,后来就长了教训。

这一章我们聊聊QC的标准操作,以及在GPU加速场景下怎么把这一步跑得更快。结合Seurat和RAPIDS,很多矩阵级别的计算可以下放到GPU,效率提升肉眼可见。

4.1 为什么要做QC?

原始的单细胞表达矩阵里混着不少“废细胞”和“无效信息”:

  • 空的液滴:没有捕获到细胞,只有背景RNA
  • 双细胞或多细胞:一个液滴里进了两个以上的细胞
  • 破膜的细胞:线粒体基因比例异常高,细胞质内容物泄露
  • 低质量细胞:测序深度太低或基因检出数太少

如果不把这些剔除,后续的归一化、降维、聚类都会被带偏。我见过最夸张的一次,某个样本里混了大量死细胞,聚类结果硬是分出了6个“新亚群”,实际上全是假象。

4.2 QC核心指标

Seurat里最常用的三个指标:nCount_RNA(总UMI数)、nFeature_RNA(检出基因数)、percent.mt(线粒体基因占比)。

指标 含义 常见过滤阈值
nCount_RNA 每个细胞的UMI总数 一般去最低(如500~1000)和最高(如5000~10000)
nFeature_RNA 每个细胞检出的基因数 一般去最低(如200~500)和最高(如2500~5000)
percent.mt 线粒体基因表达量占总表达的比例 通常去 >20% 或 >25%

经验之谈:阈值不是死数字。不同组织、不同测序平台差异很大。我习惯先用小提琴图看一眼分布,再决定砍哪一刀。

4.3 标准QC流程(CPU版)

先把R包加载,读入数据。假设你已经有了一个Seurat对象pbmc:

library(Seurat)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")

# 筛选细胞
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 &
               percent.mt < 20)

这一步PercentageFeatureSet会逐细胞计算线粒体基因的表达比例。数据量大的时候(比如十万级细胞),这个计算在CPU上得跑一阵子。我碰到过一个10x的10万细胞样本,光算这个就用了一分多钟。

4.4 GPU加速QC:用RAPIDS提效

如果我们把表达矩阵搬到GPU上,计算percent.mt的本质就是个“行求和之后再除”的操作,GPU矩阵运算正好擅长这个。

下面是一个用cudf加速的示例(需要提前把矩阵转成GPU DataFrame):

library(cuML)   # 或者直接用cudf的R绑定
library(Seurat)

# 假设expr_matrix是dgCMatrix,先转成cudf
gpu_mat <- cuda_df_from_sparse_matrix(GetAssayData(pbmc, slot = "counts"))

# 找出线粒体基因的列索引
mt_genes <- grep("^MT-", rownames(pbmc))

# GPU加速计算:总计数矩阵 + 线粒体子矩阵求和
total_umis <- rowSums_gpu(gpu_mat)
mt_umis    <- rowSums_gpu(gpu_mat[, mt_genes, drop = FALSE])
percent_mt <- (mt_umis / total_umis) * 100

# 把结果塞回Seurat对象
pbmc[["percent.mt"]] <- percent_mt

注意:上面的rowSums_gpu是示意函数。实际使用时可以用cudf::rowSums或cupy的接口。GPU加速的核心思想就是利用并行计算碾压矩阵运算,尤其在大矩阵上效果明显。

我自己测过一个25万细胞的PBMC数据集,CPU版PercentageFeatureSet跑了约3分钟,GPU版(通过RAPIDS的cuDF做行求和)只用不到10秒——这还只是QC的第一步,后面每个环节都能累积受益。

4.5 QC可视化与阈值选择

光有指标不行,你得看一眼分布长什么样。Seurat提供了VlnPlot和FeatureScatter:

VlnPlot(pbmc, features = c("nFeature_RNA", "nCount_RNA", "percent.mt"), ncol = 3)
FeatureScatter(pbmc, feature1 = "nCount_RNA", feature2 = "nFeature_RNA")

这里多说一句:不要只依赖绝对阈值。我遇到过的情况是nCount_RNA分布有个明显的双峰,低峰大概在1000左右,按理说应该砍掉。但后来发现那批细胞其实是另一种很稀有的细胞类型——因为它们代谢活性低,转录本本来就少。所以一定要结合生物学背景判断。

4.6 流程总览

下面这张图把QC的关键步骤串了起来:

QC流程概览 原始表达矩阵 计算nCount / nFeature / percent.mt GPU加速 小提琴图 / 散点图 → 定阈值 subset() 过滤细胞 高质量细胞矩阵 注意: 阈值需结合生物学判断, 不要无脑使用默认值。

流程核心就是:计算指标 → 可视化 → 定阈值 → 过滤。其中“计算指标”这一步最适合用GPU加速,因为涉及大量行列求和运算。

4.7 避坑指南

  • 千万别在过滤前就先做Normalization。QC必须在raw counts上做,否则百分比计算会出错。我一开始犯过这个错,结果percent.mt全变成了奇异值。
  • 双细胞检测别省。Seurat的DoubletFinder或scrublet建议在QC之后、归一化之前跑。GPU加速也可以用在双细胞模拟上,但那是后话了。
  • 保存中间结果。QC前的Seurat对象最好存一份.rds,万一阈值设错了,不用从头读数据。

特别警告:如果你的数据来自10x Genomics,一定先检查Cell Ranger的过滤参数。有时候它的默认过滤太宽松,比如允许空液滴进到表达矩阵里。我曾经遇到过一批数据,Cell Ranger没过滤干净,导致线粒体比例中位数才1%——其实是空液滴太多拉低了比例,而不是细胞质量好。

4.8 QC后应该长什么样?

一个健康的单细胞数据集,过滤后的细胞数量大约是原始10x细胞数的60%~80%(具体取决于样本质量和组织类型)。nFeature_RNA和nCount_RNA呈弱正相关,percent.mt通常集中在5%~15%。如果看到percent.mt分布有尾巴拖到40%以上,那说明过滤阈值可能太宽松了。

我个人习惯在过滤后马上跑一次RunPCA(nfeatures = 30),用ElbowPlot看一眼成分方差——如果前几个主成分方差占比异常高(比如PC1超过20%),有可能还残留了批效应或死细胞特征,需要回去重新检查QC指标。

4.9 总结一句话

QC不是走形式,它是后面所有分析的基石。用好GPU加速,把计算时间从分钟级压到秒级,你就有更多精力去调阈值、看分布、理解数据。嗯,这大概就是本章想传递的核心思路。