数据预处理:质量控制、过滤、归一化
好,咱们进入第3章。前面聊了怎么读数据、看数据结构,现在要动真格的了——对原始数据进行预处理。
我个人认为,这步是整个流程里最容易被低估的环节。很多同学一上来就跑什么批次校正、聚类,结果出来的图乱七八糟,回头一查,原来是原始数据里混了太多垃圾。听我一句劝,预处理做扎实了,后面80%的问题都不会出现。
3.1 数据概览:先摸清家底
拿到一个空间转录组数据集,别急着上脚本。先问问自己几个问题:
- 这是哪个平台的数据?10x Visium、Slide-seq还是MERFISH?
- 每个spot覆盖多少个细胞?
- 测序深度大概多少?UMI数分布怎么样?
我不建议大家直接写代码,先用手头的探索性工具看一看全局。比如用Seurat或者Scanpy快速画几个分布图。
我常用的几个快速检查指标:
- 每个spot的总count数(UMI数)
- 每个spot检测到的基因数
- 线粒体基因占比
- 空spot比例(如果有H&E染色辅助判断)
如果你发现某个样本的UMI数中位数只有几百,另一个样本有上万,那后面必须做标准化,不然所有比较都白搭。
3.2 质量控制:把脏东西筛出去
质量控制说白了就是两个维度:一个是spot级别的QC,一个是基因级别的QC。但空间转录组有个特殊之处——你没法像单细胞那样直接看细胞活性,但你可以看spot的空间位置。
Spot级别的QC指标:
注意:空间转录组的QC阈值不能照搬单细胞。比如单细胞里线粒体比例超过20%的细胞通常扔掉,但空间转录组的spot可能混了好几类细胞,线粒体比例高不一定代表该spot质量差。我在项目里遇到过一例,某个组织区域的spot线粒体比例普遍偏高,后来切片染色确认那片区域就是代谢活跃区,不是坏点。
我建议的QC流程:
- 总UMI数过滤:扔掉UMI数低于某个阈值的spot。阈值怎么定?看UMI分布曲线的拐点。一般在100-500之间。
- 检测基因数过滤:检测到基因数太少的spot,可能是组织边缘或者空位。阈值通常在200-300。
- 线粒体比例过滤:空间转录组我一般放到30%-50%,看具体组织类型。脑组织线粒体比例通常偏高,肝脏也是。
- 空间异常点过滤:这点很多教程不提。有些spot虽然UMI数正常,但位置在组织边缘或者组织空洞里。如果有组织学图像,一定要结合来看。
# 伪代码示例:Seurat风格的QC过滤
# 假设你已经有了Seurat对象
spatial_obj[["percent.mt"]] <- PercentageFeatureSet(spatial_obj, pattern = "^MT-")
# 过滤条件
spatial_obj <- subset(spatial_obj,
subset = nCount_Spatial > 500 &
nFeature_Spatial > 200 &
percent.mt < 30)
等等,这里有个坑。上面这个过滤是全局一刀切,但空间转录组数据经常有批次差异。我遇到过两个批次的样本,UMI数分布差了两倍——一个批次中位数800,另一个中位数2000。这种时候一刀切就不合适了。
我的做法:对每个样本单独做QC,各自看拐点、定阈值。别偷懒,每个样本跑一遍分布图,花不了几分钟。
3.3 过滤:删除低表达基因
基因级别的过滤比单细胞简单。核心原则就一条:在整个数据集中表达量极低、检测率低的基因,删掉。
为什么要删?
- 减少计算量,你想想看几万个基因里90%都是0,算起来浪费资源
- 去除噪声,低表达基因容易被技术噪声放大
- 提高下游分析稳定性,比如归一化、PCA都对稀疏数据敏感
常见过滤策略:
| 策略 | 说明 |
|---|---|
| 最小表达spot数 | 基因至少在N个spot中有表达(count > 0) |
| 最小总UMI数 | 基因在所有spot中的总表达量高于阈值 |
| 最大表达比例 | 删除在太多spot中都表达的基因(往往是housekeeping基因,没什么信息量) |
避坑提醒:过滤不要过滤得太狠。我曾经在某个项目中把最小表达spot数设为50,结果把一批稀有细胞类型的marker基因全干掉了,后来重新做才找到问题。根据数据规模来:如果总共就1000个spot,设5-10就够;如果是1万个spot,可以设到50-100。
# 过滤基因:至少在3个spot中有表达
# Scanpy风格
import scanpy as sc
adata.var["n_cells"] = np.sum(adata.X > 0, axis=0).A1
adata = adata[:, adata.var["n_cells"] >= 3]
# 或者用Seurat
# spatial_obj <- spatial_obj[rowSums(spatial_obj@assays$Spatial@counts > 0) >= 3, ]
3.4 归一化:让数据可比
归一化——这步我估计是争议最多的。
常规单细胞转录组里,我们通常用LogNormalize、SCTransform或者scran的pooling方法。空间转录组能不能直接用?能,但要小心。
空间转录组归一化的特殊之处:
- 每个spot的测序深度差异更大,因为spot覆盖的细胞数不均匀
- 空间邻居之间有生物学相关性,普通归一化可能放大局部噪声
- 有些归一化方法(比如SCTransform)会过度拟合某些批次效应
我常用的三种归一化方法对比:
| 方法 | 适用场景 | 优缺点 |
|---|---|---|
| LogNormalize | 快速探索、初步分析 | 简单、速度快;但无法处理深度差异极大的情况 |
| SCTransform | 需要更准确的方差估计时 | 效果好、保留更多生物变异;但计算量大、会过度校正某些批次 |
| scran pooling | 需要稳健的size factor估计 | 受异常值影响小;但空间数据不一定满足pooling假设 |
我个人经验:最开始做空间转录组时,我习惯LogNormalize打底,后面如果需要精细分析再换SCTransform。但如果你面对的批次效应特别严重(比如不同医院、不同试剂批次),建议优先用SCTransform,它对技术噪声的建模更鲁棒。
# 常见的归一化代码
# Seurat风格
spatial_obj <- NormalizeData(spatial_obj,
normalization.method = "LogNormalize",
scale.factor = 10000)
# SCTransform
spatial_obj <- SCTransform(spatial_obj,
assay = "Spatial",
variable.features.n = 3000)
Tip:归一化之后记得检查一下。我习惯画一个violin plot,看每个样本的归一化后表达量分布是否一致。如果某个样本分布明显左偏或者右偏,说明归一化参数可能不合适,需要调整scale factor或者试试其他方法。
这里我想专门提一下 SCTransform 的坑。它默认会做正则化负二项回归,把测序深度作为协变量回归掉。这个逻辑在单细胞里没问题,但在空间转录组里,测序深度和spot内细胞数正相关,而细胞数又和某些生物学特征(比如细胞密度、组织区域)相关。你一旦回归掉深度,可能连带着把生物学信号也去掉了。
怎么预防?我一般会同时跑一个LogNormalize版本和SCTransform版本,对比关键基因的表达模式。如果发现某个已知的区域marker基因在SCTransform版本里消失了,那可能就是过度校正了。
3.5 本章知识结构
下面这张图总结了本章的数据预处理流程,从原始数据到标准化数据之间的三步走:
你看,这个流程其实挺线性的。但实际项目里我经常要来回调整——比如过滤完之后发现某个基因没了,回头改QC阈值再跑一次。别怕重复,数据预处理就是这样。
3.6 避坑总结
嗯,最后总结几个我踩过的坑:
- 别用单细胞的QC阈值套空间数据。空间spot的线粒体比例普遍更高、UMI数分布更宽。强行套用会导致大量spot被误删。
- 过滤基因要留余地。有些基因在数据里表达很低,但可能就是你要找的稀有细胞marker。宁可多留几个让后面的方法去筛选,不要一刀切砍光。
- 归一化后要验证。我习惯对每个样本画一个归一化后的UMI分布图,如果某个样本明显偏离,说明scale factor或者方法选错了。
- SCTransform不是万能的。它会回归掉测序深度,而空间数据的深度和细胞数是关联的。深度差异本身可能就是生物学信号。用的时候多留个心眼。
最后一个小建议:把预处理脚本写成可重复执行的pipeline,每次改参数后能一键重跑。我自己用Snakemake管理,每次改QC阈值、过滤条件或者归一化方法都很方便。这样在探索阶段可以快速迭代不同的参数组合。