3. 置换检验基本原理归纳总结
说实话,很多做组学数据分析的朋友,一听到「置换检验」就觉得头大。觉得它是个绕来绕去的统计黑盒。但我自己用过几次之后发现,它的核心逻辑其实特别简单——说白了就是一把「终极公平秤」。
你想想看,传统的t检验、方差分析,它们都假设数据服从某个已知分布。但现实世界里,尤其是我们做批次校正后的数据,鬼才知道它到底服从什么分布。这时候参数检验就容易翻车。
置换检验的好处在于:它不管你的数据长什么样,它只问你一个核心问题——「你观察到的差异,放到随机世界里算不算稀奇?」
3.1 核心思想:把标签打乱
置换检验的整个逻辑建立在「零假设成立」的基础上。什么意思呢?就是说我们假设分组和观测值之间没有关联——A组和B组其实是同一堆数据,只是被我们随机贴了个标签。
那怎么验证这个假设?很简单:把标签随机打乱,重新算统计量,重复很多很多次。如果原始统计量在随机分布里属于极端值,那就说明原始分组确实有东西。
我刚接触这个方法时,总觉得它像个「暴力穷举法」。但后来发现,这种「不依赖任何理论分布」的特性,恰恰是它最大的护城河。
一句话总结:置换检验 = 对数据框的行进行随机重排 + 重新计算差异指标 + 重复N次 + 看原始结果落在什么位置。
3.2 零假设与检验统计量
做置换检验你得先定两样东西:零假设和检验统计量。
零假设 H₀:两组样本来自同一分布,标签是随机分配的。说白了,如果我把组别标签打乱,重新算出来的统计量应该和原始的差不多。
检验统计量:这个你随便选。常见的有均值差、中位数差、t统计量、秩和统计量等等。我个人的习惯是——选一个最贴近你研究问题的指标。比如你要比较表达量变化,用均值差就足够了。
| 统计量类型 | 适用场景 | 计算示例 |
|---|---|---|
| 均值差 | 对称分布、无异常值 | mean(B) - mean(A) |
| 中位数差 | 存在异常值 | median(B) - median(A) |
| t统计量 | 样本量不均衡 | (mean_diff) / (pooled_se) |
| 秩和差 | 非参数、稳健 | rank_sum(B) - rank_sum(A) |
小经验:如果你不确定选哪个统计量,我建议你先用均值差。它最直观,而且置换检验本身对统计量的分布假设不做要求,所以选简单的反而更容易解释。
3.3 置换检验的完整步骤
嗯,这里我把整个流程拆成五个步骤,你看完就能自己写代码了。
- 合并数据:把A组和B组的数据合并成一个向量,长度n = nA + nB。
- 计算原始统计量:用真实的分组标签,计算你选好的统计量(比如均值差)。记录下来,记为T₀。
- 重复打乱与计算:把合并后的数据随机分成两组,大小跟原始A组、B组一样。重新计算统计量Tᵢ。重复这个步骤很多次(比如1000次或10000次)。
- 构建零分布:把所有的Tᵢ汇总成一个分布,这就是「零假设下的统计量分布」。
- 计算p值:看T₀在这个分布里的位置。p = (|Tᵢ| ≥ |T₀| 的次数 + 1) / (总置换次数 + 1)。
我曾经在这个「+1」上吃过亏。有一次算出来的p值恰好是0,直觉告诉我不对劲。后来查资料才发现,加这个1是为了避免p值为0——因为零假设下,T₀本身也是零分布里的一个样本。
import numpy as np
def permutation_test(a, b, n_perm=10000, statistic='mean_diff'):
# 合并数据
combined = np.concatenate([a, b])
n_a = len(a)
n_b = len(b)
# 原始统计量
if statistic == 'mean_diff':
T0 = np.mean(b) - np.mean(a)
# 存储置换结果
perm_stats = np.zeros(n_perm)
for i in range(n_perm):
np.random.shuffle(combined)
perm_a = combined[:n_a]
perm_b = combined[n_a:]
if statistic == 'mean_diff':
perm_stats[i] = np.mean(perm_b) - np.mean(perm_a)
# 计算p值(加上1避免p=0)
p = (np.sum(np.abs(perm_stats) >= np.abs(T0)) + 1) / (n_perm + 1)
return T0, p, perm_stats
注意:置换次数太少会影响p值的分辨率。比如你只做100次,那么p值最小只能到0.01左右。我建议至少1000次,组学数据常做10000次或更多。
3.4 一个完整的案例连起来
我们拿一个具体场景来走一遍。假设你有两个基因表达向量,一个是处理组,一个是对照组。你怀疑处理组表达量更高。
第一步,计算原始均值差。第二步,把两组数据合并、打乱、重分组、再算均值差。重复10000次。第三步,画出零分布,把你的原始均值差标上去。
为什么会这样?因为如果零假设成立,你的原始均值差应该落在零分布的中间区域。如果它跑到尾巴上去了,那就说明——你观察到的差异不太可能是随机产生的。
# 模拟数据
np.random.seed(42)
group_a = np.random.normal(loc=10, scale=2, size=30)
group_b = np.random.normal(loc=12, scale=2, size=30)
T0, p_value, perm_dist = permutation_test(group_a, group_b, n_perm=10000)
print(f"原始均值差: {T0:.4f}")
print(f"置换p值: {p_value:.4f}")
输出大概长这样:
原始均值差: 2.1543
置换p值: 0.0023
p值0.0023意味着——在零假设下,你观察到这么极端的结果的概率只有0.23%。这当然很小,所以我们有理由拒绝零假设,认为两组确实有差异。
我个人习惯在做完置换检验后,再加一个可视化:把零分布画成直方图,把原始统计量用一条竖线标出来。这样你一眼就能看出结果是否显著,也给审稿人一个直观证据。
3.5 做个总结
置换检验说白了就是一个「暴力重采样」的思路。它不依赖任何分布假设,所以特别适合批次校正后那种奇奇怪怪的数据分布。你想啊,批次校正后的数据,有时候是偏态分布,有时候有长尾,参数检验很难处理。而置换检验一把梭,只问结果够不够极端。
有一点你务必记住:置换检验得到的p值是<0.05还是>0.05,本身只告诉你「随机产生这个结果的可能性有多大」。它不告诉你效应大小,也不告诉你实际差异有没有生物学意义——那是另外两步的事情了。