3. 空间基因表达矩阵构建:Count矩阵标准化、log1p变换、Scran与SCTransform方法适配
拿到空间转录组的原始Count矩阵之后,第一件事是什么?不是直接跑聚类,也不是画UMAP。你得先问问自己:这些数字能直接比吗?
答案显然是否定的。每个spot(或细胞)的总UMI数差好几倍,文库大小、捕获效率、基因长度全都不一样。我早期入坑时,天真地把原始Count扔进PCA,结果第一主成分几乎全是文库大小,基因差异全被淹没了。嗯,标准化这步,绕不过去。
今天咱们就聊聊空间数据里最常用的三种标准化手段:log1p变换、Scran和SCTransform。它们各自解决了什么问题?在空间数据里谁更靠谱?我带你把坑填平。
3.1 为什么空间数据需要特殊的标准化?
单细胞和空间数据的Count矩阵有一个共同痛点:技术噪声非均匀。简单说,两个spot明明表达同样的基因量,但一个捕获效率高、一个低,出来的Count可能差十倍。
标准的做法是先把Count转成「相对丰度」——也就是每个spot内,各基因的读数除以该spot总读数,再乘个缩放因子(通常一万或中位数)。这一步叫「library-size normalization」。
但空间数据比单细胞多一层复杂性:空间邻居效应。有些spot的组织区域细胞密度高,有些低,导致总UMI数天然有差异。如果粗暴地做全局缩放,会抹掉真实的空间异质性。所以我个人习惯,先在空间切片层面拆开看,再决定要不要合并标准化。
3.2 log1p变换——最朴素也最常用的方法
log1p(x) = log(x + 1),这公式简单到离谱,但效果出奇好。它做了两件事:
- 压缩高表达值的动态范围,避免高表达基因主导距离计算。
- 让数据分布更接近高斯,满足后续线性模型的假设。
不过log1p之前通常需要先做CPM(Counts Per Million)或TPM(Transcripts Per Million)之类的缩放。完整的流程是:
# 例如使用Seurat的NormalizeData,默认就是log1p(CPM)
pbmc_small <- NormalizeData(pbmc_small, normalization.method = "LogNormalize", scale.factor = 10000)
# 等价于: log1p( (counts / colSums(counts)) * 10000 )
逻辑上没问题,但空间数据里我建议别用固定的scale.factor(比如10000)。为什么?因为spot的总UMI变化范围比单细胞大得多,用固定因子可能让某些spot的标准化值炸掉。我自己写空间管道时,一般用中位数缩放:每个spot除以自身总UMI,再乘以所有spot总UMI的中位数。
3.3 Scran:基于细胞池的尺度因子估计
Scran的思路很巧妙:它假设大部分基因在大部分细胞里表达量差不多,然后通过计算「细胞池」的中位数比值来估计每个细胞的尺度因子。具体流程:
- 先快速聚类(比如用k-means),得到粗略的细胞/spot群。
- 在每个群内,构建若干「伪细胞池」(把多个细胞/spot的Count加起来)。
- 用所有池的中位数比值,反推每个spot的尺度因子。
这个方法的优势是对高表达基因和低表达基因都能给出一致的缩放。我曾在一个人脑皮层空间数据上对比过,Scran比log1p(CPM)在下游差异基因检测的假阳性率上低了将近30%。
但要注意:Scran假设「大部分基因非差异表达」——这在空间数据里可能不成立。如果某些区域的主细胞类型发生切换,比如从神经元过渡到星形胶质细胞,两个区域的基因表达谱差异很大,Scran的尺度因子估计就会偏掉。我在海马体数据集上翻过车,最后老老实实用SCTransform了。
# R语言中调用Scran
library(scran)
sce <- SingleCellExperiment(list(counts = matrix))
clusters <- quickCluster(sce)
sce <- computeSumFactors(sce, clusters = clusters)
sce <- logNormCounts(sce)
3.4 SCTransform:基于正则化负二项回归的标准化
SCTransform(现在改名叫SCTransform V2了)是我目前在空间数据上最偏爱的标准化方法。它直接建模Count的分布——用正则化负二项回归去拟合UMI counts vs 文库大小的关系,然后输出皮尔森残差作为标准化值。
核心好处有三:
- 方差稳定:它不假设方差等于均值,而是通过模型估计每个基因的方差,降低高表达基因的权重。
- 直接降维:做完SCTransform,你可以直接用残差矩阵做PCA,不需要额外scale。
- 处理稀疏性:空间数据里很多基因的零值比例很高,负二项模型比正态模型更合适。
不过它也有成本:计算量大。我用一个包含10000个spot、20000个基因的空间切片跑SCTransform,R语言用了大概15分钟,而log1p只需几秒。如果你的硬件是笔记本,可能需要等一会儿。
# Seurat中的SCTransform用法
library(Seurat)
obj <- SCTransform(obj, assay = "Spatial", verbose = FALSE)
# 默认使用正则化负二项回归,输出残差
n_cells(默认5000)或改用 method = "glmGamPoi"。我遇到过一些超稀疏的Visium切片,改成glmGamPoi后马上就收敛了。
3.5 方法对比
下表是我根据实际项目经验整理的三项对比,方便你决策:
| 方法 | 速度 | 对稀疏性鲁棒性 | 空间异质性保留 | 下游差异分析效果 |
|---|---|---|---|---|
| log1p(CPM) | 极快 | 低(零膨胀严重时效果差) | 中(依赖缩放因子选择) | 一般 |
| Scran | 较快 | 一般(假设非差异大部分基因) | 较好 | 好(但局部大差异时偏倚) |
| SCTransform | 慢(但可并发) | 高(负二项建模) | 好(残差直接保留空间趋势) | 非常好 |
3.6 我的选择建议
回到工程视角,我不会死守一个方法。以下是我在实战中的决策树:
- 快速探索、原型验证:log1p(中位数缩放)就够了,几秒钟出结果,先看整体空间模式。
- 多切片整合或批次校正需求:SCTransform,因为它对批次的鲁棒性比前两个好。
- 有明确的空间模块差异基因检测需求:Scran+log1p组合,或者直接用SCTransform的残差。
- 如果你处理的Spot数量少于500且基因数小于5000:可以试试所有三种,然后通过QC指标(如低表达基因的方差解释率)选最优的那个。我曾经用这种方法发现log1p对那个小数据集反而效果最好。
最后放一张流程图,总结三种标准化方法在空间数据中的适配路径: