R语言批次效应建模与自动化评估 - 第4节

4.1 批次效应移除的线性模型思想

先聊一个基础问题:批次效应到底怎么“移掉”?你直接减去平均数?那肯定不行。我早年刚接触这个功能时,还真试过按批次算均值再扣掉——结果后续分析全乱了。

实际上,limma 的 removeBatchEffect 背后是个标准的线性模型。简单说就是:

Y = Xβ + 批次效应 + 误差

其中 Y 是基因表达量矩阵,X 是设计矩阵(包含你关心的生物学变量),批次效应 是批次协变量对应的系数的乘积。函数做的事是:先拟合这个模型,估计出批次项的系数,然后从原始表达量中减去批次项贡献的值,只留下 Xβ + 误差。

我习惯把这种操作理解为“在控制生物学差异的前提下,把批次带来的偏移拉平”。说白了,它不是简单地“去噪”,而是用线性回归把可解释的批次分量揪出来并减去。

从设计矩阵到批次因子

那设计矩阵长什么样?假设你有 6 个样本,分两个批次,每组都有处理 vs 对照:

样本  批次  条件
S1     1    处理
S2     1    对照
S3     1    处理
S4     2    对照
S5     2    处理
S6     2    对照

对应的设计矩阵(不含截距)可能是:

        条件处理  批次2
[1,]        1      0
[2,]        0      0
[3,]        1      0
[4,]        0      1
[5,]        1      1
[6,]        0      1

拟合模型后,批次2的系数就是批次2相对批次1的表达偏移。这个偏移被估计出来后,removeBatchEffect 就会把它从所有样本中减掉。

核心思想:批次效应是线性可加的,而且必须和生物学变量一起建模,否则会被错误吸收到生物学系数里。

你想想看,如果批次和条件完全混在一起(比如所有处理都在批次1,对照都在批次2),那就没办法区分了——这叫完全混杂。我遇到过这种“完美”设计,数据拿到手直接傻眼。

4.2 removeBatchEffect() 函数详解

函数在 limma 包里,签名长这样:

removeBatchEffect(x, batch, batch2 = NULL, covariates = NULL, design = NULL, ...)

我列个表格,把几个关键参数说清楚:

参数 用途 注意事项
x 表达量矩阵(行基因,列样本) 通常需 log2 转换后的数据,如 logCPM
batch 批次分组因子(向量) 必须与样本列一一对应
batch2 第二个批次因子(可选) 比如日期与操作员两个混杂因素
covariates 其他需要保留的连续协变量 比如年龄、GC含量等,不会被移除
design 完整的设计矩阵(包含生物学变量) 如果提供了,函数会用这个设计矩阵拟合,而不是自动构造

基本用法示例

# 假设我们已经有了 logCPM 表达矩阵 expr,批次信息 batch
# 以及一个分组因子 group(处理 vs 对照)
design <- model.matrix(~ group)
expr_corrected <- removeBatchEffect(expr, batch = batch, design = design)

这里有个坑:design 参数一定要包含你关心的生物学变量。我见过有人只传 batch 不传 design,结果函数内部会自动假设一个 intercept-only 模型——这样批次效应估计时就没有考虑生物学差异,相当于把生物学信号也擦掉了一部分。

注意:removeBatchEffect 返回的是修正后的表达矩阵,维度与 x 完全一致。你可以用它做 PCA、热图或者可视化,但千万不要把它直接喂给差异分析函数(比如 lmFit)。原因见下一节。

与差异分析的关系

这个问题值得单拎出来说。我经常在论坛上看到有人问:“我用了 removeBatchEffect 做了批次矫正,然后跑 limma 差异分析,为什么结果不对劲?”

原因很简单:差异分析中的批次效应应该作为协变量留在线性模型里,而不是先一步去掉。

你把批次预移除后,原本属于批次项的自由度被“吞掉”了,残差自由度变大,检验会偏宽松——说白了就是假阳性。反过来,如果批次效应不明显,你又提前移除了,也可能把微弱的真实信号一起丢掉。

正确的做法是:

  • 用 removeBatchEffect 的结果做探索性分析(PCA、聚类)
  • 差异分析时,直接在 lmFit 的设计矩阵中包含批次变量
# 差异分析的正确姿势(无需预矫正)
design_full <- model.matrix(~ batch + group)
fit <- lmFit(expr, design_full)
fit <- eBayes(fit)
results <- topTable(fit, coef = "group")

我刚开始做分析时,为了可视化美观,先把批次去掉,然后顺手就把矫正后的矩阵拿去做差异分析。结果跟朋友的标准流程对比,假阳性基因多了整整一倍。从那以后,我总会在差异分析脚本开头加一句注释:# 此处不要用 pre-corrected data。

一句话总结:removeBatchEffect 是可视化工具,不是统计分析预处理。

4.3 实战案例:真实数据演示

用手边的小例子走一遍。我们用 GSE5859 数据的一个子集(13 男 7 女,两个批次)。

library(limma)
library(GEOquery)

# 加载数据(此处省略完整下载,仅示意流程)
gset <- getGEO("GSE5859", GSEMatrix=TRUE)[[1]]
expr <- log2(exprs(gset))
batch <- as.factor(phenoData(gset)$"batch:ch1")
sex <- as.factor(phenoData(gset)$"Sex:ch1")

# 用于可视化的移除批次
design <- model.matrix(~ sex)
expr_adj <- removeBatchEffect(expr, batch = batch, design = design)

# PCA 对比
par(mfrow=c(1,2))
plotPCA(expr,    col=batch, main="原始数据")
plotPCA(expr_adj, col=batch, main="批次矫正后")

我在项目里用这个流程检查过多次矫正效果。如果 PCA 图上批次聚集现象明显减少,说明矫正有效。

4.4 避坑指南

我这些年踩过的坑,整理成列表,你能少走弯路:

  • 忘记传 design:等于只做了批次平均校正,生物学差异被当作批次抹掉。
  • 批次与条件完美混杂:这种设计本身无解,removeBatchEffect 也救不了。只有重新设计实验。
  • 对非 log2 数据使用:函数默认数据是正态分布的,如果你用的是原始 count,最好先用 cpm 或 voom 转换。
  • 批次因子顺序敏感:我曾经因为 batch 和 batch2 顺序写反,导致第二批次被当作主要批次,结果矫正后批次效应更明显。
  • 直接用于差异分析:前面已经说过,这是最常见的错误。最好在脚本里画个图提醒自己。

小技巧:如果你不确定批次效应是否成功移除,可以计算批次间的方差占比(ANOVA),对比矫正前后批次因子解释的变异比例。我常用 limma::plotMDS 配合 batches 着色,肉眼判断。

批次效应移除与差异分析的正确流程 原始表达量矩阵 设计矩阵 (包含生物学变量 + 批次) 拟合线性模型 估计批次系数 移除批次效应 (减去批次项) 用于 PCA/热图可视化 直接使用 lmFit 批次作为协变量 eBayes 统计检验 残差自由度正确 差异基因列表 (可靠) ✗ 切勿将矫正后数据用于差异分析 颜色说明:蓝色=输入数据 | 紫色=模型设计 | 绿色=可视化流程 | 红色=差异分析流程 | 青色=输出结果