4. 标准化与归一化:文库大小标准化、LogNormalize、SCTransform、scran 方法对比
拿到表达矩阵以后,第一步不是直接往下跑降维或聚类。你为什么得先做标准化?说白了,每个 spot(或每个细胞)捕获到的 mRNA 总量不一样。有的 spot 测了 2 万 reads,有的只有 5 千,如果不处理,差异主要来自测序深度,而不是真实的生物学差异。
这一节我带你们过四种主流方法。我个人的习惯是 – 先看数据量,再挑方法。小数据用 LogNormalize 就够了,大规模或高噪音的,我倾向用 SCTransform。下面一个一个来捋。
4.1 文库大小标准化(CPM / CPM+1)
最朴素的做法:把每个 spot 的 counts 除以它的总文库大小,再乘以一百万(或者一万)。公式很简单:
CPM = (gene_counts / total_counts) * 1e6
有些工具会进一步加 1 再做 log,但我很少直接用原始 CPM 做下游。为什么?因为 CPM 放大了低表达基因的噪音。比如一个基因只有 1 个 count,在文库 1 万 vs 5 万的 spot 里,CPM 会差 5 倍,但那个 1 个 count 可能只是随机捕获的。
我的经验:做空间转录组时,如果只是快速预览基因表达分布,CPM+1 够用。但真要建模,别偷懒,至少用 LogNormalize。
4.2 LogNormalize(log(CPM+1))
这是 Seurat 里的默认方法。先算 CPM(或总分子数归一化),再取 log1p。公式是:
log1p_CPM = log( CPM + 1 )
加 1 避免 log(0),同时不改变零值。我刚开始做单细胞时也一直用这个。但后来发现一个问题:它只校正了文库大小,没管基因表达水平的方差。高表达基因的方差被压得太低,低表达基因的噪音反而被放大。说白了,它不处理异方差性。
注意:LogNormalize 假设所有基因的方差与均值关系一致,但实际数据里这假设不成立。如果你后续用 PCA 或 t-SNE,可能会看到一团无法解释的高表达基因驱动的主成分。
4.3 SCTransform(基于负二项回归)
SCTransform 是 Hafemeister & Satija 2019 提出的。它用正则化负二项回归来同时建模均值-方差关系。核心思路是:
- 为每个基因拟合一个负二项模型,用文库大小作为 offset。
- 对残差做皮尔森标准化(Pearson residuals),得到方差稳定化的表达值。
- 保留高变基因的同时,自然去除了技术噪音。
用 Seurat 调用超简单:
SeuratObject <- SCTransform(SeuratObject, verbose = FALSE)
SCTransform 返回的是残差值(单位方差),可以直接跑 PCA、聚类。我有个项目用了 Visium 的肠道切片数据,基因表达动态范围很大 – 从 0 到 3000 counts。LogNormalize 后 PCA 前几个维度被几个高表达基因(比如 ACTB、GAPDH)彻底主宰,看不清区域差异。换成 SCTransform 后主成分的生物学分离马上就出来了。嗯,这个坑我踩过。
优点:一次性搞定归一化 + 方差稳定 + 高变基因选择。不需要额外跑 FindVariableFeatures。
但注意:SCTransform 计算量大,尤其当你有几万个 spot 时。试试调 n_cells 参数(默认 5000)来加速,或者分 batch 跑。
4.4 scran(基于去卷积法)
scran 是 Lun 等人开发的 R 包,方法叫 “deconvolution-based size factor estimation”。它更适合 scRNA-seq,但也能用于空间数据。思路是:
- 将细胞/spot 分成很多小组(pool),每个 pool 内按总 counts 估算 size factor。
- 再通过线性解卷积反推出每个单个 spot 的 size factor。
- 最后用 size factor 做除法和 log 变换。
用法代码示范:
library(scran)
library(SingleCellExperiment)
sce <- SingleCellExperiment(list(counts = counts_matrix))
clusters <- quickCluster(sce)
sce <- computeSumFactors(sce, clusters = clusters)
sce <- logNormCounts(sce)
scran 在遇到细胞类型比例极度不均衡的数据时表现不错。但空间数据里 spot 之间往往没有明显的“群组”,所以我不太推荐直接用默认参数。我曾经尝试在一张 Visium 切片上跑 scran,结果 size factor 被平滑得像一片平地,基本没校正效果。后来我手动调了 min.mean 和 pos.counts 参数才勉强好用。如果你非要用,建议先做简单的预处理过滤,再试。
4.5 方法对比总览
| 方法 | 原理 | 校正内容 | 方差处理 | 计算量 | 推荐场景 |
|---|---|---|---|---|---|
| 文库大小标准化 | CPM / TPM | 文库深度 | 无 | 极低 | 快速查看, 或作为后续输入 |
| LogNormalize | log(CPM+1) | 文库深度 | 无 | 低 | 一般 scRNA-seq / 小规模数据 |
| SCTransform | 负二项回归 + 残差 | 文库深度 + 方差 | 有(稳定化) | 中-高 | Visium / 10x 数据, 高动态范围 |
| scran | 去卷积 size factor | 文库深度 | 无 | 中 | scRNA-seq 有明确聚类时, 空间需调参 |
这张表帮你快速选。个人经验是:做 Visium,优先跑 SCTransform,除非你时间很紧或者数据超级干净。LogNormalize 作为兜底。
4.6 方法选择流程图
我画了一张决策图,帮你理清思路:
这张图从左到右走:原始矩阵进来,先问数据规模大不大、噪音高不高。是的话直接走 SCTransform。否则问你想不想要方差稳定。如果否,就沦落到 CPM 或 scran 二选一了。我个人看到“否”的时候,会优先考虑 LogNormalize,而不是直接跳 CPM。
4.7 避坑指南
- 千万不要在标准化前直接 log(counts) – 零值变成 -Inf,下游全崩。我就是有一次手快没加 1,debug 了一下午。
- SCTransform 用了以后记得不要再跑 ScaleData – 它的残差已经是标准化过的,再 Scale 会扭曲方差。好多新手在这个地方重复归一化。
- scran 的 size factor 对稀疏矩阵很敏感 – 如果你的数据零太多(Visium 也常见),scran 会估算出负的 size factor。我当时遇到负数直接懵了,后来加了伪计数才搞定。
4.8 最终建议
标准化这一步决定了下游分析的成败。别急着跑流水线,先花 10 分钟确认你的数据特性。我现在的固定流程是:
- 先跑 SCTransform(如果计算资源够)。
- 不够的话,LogNormalize + FindVariableFeatures。
- 只有做快速验证时才用 CPM+1。
好,这部分就到这里。记住,没有万金油方法,但有了对比表格和流程图,你至少知道什么时候该用什么。