第四章 基于统计检验的检测方法:ANOVA、Kruskal-Wallis、ComBat
各位做药物批次效应检测,最怕什么?
最怕你不知道有批次效应,或者知道了却拿不准它的严重程度。
这一章咱们就来聊聊怎么用统计检验给批次效应做个"体检"——ANOVA、Kruskal-Wallis和ComBat。这三招我在项目里反复用过,各有各的脾气。
4.1 为什么先用统计检验打头阵?
说实话,很多人一上来就上深度学习模型,结果数据本身就有结构性偏差,模型学到的全是批次差异。我个人习惯是:可视化之后,立刻上统计检验,先定性再定量。
统计检验能帮你回答三个核心问题:
- 有没有差异?——批次间的均值或分布是否显著不同
- 差异多大?——效应量的大小
- 哪些特征受影响?——具体是哪些基因/化合物被批次"污染"了
你想想看,如果没有这一步就直接上校正,很容易过度校正或校正不足。我在一个RNA-seq项目里就吃过这个亏,当时跳过ANOVA直接跑ComBat,结果把真实生物学差异也给抹掉了,后来重跑才补救回来。
4.2 ANOVA:参数检验的"老大哥"
ANOVA(方差分析)是检验多个批次均值是否相等的经典方法。它假设数据服从正态分布且方差齐性——说白了就是各批次的"身材"差不多,只是"站位"可能不同。
适用场景:连续型数据、批次数量≥2、正态分布假设基本成立
ANOVA的核心逻辑是这样的:
- 总变异 = 批次间变异 + 批次内变异
- 如果批次间变异远大于批次内变异,F值就大,p值就小
- p < 0.05 说明至少有一个批次和其他批次不一样
我一般先用ANOVA做个全局扫描。代码很简单:
import pandas as pd
import numpy as np
from scipy import stats
# data: 行为样本,列为特征,batch: 批次标签
def batch_effect_anova(data, batch_labels):
results = []
for col in data.columns:
groups = [data[col][batch_labels == b] for b in np.unique(batch_labels)]
f_stat, p_val = stats.f_oneway(*groups)
results.append({'feature': col, 'F_stat': f_stat, 'p_value': p_val})
return pd.DataFrame(results).sort_values('p_value')
# 举个实际例子
df_anova = batch_effect_anova(expression_data, batch)
print(df_anova.head(10))
跑完之后,我会重点看p值最小的那些特征——它们被批次效应影响最严重。
注意:ANOVA对异常值非常敏感。如果你的数据里有离谱的离群点,先做截尾处理或改用非参数方法。我曾经在一个代谢组学数据上直接跑ANOVA,结果被两个异常样本带偏了,差点误判。
4.3 Kruskal-Wallis:非参数检验的"稳健派"
遇到数据不满足正态分布,或者样本量小、方差不齐,怎么办?
这个时候Kruskal-Wallis检验就派上用场了。它不关心数据的具体分布,只关心数据的秩(排名)。说白了,就是把所有数据排个队,看各批次的排名分布是否差不多。
适用场景:连续型或有序数据、分布未知或非正态、样本量差异大
Kruskal-Wallis的H统计量计算:
- 把所有数据混合排序,赋秩次
- 计算每个批次的平均秩次
- 如果各批次的平均秩次差异大,H值就大,p值就小
from scipy.stats import kruskal
def batch_effect_kruskal(data, batch_labels):
results = []
for col in data.columns:
groups = [data[col][batch_labels == b] for b in np.unique(batch_labels)]
h_stat, p_val = kruskal(*groups)
results.append({'feature': col, 'H_stat': h_stat, 'p_value': p_val})
return pd.DataFrame(results).sort_values('p_value')
# 小样本场景下我更喜欢用这个
df_kw = batch_effect_kruskal(expression_data, batch)
print(df_kw[df_kw['p_value'] < 0.05].shape[0], "个特征存在显著批次效应")
Kruskal-Wallis的优点是稳健,缺点是灵敏度比ANOVA稍低——如果数据真的满足正态分布,ANOVA的检验力更强。我个人习惯是:样本量大且正态时用ANOVA,否则用Kruskal-Wallis。
个人技巧:两个方法都跑一遍,看看结果的一致性。如果ANOVA和Kruskal-Wallis同时指向同一个特征,那这个特征的批次效应基本上是"实锤"了。
4.4 ComBat:经验贝叶斯的"矫正大师"
好,前面两个方法都是在"检测"问题。那检测出来之后呢?
ComBat就是目前最流行的批次效应校正方法之一。它由Johnson和Li在2007年提出,用经验贝叶斯估计每批次的均值和方差偏移,然后把这些偏移"掰正"。
ComBat的核心假设:各批次的特征分布属于同一族,只是位置和尺度不同
ComBat的校正过程可以概括为:
- 估计每批次的均值偏移和方差偏移
- 用经验贝叶斯"收缩"估计值,防止过度校正
- 从原始数据中减去批次偏移,保留生物学变异
# 使用pycombat库
from pycombat import pycombat
# expression_data: 基因表达矩阵 (基因为行,样本为列)
# batch: 批次标签,长度与样本数一致
adjusted_data = pycombat(expression_data, batch)
# 校正前后对比
print("校正前批次间均值差异:",
expression_data.groupby(batch).mean().std().mean())
print("校正后批次间均值差异:",
adjusted_data.groupby(batch).mean().std().mean())
实战建议:ComBat对样本量较小的批次会做"收缩",这恰恰是它的优势。如果你有一个批次只有2-3个样本,ComBat会谨慎地调整,而不是硬掰。
ComBat有很多变体,比如ComBat-Seq处理RNA-seq的count数据,ComBat-GAM处理非线性时间序列数据。我一般先用标准ComBat,如果效果不理想再换变体。
避坑指南:ComBat不能处理缺失值。如果你数据里有NaN,记得先做填充或删除。我曾经因为忘了这步,跑出来的结果是全NaN,排查了半天才发现。
4.5 三种方法的对比与选型
这三种方法我都反复用过,很多时候是组合使用的。下面这张表是我自己总结的选型指南:
| 维度 | ANOVA | Kruskal-Wallis | ComBat |
|---|---|---|---|
| 目的 | 检测差异 | 检测差异 | 校正差异 |
| 数据类型 | 连续型,正态 | 连续型/有序,非正态 | 连续型/离散型 |
| 批次数量 | ≥2 | ≥2 | ≥2 |
| 样本量要求 | 每批≥10 | 每批≥5 | 每批≥2 |
| 输出结果 | F值, p值 | H值, p值 | 校正后数据 |
| 是否校正 | 否 | 否 | 是 |
| 稳健性 | 低(对异常值敏感) | 中(基于秩次) | 高(经验贝叶斯收缩) |
说说我的工作流:先用ANOVA或Kruskal-Wallis做全特征扫描,筛出受批次效应影响严重的特征;然后用ComBat做整体校正;最后再用同样的检验方法验证校正效果。这样就形成闭环了。
4.6 组合使用的实战案例
我来说一个具体的例子。之前做一个多中心药物代谢组学研究,3个实验室,每个实验室测了50个样本。我先做了主成分分析,发现PC1和PC2基本是按实验室分的——典型的批次效应。
第一步,用Kruskal-Wallis检测每个代谢物:
kw_results = batch_effect_kruskal(metabolite_data, lab_batch)
hits = kw_results[kw_results['p_value'] < 0.01]
print(f"检测到{hits.shape[0]}个代谢物有显著批次效应(p<0.01)")
第二步,用ComBat校正:
from pycombat import pycombat
corrected_data = pycombat(metabolite_data.T, lab_batch).T
第三步,用Kruskal-Wallis再次验证:
kw_after = batch_effect_kruskal(corrected_data, lab_batch)
hits_after = kw_after[kw_after['p_value'] < 0.01]
print(f"校正后剩{hits_after.shape[0]}个代谢物仍有显著批次效应")
结果:从187个显著代谢物降到了22个,校正效果很理想。那22个我进一步分析,发现其中有几个是真正的生物学差异。
经验之谈:校正前后对比p值的分布变化,比单看p值更有意义。如果校正后p值分布整体右移(变大了),说明ComBat确实在起作用。
4.7 选型建议与注意事项
总结几条我踩过坑之后得出的建议:
- 小样本(每批<5):首选Kruskal-Wallis做检测,ComBat做校正时注意收缩参数
- 大样本(每批>30):ANOVA + ComBat组合,灵敏度高
- 高维数据(特征数>样本数):直接上ComBat,检测步骤可以简化
- 非连续数据(count、有序):Kruskal-Wallis + ComBat-Seq
重要提醒:统计检验只能告诉你"有没有",不能告诉你"该不该校正"。有些批次差异可能本身就包含了生物学意义——比如不同中心收集的病人亚型不同。你想想看,盲目校正反而会抹掉真实发现。
这一章的内容就到这里。ANOVA、Kruskal-Wallis、ComBat这三板斧,是我每次处理批次效应的标配工具。检测和校正缺一不可,组合使用才能把数据洗干净。