第4章 数据预处理:质量控制

空间转录组数据拿到手,第一步不是跑分析,而是先做质控。我早年处理单细胞数据时,就吃过没好好过滤的亏——线粒体比例异常高的那群细胞,硬生生把聚类结果带偏了一个亚群。后来学乖了,每个项目上来先把质控流程跑一遍。

这一章,我们就聚焦三个关键指标:基因数、线粒体比例、spatial背景。然后基于这些指标,把不合格的细胞和基因筛掉。

4.1 三个核心质控指标

说白了,每个捕获点(spot)都是一个“细胞”级别的数据。我们需要判断它到底是个活细胞,还是空斑、死细胞或者背景噪声。

指标 含义 常见阈值范围
基因数(n_genes_by_counts) 一个spot中检测到的基因种类数 稀疏组织:200-300;密集组织:500-800
UMI总数(total_counts) 一个spot中所有基因的表达量总和 通常与基因数正相关,筛选低质量spots时看下限
线粒体比例(pct_counts_mt) 线粒体基因表达量占总表达量的百分比 上限5%-20%,过高提示细胞受损或凋亡
Spatial背景(doublet / 空斑) 捕获点下方没有细胞或只有细胞碎片 依赖组织形态或表达总量<100 UMI

嗯,这里要注意:基因数和UMI总数高度相关,但侧重点不同。基因数反映多样性,UMI总数反映表达丰度。线粒体比例是区分活细胞和死细胞的金标准,我见过一些样本线粒体比例高达40%,这种数据强行往下做,特征基因基本都被线粒体基因占据,毫无生物学意义。

4.2 判断spatial背景的几种方式

空间转录组比单细胞多了一个维度——组织切片的物理位置。背景斑点(background spots)往往在组织边缘或无细胞区域,它们的特点是:

  • 总UMI极低(< 100),与组织内正常spots有明显断崖。
  • 所有基因的表达量都很均匀且极低,没有明显的细胞类型特征。
  • 在H&E染色图像上对应空白区域(如果提供了图像)。

我习惯先用一个简单粗暴的规则:总UMI < 200 且 线粒体比例 < 5% 的spot,基本可以定义为背景。为什么这么判?因为死细胞线粒体比例高,背景斑点既无细胞结构,也无线粒体泄露信号。

一点个人经验:不要盲目套用单细胞质控的阈值。空间转录组每个spot覆盖多个细胞,基因数和UMI总量通常比单细胞高一个数量级。我曾经按单细胞的标准过滤,结果把一半的有效spots都给筛掉了。

4.3 实战:用Scanpy计算质控指标

下面是我常用的代码片段,基于Scanpy的sc.pp.calculate_qc_metrics函数。它会自动计算每个基因、每个细胞的统计量,非常方便。

import scanpy as sc

# 加载已经预处理好的AnnData对象(假设为adata)
# 首先保证线粒体基因列表存在
adata.var['mt'] = adata.var_names.str.startswith('MT-')

# 计算质控指标
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], percent_top=None, log1p=False, inplace=True)

# 查看结果列名
print(adata.obs.columns.tolist())
# 输出类似:
# ['n_genes_by_counts', 'total_counts', 'total_counts_mt', 'pct_counts_mt']

得到了每个spot的三个关键指标后,我们就可以可视化分布,决定过滤阈值。

# 绘制小提琴图
sc.pl.violin(adata, 'n_genes_by_counts')
sc.pl.violin(adata, 'total_counts')
sc.pl.violin(adata, 'pct_counts_mt')

# 画出基因数-线粒体比例散点图
sc.pl.scatter(adata, x='total_counts', y='n_genes_by_counts')
sc.pl.scatter(adata, x='total_counts', y='pct_counts_mt')

看着这些图,我会重点观察三点:

  • 是否有明显的低值“尾巴”——那些总UMI小于200、基因数小于100的点。
  • 线粒体比例是否有超过10%的异常点群。
  • 散点图中,总UMI与基因数是否呈线性关系,偏离太远的可能是死细胞或空斑。

4.4 过滤细胞与基因

手动看阈值后,用sc.pp.filter_cells和sc.pp.filter_genes做自动过滤,但我更推荐用布尔索引手动筛选,更可控:

# 设置阈值(需根据具体数据调整)
min_genes = 200
max_genes = 5000  # 排除极稀有或可能的doublets
min_umis = 300
max_pct_mt = 20

# 过滤细胞
cell_mask = (
    (adata.obs['n_genes_by_counts'] >= min_genes) &
    (adata.obs['n_genes_by_counts'] <= max_genes) &
    (adata.obs['total_counts'] >= min_umis) &
    (adata.obs['pct_counts_mt'] <= max_pct_mt)
)
adata_filtered = adata[cell_mask].copy()

# 过滤基因:至少在3个细胞中表达
sc.pp.filter_genes(adata_filtered, min_cells=3)

print(f"过滤前细胞数: {adata.n_obs},基因数: {adata.n_vars}")
print(f"过滤后细胞数: {adata_filtered.n_obs},基因数: {adata_filtered.n_vars}")

注意:我曾经遇到过一批数据,线粒体比例的整体基线偏高(8%),因为组织取材时缺氧时间较长。这时如果硬套15%的阈值,会筛掉很多正常的高质量spots。正确的做法是:观察pct_counts_mt的分布,取中位数 + 2×标准差作为边界,或者人工看具体组织类型的先验知识。

过滤完细胞后,基因也经常需要过一遍。那些只在一两个细胞中表达的基因,对后续降维和聚类贡献极小,反而增加计算噪声。我一般设置min_cells=3,如果数据集特别大(几万个细胞),可以提到5或10。

4.5 避坑指南

  • 不要在过滤前归一化或对数化——质控必须在原始counts上进行。
  • 别忘了检查总UMI的上限:有些样本可能存在技术性双重捕获(两个组织片落在同一spot),表现为总UMI异常高,需要cut掉顶部1%的点。
  • spatial背景有时需要结合组织学图像一起看。如果图像与表达数据对应,可以标记组织外的spot直接移除。
  • 记录过滤日志:我习惯把每个过滤步骤的细胞数变化保存下来,方便回溯和论文撰写。

我的小技巧:在过滤前先画一张UMI vs 基因数的散点图,用颜色标出线粒体比例。这样一眼就能看到哪些点既低表达又高线粒体——这些就是典型的死细胞或空斑,可以直接用多边形选框手动剔除。

说到底,质控没有绝对的标准答案。你的阈值取决于组织类型、测序深度、实验批次。我建议你把这个流程写成函数,参数化阈值,然后一份数据跑几次,感受不同阈值对后续分析的影响。当你看到过滤前后的UMAP聚类变化时,就明白这一步有多关键了。

空间转录组质量控制流程 原始 AnnData 基因数 (n_genes_by_counts) 线粒体比例 (pct_counts_mt) Spatial背景 (总UMI极低) 设置过滤阈值 基因数 ≥ 200,线粒体 ≤ 20%,UMI ≥ 300 丢弃不合格spots 保留高质量spots 过滤后的 AnnData 使用 sc.pp.calculate_qc_metrics 实现

质控这一步做完,你的数据就干净了很多。接下来再去归一化、找高变基因、降维聚类,得到的生物学结论才真正可信。花在这上面的时间,未来会以十倍的分析效率回报你。