3. 批次校正的插入位置与策略归纳
讲完了原理和工具,咱们该聊点实操了。你想想看,拿到一堆单细胞数据,预处理也做了,归一化也跑了,到底在哪一步插入批次校正比较合适?
这个问题,我一开始也纠结过。不同的插入位置,直接影响你后续分析的结论。选错了,要么生物信号被抹掉,要么校正了个寂寞。这里头门道不少。
3.1 批次校正的“最佳插入点”
单细胞数据分析流程里,批次校正不是个孤立步骤。它得跟其他模块配合好。我个人习惯把它放在数据预处理之后、降维聚类之前。为什么?
- 太早(原始数据层面): 没经过质量控制(QC)和归一化的数据,噪声太大。校正算法容易把技术差异和生物学差异混在一起,结果一团糟。
- 太晚(聚类之后): 每个批次都单独跑一遍聚类,然后硬把结果合并。这属于事后补救。不同批次间细胞群的对应关系很难定义,而且校正后细胞类型可能完全跑偏。
我的经验法则:
- Seurat(CCA/MNN 系列): 在
NormalizeData找完高变基因后,立刻做FindIntegrationAnchors+IntegrateData。然后基于校正后的表达矩阵继续降维聚类。 - Scanpy(Harmony/BBKNN 系列): 在
sc.pp.normalize_total和sc.pp.log1p之后,直接在 PCA 嵌入空间里跑sc.external.pp.harmony_integrate。校正后的主成分再拿去算邻居图。
说白了,批次校正的黄金窗口就是:数据已经洗干净、归一化了,但还没被强行“分团”的时候。
3.2 两张主策略:基于“锚点” vs 基于“软聚类”
目前主流的批次校正方法,我把它粗分成两大流派。别管名字多花哨,底层逻辑就这俩。
| 策略流派 | 代表方法 | 核心思路 | 适用场景 |
|---|---|---|---|
| 锚点匹配型 | Seurat CCA+MNN | 找跨批次的结构保守单元格做“锚点”,用它们构建变换矩阵,把其他细胞拉过去 | 批次间细胞类型组成差异大,想保留稀有群体 |
| 软聚类/空间型 | Harmony, BBKNN | 在低维空间里做软聚类,然后迭代调整细胞的位置,让不同批次的同类细胞尽可能靠近 | 批次效应比较均匀,且数据量极大(10万+细胞) |
我记得有一次,用 Seurat 的基于锚点方法处理一批 PBMC 数据,效果特别好。但换到另一个脑组织的项目,同样的参数死活跑不动。后来换成 Harmony,几秒钟就收敛了。所以,没有银弹。得根据数据量和生物学复杂度来选。
3.3 时序图:到底谁先谁后?
为了让你看明白,我手绘了一张流程决策图。它概括了插入位置和策略的对应关系。
3.4 避坑:插入位置不当的后果
我曾经接手过一个同事的 project。他把批次校正放到了 FindNeighbors 之后。结果校正完一看,UMAP 图漂亮得很,所有批次完全重合了。但再看 marker 基因——传统 T 细胞 marker 跟 NK 细胞的 marker 混在一个簇里。完全失去了生物学意义。
高风险操作:
- 在差异表达分析之后做校正: 这属于因果倒置。先看到差异,再硬套校正来消除,100% 会引入假阴性,丢失真正的生物差异。
- 对未归一化的计数矩阵直接跑批次校正: 大部分校正方法(尤其是基于线性模型的)假设数据已经过 log 变换或 SCTransform。直接扔 raw counts 进去,算法会被文库大小差异带偏。
嗯,这里要注意:你怎么判断校正效果好不好?别光看 UMAP 图。一定要看校正前后的 PCA 散点图上批次来源是否能有效混合,同时保留原细胞类型的分离趋势。
3.5 协议对照:Seurat 与 Scanpy 的“校正流水线”
最后,放一个我在两个框架中常用代码段的对比。这样你看着更直观。
Seurat 标准协议(锚点集成):
# 数据已经经过 NormalizeData, FindVariableFeatures
# split 成 list
seu_list <- SplitObject(seu_obj, split.by = "orig.ident")
anchors <- FindIntegrationAnchors(object.list = seu_list,
dims = 1:30)
seu_integrated <- IntegrateData(anchorset = anchors,
dims = 1:30)
# 后续使用 seu_integrated 降维
DefaultAssay(seu_integrated) <- "integrated"
seu_integrated <- ScaleData(seu_integrated)
seu_integrated <- RunPCA(seu_integrated)
Scanpy 标准协议(Harmony 集成):
import scanpy as sc
import scanpy.external as sce
# 数据已归一化、log 化、找到高变基因
sc.pp.pca(adata, n_comps=30)
# 直接在 PCA 嵌入上跑 Harmony
sce.pp.harmony_integrate(adata,
key='batch',
n_iter=10,
adjusted_basis='X_pca')
# 校正后的 PCA 存在 adata.obsm['X_pca_harmony']
# 后续无需集成,直接算邻居、聚类
sc.pp.neighbors(adata,
use_rep='X_pca_harmony')
sc.tl.umap(adata)
你想想看,Seurat 那套流程跑完,会返回一个新的校正后的表达矩阵(Assay “integrated”)。而 Scanpy 用 Harmony,因为校正发生在嵌入空间,它并没有生成新的表达矩阵。这是两种思路在数据结构层面的直接体现。没有谁绝对好,关键看下游分析你要做什么。例如你需要校正后的全基因表达来做差异分析,那 Seurat 的集成方法更顺手。