批次效应识别与校正:从PCA到统计方法

做RNA-seq分析的人,迟早会撞上批次效应。我刚开始那会儿,拿到一个肝癌vs正常组织的表达矩阵,PCA一画,咦?正常样本分成了两坨,肝癌也分成了两坨。后来一查才知道,正常样本来自两个不同测序批次,癌样本也是。说白了,就是技术差异把生物学信号淹没了。

批次效应不是实验设计的锅,而是不可避免的“技术噪音”。不同试剂批次、不同操作人、不同测序仪、不同时间点,都会引入系统偏差。如果不校正,机器学习模型学到的可能全是批次差异,根本不是生物学差异。

这一节,我带你走一遍三个常用方法:PCA可视化看问题、ComBat参数校正、limma线性模型移除。每个方法我都踩过坑,会给你讲明白怎么用、何时用、别犯什么错。

1. PCA可视化:先看见问题

先别急着套校正工具。我习惯第一步是画PCA图,看看样本到底怎么聚类的。用R里自带prcomp()就能搞定,加上ggplot2染色批次信息。

# R代码示例:PCA可视化探索批次效应
pca_res <- prcomp(t(log2(counts + 1)), center = TRUE, scale. = TRUE)
pca_df <- as.data.frame(pca_res$x)
pca_df$batch <- sample_info$batch
pca_df$group <- sample_info$condition

library(ggplot2)
ggplot(pca_df, aes(x = PC1, y = PC2, color = batch, shape = group)) +
  geom_point(size = 3) +
  theme_minimal() +
  labs(title = "PCA before batch correction")

正常情况,同一种生物学条件(比如“肿瘤” vs “正常”)的样本应该先聚在一起。如果同一个条件内的样本被批次颜色分开,那就是批次效应。我见过一个项目,PCA前两个主成分解释方差不到15%,但批次却完美地把样本分成两群。这时候,不校正直接扔给模型,准确率看着高,但换个批次全翻车。

小技巧:PCA之前记得做log2变换+中心化+缩放。直接用原始counts,高表达基因会压垮主成分方向。我一般先用DESeq2的vst或rlog变换,比log2(counts+1)更稳健。

2. ComBat:经验贝叶斯校正

ComBat是我在多个项目中用得最多的方法,对付2~5个批次特别好使。它假设每个基因的批次效应是“可加的”和“乘性的”,用经验贝叶斯估计每个批次的调整参数。优点是很省心——你只需要给出表达矩阵和批次标签。

我用的是sva包的ComBat_seq函数,它专门针对counts数据(负二项分布家族)。如果你已经做了log2变换,那就用ComBat。

# 使用ComBat_seq校正counts数据
library(sva)
batch <- sample_info$batch
# 假设有2个批次:batch1, batch2
adjusted_counts <- ComBat_seq(
  counts = counts,
  batch = batch,
  group = NULL,  # 如果有生物学分组可传入,会保留组间差异
  full_mod = FALSE
)

# 校正后PCA验证
pca_adj <- prcomp(t(log2(adjusted_counts + 1)), center = TRUE, scale. = TRUE)
pca_adj_df <- as.data.frame(pca_adj$x)
pca_adj_df$batch <- batch
ggplot(pca_adj_df, aes(x = PC1, y = PC2, color = batch)) + geom_point()

这里有个坑:ComBat的group参数特别关键。如果你传入生物学分组(比如肿瘤vs正常),它会计算组内平均效应,不把真实生物差异当成批次去校正。我之前有一次忘了设group,结果把肿瘤和正常之间的差异也给抹平了,后续差异基因一个都没找到。嗯,这个错我犯过一次就记得死死的。

警告:ComBat_seq假设每个批次内样本数量>=2。单样本批次无法估计方差,会直接报错。如果某个批次只有一个样本,我建议用limma的removeBatchEffect或者干脆合并相邻批次。

3. limma removeBatchEffect:线性模型取残差

limma包里有个removeBatchEffect函数,原理很简单:建一个线性模型,把批次效应当作协变量拟合,然后从表达量中减去批次效应的估计值。它不像ComBat那样做经验贝叶斯收缩,但计算快、灵活——你可以同时控制多个批次变量。

这个函数输入必须是log2变换后的表达矩阵(或者经过voom变换的)。用法如下:

# 使用limma的removeBatchEffect
library(limma)
# 假设有2个批次因子batch,以及生物学条件condition
design <- model.matrix(~ condition)
adjusted_expr <- removeBatchEffect(
  log2_expr,
  batch = batch,
  design = design
)

# 然后就可以用校正后的矩阵做PCA或机器学习

注意design参数是生物学分组的设计矩阵,它会保留你关心的差异。如果你不传design,函数默认所有样本属于同一组,那就会把生物学差异也一起“移除”——这基本是个灾难。我见过有人直接用removeBatchEffect(log2_expr, batch = batch),以为批次校正会自动尊重组别,结果一画PCA,肿瘤和正常混在一起了。所以要记住:design一定要有你要保留的分组变量。

关键对比:ComBat适合2~5个批次、样本量中等的情况;limma removeBatchEffect更适合连续批次(比如测序日期)、或者你想要线性模型更灵活控制协变量。我个人偏好:如果批次数量少(<6)且每个批次样本数>3,用ComBat_seq;如果批次数量多(>10)或者批次是连续变量(如年份),用limma。

知识体系总览

下面这张图把批次效应识别的三步走概括起来:先可视化确认问题,再选择校正策略,最后验证校正效果。我用SVG画了核心流程,节点用的是我实际项目中最常用的工具和步骤。

批次效应识别与校正流程 原始表达矩阵 log2(counts+1) 或 vst PCA可视化 prcomp + ggplot2 样本按批次聚类? 而非按生物学条件 无需校正 选择校正策略 ComBat_seq 经验贝叶斯 removeBatchEffect 线性模型残差 校正后PCA验证

校正后验证:别偷懒

无论用哪种方法,一定要再做一遍PCA(或者t-SNE/UMAP),看看批次是否被“混合”了。如果校正后批次仍然明显分开,说明方法不对或者参数没设好。我遇到过一种情况:ComBat校正后PCA上批次混得很均匀,但用KNN分类器预测批次标签,准确率还是高达95%——这说明批次效应并没有完全消除,只是在前两个主成分里看不出来了。所以,我建议用定量指标辅助判断:

指标说明理想值
PCA批次方差贡献批次主成分解释方差比例< 5%
Silhouette得分(基于批次)样本与同批次相似度 vs 不同批次接近0
KNN批次分类准确率用基因表达预测样本批次接近随机(1/k)

实际项目中,我通常会跑完校正后算一个“批次分类准确率”,如果低于50%(对于2个批次),我就觉得可接受。当然,如果它仍然很高,可能就需要回归到数据本身,看看是否有个别批次存在严重质量问题——比如测序深度异常、rRNA残留等,这些不是一个统计校正能解决的。

经验之谈:不要把希望完全寄托在校正方法上。最好的策略是在实验设计阶段就平衡批次和生物学条件(比如每个批次都有肿瘤和正常样本,而非一整个批次全是肿瘤)。如果批次与条件完全混杂(confounded),那任何统计方法都无法区分。这样的数据,我建议直接放弃该分析,重新设计实验更靠谱。