ComBat 的 Location-Scale 模型:从原理到调参直觉

做批次校正的人,十有八九绕不开 ComBat。但说实话,很多人只把它当黑盒调参,参数调来调去,最后一问“你到底在调整什么?”,答不上来。我个人习惯是,用工具前先搞懂它背后的数学模型,不然你连报错都读不懂。

ComBat 的核心假设很简单:每个批次的生产数据,可以看作是从一个“共性分布”里加上批次特有的位置偏移(Location)和尺度伸缩(Scale)得来的。用统计语言讲,这就是一个 Location-Scale 模型。你在做的 Batch Effect 校正,本质上就是在估计并消除这两个扰动量。

我的经验口诀: ComBat 不是直接拽数据,而是先算出每个批次“偏了多少(Location)”、 “散了多少(Scale)”,然后用贝叶斯的后验估计把它们拉齐。

Location-Scale 模型的数学骨架

我们先把公式摆出来,你会发现它其实不复杂。假设你有 \(i\) 个特征(比如基因表达矩阵里的基因), \(j\) 个样本,\(k\) 个批次。原始数据 \(Y_{ijk}\) 被建模为:

Y_{ijk} = α_i + β_i * X_j + γ_{ik} + δ_{ik} * ε_{ijk}

其中:
  α_i   —— 特征的全局基线均值
  β_i   —— 协变量(如年龄、性别)对特征的影响系数
  X_j   —— 样本 j 的协变量向量
  γ_{ik} —— 批次 k 对特征 i 的**位置偏移**(均值偏移)
  δ_{ik} —— 批次 k 对特征 i 的**尺度因子**(方差缩放)
  ε_{ijk} —— 残差,服从 N(0, σ_i²)

注意看,这里的 γik 和 δik 才是我们真正要校正的目标。 在校正过的数据 Yijkadj 里,它们会被减去或除掉:

Y_{ijk}^{adj} = (Y_{ijk} - γ_{ik}) / δ_{ik} + γ_{ik}^{global}

γikglobal 是全局均值,目的是让校正后数据的整体中心不跑偏。嗯,这里要注意—— 很多使用者会忘记加回这个全局均值,导致校正后的数据整体平移了一个常数。

我曾经踩过的坑: 有一次用 ComBat 校正多批次 RNA-seq 数据, 反复检查校正效果都不对,后来发现是我在手动实现公式时 忘记了加回全局均值,导致每个特征的均值都集体向左挪了 0.5 个单位。 那晚我盯着火山图看了半小时,才意识到 γikglobal 才是保持整体可比性的关键。

贝叶斯先验:不让人多嘴的经验法则

直接对每个批次每个特征单独估计 γ 和 δ 是不行的。样本少时,估计值方差大,校正完反而引入噪声。ComBat 干的聪明事是:把所有特征的 γ 和 δ 放到一个层次模型里,用经验贝叶斯(Empirical Bayes)方法共享信息。

简单说,它们假设:

  • γik 来自一个正态分布,均值为 0,方差为 τk²(先验方差)。
  • δik 来自一个逆伽马分布,形状参数 λk,尺度参数 θk。

这些先验参数 τk²、λk、θk 是从所有特征的数据中估计出来的。你想想看,这就相当于把所有特征的“批次效应强弱”聚在一起看,然后给每个单独的估计“拉一把”。

用代码表达这个先验估计过程(简化版):

# 伪代码:对每个批次 k 计算先验参数
for k in batches:
    # 先算每个特征的原始偏移和方差
    gamma_hat[k, i] = mean(Y[i, j in batch k]) - global_mean[i]
    delta_hat[k, i] = var(Y[i, j in batch k]) / pool_var[i]

    # 然后从所有特征中求先验
    tau_k_sq = var(gamma_hat[k, :])   # 位置偏移的方差
    lambda_k, theta_k = fit_inv_gamma(delta_hat[k, :])  # 尺度因子的先验
⚠️ 参数调优关键: ComBat 默认的贝叶斯框架有“收缩强度”控制, 在 R 包的 mean.only=FALSE 或 Python 版 par.prior 参数里体现。 如果你的数据批次效应只影响均值,不影响方差,可以设 mean.only=TRUE, 避免对方差做不必要的调整。我在单细胞数据上试过这个开关,效果差异挺大。

后验估计与校正:贝叶斯把力往中间“拉”

有了先验,下一步就是计算后验均值 E[γik | data] 和 E[δik | data]。 对于正态-正态共轭模型,后验公式是:

γ_{ik}^{post} = ( tau_k² / ( tau_k² + sigma_i² )) * γ_{ik}^{hat}

也就是:后验均值 = 先验方差 / (先验方差 + 残差方差) * 原始估计

这个比值叫做“收缩因子”。当某个特征的原始估计方差(sigma_i²)很大时, 收缩因子接近 0,后验值被拉向先验均值 0,相当于告诉系统“这个特征的单批样本太少, 别信它的偏移,用全局信息补”。反之,如果特征本身很稳定(sigma_i² 小), 就基本保留原始估计。这个机制,说白了就是贝叶斯在替你做权衡。

同理,δik 的后验期望也由逆伽马共轭公式给出,你会看到同样的收缩效果—— 特征数少时,尺度因子被拉向全局先验,避免方差被错误放大或缩小。

一张图看懂整个流程

下面这张 SVG 把 Location-Scale 模型的核心数据流画了出来。从左到右, 你能清楚看到:原始数据 → 估计位置/尺度 → 贝叶斯先验 → 后验收缩 → 校正输出。

ComBat Location-Scale 模型处理流程 原始多批次数据 Yijk 逐特征逐批次 估计 γik & δik (原始偏移 + 尺度因子) 经验贝叶斯:从所有特征 估计先验 τk², λk, θk 后验 γikpost + δikpost (贝叶斯收缩) Yijkadj 输出 (减位置 / 除尺度) 共享残差 σi² 说明:模型先并行估计每个特征的原始批次效应,再通过经验贝叶斯跨特征借力,最后用后验替代原始估计完成校正。

不同参数对 Location-Scale 模型的影响

为了让调参更有方向,我整理了一个对照表,把 ComBat 主要参数和位置/尺度调整的关系列出来。你看完就知道该扭哪个旋钮。

参数(R 包名称) 影响位置(γ) 影响尺度(δ) 典型场景建议
mean.only=TRUE √(仅校正均值) ✕(不校正方差) 批次间方差相似,仅均值漂移;慎用,误用会保留方差异质性
par.prior=TRUE (默认) √ 强收缩 √ 强收缩 特征数 < 500 或批次内样本少,建议保留默认
par.prior=FALSE √ 弱收缩 √ 弱收缩 特征数 > 5000 且每批样本足,可放开先验限制
手动调整 prior.plugs √ 直接替代先验 √ 直接替代先验 仅高级用户:你知道批次效应先验分布时用,
一个实用技巧: 当你发现校正后某个批次的数据整体方差明显异于其他批次, 大概率是 δ 的收缩过度或不足。试试 par.prior=FALSE,如果方差差距缩小, 说明默认参数收缩过分了。我在一次微阵列数据整合中,就是靠这个技巧, 把校正后的跨批次方差从 1.8 降到了 1.05。

避坑指南:Location-Scale 模型的三个常见误解

  1. 误解一:ComBat 可以处理任何形式的批次效应。 不是的。它只假设可加性位置偏移 + 乘性尺度偏移。如果你的批次效应是非线性的(比如芯片扫描时间引起的漂移),ComBat 会留下残余。我在做多组学整合时遇到过这种情况,后来只好用 RUVseq 做二次校正。
  2. 误解二:Location 和 Scale 校正互不影响。 实际上,它们通过残差方差 σi² 耦合。如果你把 δ 强制设为 1(尺度不调),γ 的后验收缩强度会受 σi² 影响,而 σi² 是未校正方差。校正过程中,最好先做尺度调整再做位置调整(ComBat 内部就是这个顺序)。
  3. 误解三:贝叶斯先验的参数不用调。 别天真。默认先验假设所有特征的批次效应分布相同,如果你的数据有“批次效应异质性”(比如某些基因对批次极度敏感,另一些不敏感),默认先验会过度平滑。我曾经在 TCGA 数据整合里,不得不自己写了一个加权先验版本。