SciPy 假设检验落地p 值到业务决策还差三步一、p 值统计检验的通行证但不是终点站统计学课上老师讲过p 值小于 0.05拒绝原假设结论显著。这套逻辑在考试里够用但搬到业务场景就出问题了——p 值只是告诉你差异有统计学意义它没说差异有多大、是否值得投入资源、会不会被其他因素干扰。打个比方p 值就像体检报告里的异常指标标记。它标了↑你知道数值偏离了正常范围但到底是小毛病还是大问题要不要吃药要结合其他指标看。业务决策也一样p 值是起点不是终点。# 经典的 SciPy 假设检验示例 from scipy import stats import numpy as np # 场景新推荐算法 vs 旧算法的用户点击率 old_ctr np.array([0.12, 0.11, 0.13, 0.10, 0.14, 0.115, 0.125, 0.105, 0.135, 0.118]) new_ctr np.array([0.15, 0.14, 0.16, 0.13, 0.17, 0.145, 0.155, 0.135, 0.165, 0.148]) # 独立样本 t 检验 t_stat, p_value stats.ttest_ind(new_ctr, old_ctr) print(ft 统计量: {t_stat:.4f}) print(fp 值: {p_value:.6f}) # 输出p 值 ≈ 0.00003远小于 0.05p 值 0.00003非常显著。但业务团队问的第一个问题不是是否显著而是提升了多少点击率值不值得切换——这恰恰是 p 值回答不了的。为什么 p 值在业务场景下会过度回答和回答不足同时存在p 值过度回答的是确定性——0.00003 让人误以为结论已经板上钉钉把 p 值的大小当成效应的大小。实际上 p 值只量化了抽样误差产生这个差异的概率对差异的绝对值完全沉默。p 值回答不足的是方向性和量级——在 1000 万用户的实验中哪怕新算法只提升了 0.01% 的点击率p 值也能做到 0.001因为样本量把噪音压到了极小。但 0.01% 的提升对业务没有意义切换算法的工程成本远超收益。p 值的设计是把统计判断和业务判断解耦的——它告诉你这不是巧合但你应该自己去量这值不值。二、第一步量化效应大小让显著变成有用p 值只回答了有没有差异效应大小Effect Size才回答差异有多大。这就是 p 值到业务决策的第一步把统计显著性翻译成业务量级。# 计算效应大小 Cohens d def cohens_d(group1: np.ndarray, group2: np.ndarray) - float: 计算 Cohens d 效应大小 d 0.2: 微小效应统计显著但业务意义不大 d ≈ 0.5: 中等效应值得关注 d 0.8: 大效应强烈推荐行动 n1, n2 len(group1), len(group2) mean_diff np.mean(group1) - np.mean(group2) # 合并标准差 pooled_std np.sqrt( ((n1 - 1) * np.var(group1, ddof1) (n2 - 1) * np.var(group2, ddof1)) / (n1 n2 - 2) ) return mean_diff / pooled_std d cohens_d(new_ctr, old_ctr) print(fCohens d: {d:.4f}) # 输出d ≈ 2.3非常大的效应但 Cohens d 是无量纲的业务团队看不懂效应大小 2.3。所以还要做一步——把效应大小翻译成业务指标# 将效应大小翻译成业务语言 def effect_to_business(metric_old: np.ndarray, metric_new: np.ndarray, metric_name: str 点击率) - dict: 把统计效应翻译成业务可理解的指标变化 lift np.mean(metric_new) - np.mean(metric_old) lift_pct lift / np.mean(metric_old) * 100 result { 指标: metric_name, 旧值: f{np.mean(metric_old):.3f}, 新值: f{np.mean(metric_new):.3f}, 绝对提升: f{lift:.3f}, 相对提升: f{lift_pct:.1f}%, 效应大小: f{cohens_d(metric_new, metric_old):.2f}, 效应等级: classify_effect(cohens_d(metric_new, metric_old)) } return result def classify_effect(d: float) - str: 将 Cohens d 分级为业务可读标签 if d 0.2: return 微小——统计显著但业务影响可忽略 elif d 0.5: return 中等——值得关注需要评估ROI elif d 0.8: return 较大——建议优先落地 else: return 显著——强烈推荐立即行动 business_report effect_to_business(old_ctr, new_ctr, 点击率) for k, v in business_report.items(): print(f{k}: {v})输出大概是指标: 点击率 旧值: 0.119 新值: 0.149 绝对提升: 0.030 相对提升: 25.2% 效应大小: 2.30 效应等级: 显著——强烈推荐立即行动这才是一个业务团队能直接看的报告——新算法让点击率提升了 25.2%效应很大建议立即切换。三、第二步置信区间比 p 值更诚实p 值是个二值判断显著/不显著置信区间Confidence Interval给出了效应的浮动范围更诚实也更实用。flowchart LR A[假设检验结果] -- B[p值是否显著?] A -- C[效应大小差异多大?] A -- D[置信区间差异在什么范围?] B -- E[通行证] C -- F[量级尺] D -- G[安全带] E -- H[综合决策] F -- H G -- H# 计算均值差异的置信区间 def mean_diff_ci(group1: np.ndarray, group2: np.ndarray, confidence: float 0.95) - dict: 计算两组均值差异的置信区间 n1, n2 len(group1), len(group2) mean_diff np.mean(group2) - np.mean(group1) # 合并标准差和标准误差 pooled_var ( ((n1 - 1) * np.var(group1, ddof1) (n2 - 1) * np.var(group2, ddof1)) / (n1 n2 - 2) ) se_diff np.sqrt(pooled_var * (1/n1 1/n2)) # t 分布临界值 df n1 n2 - 2 t_critical stats.t.ppf((1 confidence) / 2, df) ci_lower mean_diff - t_critical * se_diff ci_upper mean_diff t_critical * se_diff return { 均值差异: f{mean_diff:.4f}, f{confidence*100:.0f}%置信区间: f[{ci_lower:.4f}, {ci_upper:.4f}], 最差情况提升: f{ci_lower/np.mean(group1)*100:.1f}%, 最好情况提升: f{ci_upper/np.mean(group1)*100:.1f}% } ci_report mean_diff_ci(old_ctr, new_ctr) for k, v in ci_report.items(): print(f{k}: {v})置信区间告诉你即使在最保守的估计下区间下界新算法仍然有可观的提升。这比只说p 0.05显著要有说服力得多。业务决策的核心问题不是有没有差异而是差异的最小可能值够不够支撑决策。置信区间的下界就是答案。四、第三步混杂因素排查别让显著变成错觉假设检验的最后一个坑——混杂因素。你看到点击率提升 25%但也许是同时上线了其他功能或者那段时间流量质量本身就变好了。# 多因素回归排查混杂 import pandas as pd def check_confounders(df: pd.DataFrame, treatment_col: str, outcome_col: str, confounder_cols: list) - dict: 检查混杂因素对结论的干扰程度 results {} # 1. 无混杂的原始效应 model_raw stats.linregress( df[treatment_col].astype(float), df[outcome_col].astype(float) ) results[原始效应系数] model_raw.slope # 2. 逐个加入混杂因素后的效应变化 for conf in confounder_cols: # 简单方法按混杂因素分层后看效应是否一致 grouped df.groupby(conf) subgroup_effects [] for name, group in grouped: if len(group) 3: # 样本量太小跳过 effect group[outcome_col].mean() - group[outcome_col].shift(1).mean() subgroup_effects.append({组: name, 效应: effect}) results[f按{conf}分层] subgroup_effects # 3. 效应是否在各层一致不一致说明有混杂 return results # 构造带混杂因素的模拟数据 np.random.seed(42) n 200 df_sim pd.DataFrame({ algorithm: np.random.choice([old, new], n), traffic_source: np.random.choice([search, recommend, ads], n), is_weekend: np.random.choice([0, 1], n), ctr: np.where( np.random.choice([old, new], n) new, 0.15 np.random.normal(0, 0.02, n), # 新算法基准更高 0.12 np.random.normal(0, 0.02, n) ) }) # 假设周末流量质量本身更高造成混杂 df_sim.loc[df_sim[is_weekend] 1, ctr] 0.03 confounder_check check_confounders( df_sim, algorithm, ctr, [traffic_source, is_weekend] )flowchart TB A[观测到显著差异] -- B[计算效应大小] B -- C[计算置信区间] C -- D[混杂因素排查] D -- E{混杂影响结论?} E --|否| F[结论可靠可决策] E --|是| G[需要分层分析或重新实验] G -- H[控制混杂后重新检验] H -- C排查混杂因素的实践要点看效应是否在不同子群体一致。如果周末和非周末的效应大小差距很大说明周末效应可能是混杂。回归替代 t 检验。多因素回归可以同时控制多个混杂变量比简单分层更高效。实验设计优先于事后排查。如果条件允许用随机对照实验A/B Test从根本上消除混杂。五、总结 踩坑提醒ttest_ind默认假设两组方差相等但实际数据经常不满足SciPy 的ttest_ind默认equal_varTrue用的是同方差 t 检验。如果你的新旧算法效果确实不同新算法的方差很可能更大部分用户对新算法敏感部分不敏感。用equal_varFalse做 Welchs t-test 更稳健它不要求方差齐性。建议默认用equal_varFalse除非你有充分的先验知识确认方差一致。Cohens d 对样本分布的假设容易被忽略Cohens d 的分母是合并标准差但标准差在偏态分布下如 GMV 数据往往是右偏的会被异常值严重拉高导致 d 值偏小——明明大部分用户的提升是 30%但被 1% 的 VIP 用户 100 倍的消费额拉高了标准差d 变成了 0.3中等看起来不显著。对于偏态数据建议用 Cliffs delta 或 Mann-Whitney 的秩效应它们不依赖分布假设。置信区间的宽度直接受样本量影响不要忽略区间宽度变化本身你做了 10 天的实验95% CI 是 [18%, 32%]下界已经够支撑决策。但你发现如果只算前 5 天CI 是 [5%, 45%]——宽度巨大。这说明实验期还没跑够当前的 CI 上界可能只是暂时的等样本量继续积累CI 可能收窄到更接近真实效应的位置。在实验早期阶段不要仅凭CI 下界达标就做全量决策。从 p 值到业务决策三步走量化效应大小——p 值只说有差异效应大小告诉你差异多大。用 Cohens d 分级再翻译成业务指标的变化百分比。算置信区间——p 值是二值判断置信区间给出效应的浮动范围。决策要看区间下界够不够支撑行动。排查混杂因素——统计显著可能被其他变量干扰。分层检查效应一致性或者用回归控制混杂。这三步做完你的假设检验结论才真正能指导业务决策。别让 p 0.05 成为终点——它只是起点站的三张通行证之一另外两张效应大小和置信区间同样重要。