第3章:常见批次效应校正方法简介:核心思想比较
说到批次效应校正,我们不是非要搞懂每种方法的数学细节才能用好。但我个人觉得,理解它们的核心思想、适用场景和代价,比调参数重要得多。这就像选工具——你总得知道锤子和螺丝刀分别干嘛用的,对吧?
目前最常用的五类方法是:Harmony、Seurat(CCA/MNN)、Scanorama、BBKNN 和 scVI。它们背后藏着完全不同的哲学。我挨个拆开说。
全局对齐 vs 局部匹配 vs 深度生成
先把这五种方法按亲缘关系分个类。我画过一张表给组里新人看,这次换成一张流程图,更直观。
这张图能帮你快速定位:当你面对不同数据规模时,先选阵营,再挑具体方法。
Harmony:软聚类 + 迭代调和
Harmony 的核心思想,说白了就是:让每个批次里相似的细胞主动向彼此靠拢。它不像过去那些硬切一刀的方法,而是用软聚类——每个细胞可以同时属于多个“细胞社区”,然后一次次微调位置。
关键思路:Harmony 把批次当协变量,在 PCA 降维后的空间里反复迭代,直到不同批次的细胞在低维空间里混在一起。它不需要原始表达矩阵,只靠降维后的 embedding 和批次标签。
我印象里特别深的一次,是在一个 8 批次胰腺数据集上跑 Harmony。其他方法或多或少会抹掉某些稀有细胞类型,但 Harmony 保留得相当完整。代价嘛…就是迭代次数不能太少,我习惯设 20 轮以上。
核心参数就两个:theta 控制校正强度,lambda 控制正则化力度。说实话,大部分情况用默认值就够了。
# Harmony 示例调用(Python)
import harmonypy
harmony_out = harmonypy.run_harmony(
data_pca, meta_data, vars_use="batch",
max_iter_harmony=20, theta=[2.0], lambda_val=1.0
)
Seurat(CCA + MNN):典型相关 + 互近邻
Seurat 走的是另一条路。它先用 典型相关分析(CCA) 找到两个批次之间共享的“特征维度”,再用 互近邻(MNN) 在共享空间里配对锚点,最终通过锚点来完成整个校正。
你想想看,CCA 匹配的是基因表达模式的协方差结构,不是直接拼坐标。这让它天然对批次间的不同细胞比例不太敏感。但要注意——我没少被坑过——如果两个批次之间几乎没有共享的细胞状态,CCA 容易找到一些假的共享维度。
注意:Seurat 的 CCA 方法要求你预先指定整合的“维度数”,一般设 20-30。维度太少会欠整合,太多会混入噪声。
# Seurat V4/V5 整合调用(R 语言)
library(Seurat)
merged_obj
anchors <- FindIntegrationAnchors(
object.list = split(merged_obj, f = merged_obj$batch),
anchor.features = 2000,
dims = 1:30,
reduction = "cca"
)
integrated <- IntegrateData(anchorset = anchors, dims = 1:30)
Seurat 的优势在于生态丰富——找锚点、评分、降维一把抓。但我个人不建议对百万级细胞直接跑 FindIntegrationAnchors,内存会爆。我通常先用 SelectIntegrationFeatures 把特征压到 2000 以内。
Scanorama:全景拼接,局部光滑
Scanorama 的名字来自“panorama”(全景图)——它想把不同批次的细胞全景拼接起来。其原理是:用局部曲面匹配来找到跨批次的相似细胞,然后一个批次一个批次地叠加拼接。
它不像 Harmony 那样一次把所有批次扔进去,而是两两配对,然后用拼接后的图做一次全局校正。这种做法有个好处:对批次间异质性容忍度比较高。我曾经用 Scanorama 处理过一个小鼠脑的数据——两批次中间隔了半年做实验,批次效应非常强——结果呢?它比其他方法保留了更多的皮层区域结构。
但它的速度是个硬伤。假设你有 10 个批次,两两配对就是 45 对,每对都要做一次局部匹配。细胞数量上升到几十万的速度就比较慢了。
import scanorama
import scanpy as sc
# adatas 是每个批次单独 scanpy AnnData 对象
corrected = scanorama.correct_scanpy(
adatas, return_dimred=True,
sigma=150, alpha=0.1
)
# corrected 返回的 list 中包含校正后的低维表示
BBKNN:快速 K 近邻图构造
BBKNN 走的是最极端的路线——它根本不改变 PCA 或 UMAP 的坐标值,而是直接在建模近邻图阶段,要求每个细胞只能连接其他批次的细胞作为近邻。你想想,这意味着什么?
它假设“同一个细胞类型的真实近邻一定在另一个批次里能找到”,所以强制跨批次建边。这样 UMAP 出来的图自然就混在一起了。优点是快得惊人——轻轻松松百万级细胞。缺点呢?如果某个批次里有一个独一无二的稀有亚群,它在别的批次里找不到真正的同类,就会被迫连接到不相关的细胞上。
我的建议:如果只做探索性可视化,BBKNN 是最省时间的选择。但如果要做严谨的下游分析(比如差异表达鉴定),我不推荐它。
import bbknn
import scanpy as sc
sc.pp.neighbors(adata, n_neighbors=30, use_rep="X_pca")
bbknn.bbknn(adata, batch_key="batch", neighbors_within_batch=3)
# 这里的 neighbors_within_batch 设得越小,跨批次连接越强
我个人习惯把 neighbors_within_batch 设在 3–5。设 0 就太极端了,会让同一个批次的细胞完全不连通,做出来的 UMAP 偶尔会断裂。
scVI:深度概率生成模型
scVI 把问题推到了另一个极端:用变分自编码器(VAE)学习一个低维潜空间,然后在这个潜空间里把批次效应当作条件变量,用一个深度神经网络去学数据生成过程。说白了,它学的是“如果去掉批次效应,数据本来是什么样”。
这是五种方法里唯一能把“不确定性”带出来的——VAE 输出的是一个分布,不是一个点。我在做纵向时间序列数据的时候特别喜欢 scVI,因为可以估算潜变量的置信区间。但千万别以为它是万能药。
scVI 的训练时间——以 GPU 小时计。我初次用它跑 50 万细胞,一张 24G 显存的卡,训练了差不多 4 小时。而且,它的性能对超参数超敏感:层数、隐单元数、学习率、KL 权重…稍有不慎就崩。
import scvi
import anndata
scvi.model.SCVI.setup_anndata(adata, batch_key="batch")
model = scvi.model.SCVI(adata, n_latent=30, n_hidden=128)
model.train(max_epochs=100)
latent_representation = model.get_latent_representation()
横向对比一览
| 方法 | 核心机制 | 适合场景 | 主要局限 |
|---|---|---|---|
| Harmony | 软聚类迭代校正 | 中等规模多批次(~10批次) | 需要合理 PCA 输入 |
| Seurat CCA | CCA + MNN 锚点 | 有共享结构的多批次 | 内存随着细胞数线性飙升 |
| Scanorama | 局部曲面匹配 + 拼接 | 强批次效应、少量批次 | 计算复杂度随批次数量指数级上涨 |
| BBKNN | 跨批次强制 KNN | 超大、快速探索分析 | 改变图结构,丢失批次内局部关系 |
| scVI | VAE 条件潜空间 | 倾向概率解读、纵向数据 | 训练耗时长、调参复杂 |
一个小复盘:在实战里,我从没只依赖一种方法。我通常先用 BBKNN 快速预览数据,看有没有严重的批次混杂,然后用 Harmony 或 Scanorama 做正式校正,最后用 scVI 验证关键结论的鲁棒性。三种工具互补,比选一个“万能”方法靠谱多了。