实验结果的统计显著性检验:t检验、bootstrap与效应量的正确使用

📅 2026/7/17 18:19:04
实验结果的统计显著性检验:t检验、bootstrap与效应量的正确使用
实验结果的统计显著性检验t检验、bootstrap与效应量的正确使用一、为什么模型A比模型B高1.2%不足以作为结论在机器学习实验报告中我们的方法在测试集上达到85.7%比baseline的84.5%提高了1.2个百分点是一种常见的陈述模式。这种单次比较在方法论上是脆弱的——它没有回答一个核心问题如果使用不同的随机种子或数据划分重新训练这个1.2%的提升会反复出现还是可能消失甚至逆转统计显著性检验的目标就是回答这一问题。它量化了观测到的差异是真实效应还是随机波动的概率。但显著性检验在ML社区中面临两个极端一边是完全不使用检验仅报告单次运行的绝对数值另一边是机械地使用p0.05作为二元决策阈值忽视效应量和统计功效。正确的做法介于两者之间。二、t检验在ML实验中的正确应用t检验是ML实验中最常用的显著性检验方法但其变体的选择取决于实验设计独立双样本t检验适用于两组独立运行的模型。例如在10个完全不同的随机种子上分别训练模型A和模型B比较两组结果的均值差异。配对t检验适用于配对设计的实验。最常见的配对场景是k折交叉验证——模型A和模型B在完全相同的k个数据划分上进行评估形成k对观测值。配对检验消除了数据划分带来的方差通常比独立检验更敏感。import numpy as np from scipy import stats from typing import Tuple, Optional, List def run_statistical_comparison( scores_a: np.ndarray, scores_b: np.ndarray, paired: bool True, alpha: float 0.05, effect_size_threshold: float 0.2 # 最小有意义的Cohens d ) - dict: 对两组实验分数进行完整的统计比较。 报告包括 - 均值差和95%置信区间 - 检验类型和p值 - Cohens d效应量及解读 - 是否达到统计显著和实际显著 Args: scores_a: 模型A的多次运行分数如10次不同seed scores_b: 模型B的多次运行分数 paired: True配对t检验False独立样本t检验 alpha: 显著性水平 effect_size_threshold: 最小有意义的效应量阈值 Returns: 包含所有统计量的字典 mean_a, mean_b np.mean(scores_a), np.mean(scores_b) diff mean_b - mean_a # 执行检验 if paired: # 配对t检验每对观测是在相同条件下获得的 t_stat, p_value stats.ttest_rel(scores_b, scores_a) test_type paired t-test df len(scores_a) - 1 else: t_stat, p_value stats.ttest_ind(scores_b, scores_a) test_type independent t-test # Welch-Satterthwaite自由度近似 v_a np.var(scores_a, ddof1) / len(scores_a) v_b np.var(scores_b, ddof1) / len(scores_b) df (v_a v_b) ** 2 / ( v_a ** 2 / (len(scores_a) - 1) v_b ** 2 / (len(scores_b) - 1) ) # 差值95%置信区间 if paired: diff_values scores_b - scores_a se stats.sem(diff_values) ci stats.t.interval(0.95, dflen(diff_values)-1, locnp.mean(diff_values), scalese) else: # Welch近似 se np.sqrt( np.var(scores_a, ddof1) / len(scores_a) np.var(scores_b, ddof1) / len(scores_b) ) ci (diff - stats.t.ppf(0.975, df) * se, diff stats.t.ppf(0.975, df) * se) # Cohens d 效应量 d_value cohens_d(scores_a, scores_b) d_interpretation interpret_cohens_d(d_value) return { mean_a: mean_a, mean_b: mean_b, mean_difference: diff, ci_95: ci, test_type: test_type, t_statistic: t_stat, p_value: p_value, degrees_of_freedom: df, cohens_d: d_value, d_interpretation: d_interpretation, statistically_significant: p_value alpha, practically_significant: abs(d_value) effect_size_threshold } def cohens_d(group_a: np.ndarray, group_b: np.ndarray) - float: 计算Cohens d效应量。 使用合并标准差pooled SD。 对于小样本n20建议使用Hedges g乘以修正因子。 Args: group_a, group_b: 两组数据 Returns: Cohens d值正数表示B组均值高于A组 n_a, n_b len(group_a), len(group_b) var_a, var_b np.var(group_a, ddof1), np.var(group_b, ddof1) # 合并方差 pooled_var ((n_a - 1) * var_a (n_b - 1) * var_b) / (n_a n_b - 2) pooled_std np.sqrt(pooled_var) if pooled_std 0: return 0.0 d (np.mean(group_b) - np.mean(group_a)) / pooled_std # Hedges g 修正小样本时推荐 # 修正因子 J 1 - 3 / (4*(n_an_b) - 9) correction 1 - 3 / (4 * (n_a n_b) - 9) return d * correction def interpret_cohens_d(d: float) - str: 根据Cohen的传统阈值解读效应量大小。 d_abs abs(d) if d_abs 0.2: return 可忽略 (negligible) elif d_abs 0.5: return 小效应 (small) elif d_abs 0.8: return 中效应 (medium) else: return 大效应 (large)三、Bootstrap当分布假设不成立时t检验依赖于数据服从正态分布的假设。在ML实验中这个假设有时不成立——例如F1分数是有界的[0,1]在小样本下其抽样分布可能严重偏态。Bootstrap提供了一种不依赖分布假设的非参数替代方案。def bootstrap_comparison( scores_a: np.ndarray, scores_b: np.ndarray, n_bootstrap: int 10000, paired: bool True ) - dict: 使用Bootstrap进行两组比较。 优势不依赖正态分布假设 缺点当样本量很小时n5bootstrap分布可能不准确。 Args: scores_a, scores_b: 两组实验结果 n_bootstrap: bootstrap重抽样次数 paired: 是否配对采样 Returns: 包含bootstrap p值和置信区间的字典 rng np.random.default_rng(42) n len(scores_a) observed_diff np.mean(scores_b) - np.mean(scores_a) bootstrap_diffs np.zeros(n_bootstrap) if paired: # 配对bootstrap保持配对结构 diff_values scores_b - scores_a for i in range(n_bootstrap): indices rng.integers(0, n, sizen) bootstrap_diffs[i] np.mean(diff_values[indices]) else: # 独立bootstrap分别采样 for i in range(n_bootstrap): idx_a rng.integers(0, n, sizen) idx_b rng.integers(0, n, sizen) bootstrap_diffs[i] ( np.mean(scores_b[idx_b]) - np.mean(scores_a[idx_a]) ) # 双侧p值 p_value np.mean(np.abs(bootstrap_diffs - observed_diff) np.abs(observed_diff)) # 95%百分位置信区间 ci_lower np.percentile(bootstrap_diffs, 2.5) ci_upper np.percentile(bootstrap_diffs, 97.5) return { observed_diff: observed_diff, bootstrap_p_value: p_value, ci_95_bootstrap: (ci_lower, ci_upper), n_bootstrap: n_bootstrap }四、从统计显著到实际显著效应量的关键作用在大规模实验中如对数十万测试样本进行评估极小的性能差异也可能在统计上显著p0.001。但0.05个百分点的准确率提升在实际应用中毫无意义。效应量Cohens d或原始差值弥补了p值的这一盲区。实践建议(1) 始终报告效应量及其置信区间而非仅报告p值(2) 在实验前定义实际显著性阈值——如只有Cohens d 0.3或准确率提升 0.5%的改进才值得进一步推进(3) 当效应量小但统计显著时大样本下的常见情形诚实告知读者差异虽统计显著但幅度有限。五、总结统计显著性检验在ML实验中的恰当使用遵循三个原则(1) 检验方法匹配实验设计——配对设计用配对检验多组比较用ANOVA事后校正非正态用小样本bootstrap(2) 效应量优先于p值——效应量回答了差异有多大这个工程上更关键的问题p值仅回答了差异是否可能为零(3) 报告完整的统计量——均值差、95%置信区间、效应量和p值一起构成完整的证据链缺少任何一环都会削弱结论的可信度。最后统计检验不是替代严谨实验设计的捷径——在5次随机种子上做t检验得到的显著结论其可信度远不如在20次独立运行上获得的非显著但方向一致的观测。