统计分析与假设检验:从AB测试到因果推断的落地实践
2026/6/19 17:52:07 网站建设 项目流程

统计分析与假设检验:从AB测试到因果推断的落地实践

一、统计显著不等于业务显著

数据分析里最容易踩的坑,就是把统计显著性直接等同于业务价值。

举个例子:AB测试显示实验组转化率从3.2%提升到3.5%,P值0.03,统计上显著。但绝对提升只有0.3%,在日均10万流量的场景下,只多了300个转化。这点收益可能连实验的开发成本都覆盖不了。

还有个更隐蔽的问题:多重比较。同时测20个指标,就算实际上没差异,按0.05的显著性水平,平均也会有1个指标"碰巧"显著。不做多重比较校正,决策就会被虚假信号带偏。

假设检验不是"P值小于0.05就拒绝原假设"那么简单,它是一套完整的统计推理流程。下面从AB测试的实际场景出发,讲讲假设检验的方法论,再延伸到因果推断的工程实践。

二、从业务问题到统计假设

2.1 完整的检验链路

做假设检验,要把模糊的业务问题转成精确的统计假设,选对检验方法,最后再把统计结论翻译回业务语言。

graph TB subgraph 问题定义 BIZ[业务问题<br/>新方案是否提升转化率?] --> H0[原假设H0<br/>两组转化率无差异] BIZ --> H1[备择假设H1<br/>实验组转化率更高] end subgraph 实验设计 H0 --> SAMPLE[样本量计算<br/>基于MDE和统计功效] SAMPLE --> RANDOM[随机分组<br/>确保组间可比性] RANDOM --> SRM[SRM校验<br/>检测分组偏差] end subgraph 统计检验 SRM --> TEST[选择检验方法<br/>比例检验/均值检验/非参数检验] TEST --> ASSUMPTION[前提假设检验<br/>正态性/方差齐性/独立性] ASSUMPTION --> RESULT[计算检验统计量与P值] end subgraph 结论解读 RESULT --> EFFECT[效应量估计<br/>差异大小与置信区间] RESULT --> POWER[统计功效回顾<br/>是否足以检测到效应] EFFECT --> DECISION[业务决策<br/>综合统计与业务判断] end

2.2 样本量计算

样本量不足是AB测试失败的头号原因。样本量取决于三个参数:最小可检测效应(MDE)、显著性水平(α)和统计功效(1-β)。MDE越小、α越小、功效越高,需要的样本量就越大。业务场景中,MDE应该由业务方根据ROI来确定,而不是随便拍个数字。

三、假设检验的工程实现

"""统计假设检验工程化框架""" import math from dataclasses import dataclass from enum import Enum from typing import Optional import numpy as np from scipy import stats class TestType(Enum): """检验类型""" TWO_SIDED = "two_sided" ONE_SIDED_GREATER = "one_sided_greater" ONE_SIDED_LESS = "one_sided_less" class MetricType(Enum): """指标类型""" RATE = "rate" # 比率型指标(转化率、点击率) CONTINUOUS = "continuous" # 连续型指标(客单价、停留时长) COUNT = "count" # 计数型指标(订单数、点击数) @dataclass class ABTestConfig: """AB测试配置""" metric_name: str metric_type: MetricType test_type: TestType alpha: float = 0.05 # 显著性水平 power: float = 0.8 # 统计功效 mde: float = 0.01 # 最小可检测效应(绝对值) baseline_value: float = 0.0 # 基线值 @dataclass class ABTestResult: """AB测试结果""" control_mean: float treatment_mean: float control_std: float treatment_std: float control_size: int treatment_size: int test_statistic: float p_value: float effect_size: float # Cohen's d 或 差异比例 ci_lower: float # 置信区间下界 ci_upper: float # 置信区间上界 is_significant: bool power_achieved: float # 实际达到的统计功效 class ABTestEngine: """AB测试统计引擎""" def calculate_sample_size(self, config: ABTestConfig) -> int: """计算所需样本量""" alpha = config.alpha power = config.power mde = config.mde if config.test_type == TestType.TWO_SIDED: z_alpha = stats.norm.ppf(1 - alpha / 2) else: z_alpha = stats.norm.ppf(1 - alpha) z_beta = stats.norm.ppf(power) if config.metric_type == MetricType.RATE: # 比率型指标:基于正态近似 p = config.baseline_value delta = mde # 每组样本量公式 n = ((z_alpha * math.sqrt(2 * p * (1 - p)) + z_beta * math.sqrt(p * (1 - p) + (p + delta) * (1 - p - delta))) / delta) ** 2 else: # 连续型指标:基于效应量 # 假设标准差已知或可估计 sigma = config.baseline_value # 此处简化处理 n = 2 * ((z_alpha + z_beta) * sigma / mde) ** 2 return math.ceil(n) def run_test( self, control: np.ndarray, treatment: np.ndarray, config: ABTestConfig, ) -> ABTestResult: """执行AB测试""" ctrl_mean = np.mean(control) treat_mean = np.mean(treatment) ctrl_std = np.std(control, ddof=1) treat_std = np.std(treatment, ddof=1) n_ctrl = len(control) n_treat = len(treatment) # 根据指标类型选择检验方法 if config.metric_type == MetricType.RATE: stat, p_val = self._proportion_test( control, treatment, config.test_type ) else: # 先检验方差齐性 _, var_p = stats.levene(control, treatment) equal_var = var_p > 0.05 stat, p_val = stats.ttest_ind( control, treatment, equal_var=equal_var, alternative=config.test_type.value if config.test_type != TestType.TWO_SIDED else "two-sided", ) # 计算效应量 if config.metric_type == MetricType.RATE: effect_size = treat_mean - ctrl_mean else: # Cohen's d pooled_std = math.sqrt( (ctrl_std ** 2 + treat_std ** 2) / 2 ) effect_size = (treat_mean - ctrl_mean) / pooled_std if pooled_std > 0 else 0 # 计算置信区间 diff = treat_mean - ctrl_mean se = math.sqrt( ctrl_std ** 2 / n_ctrl + treat_std ** 2 / n_treat ) if config.test_type == TestType.TWO_SIDED: z_crit = stats.norm.ppf(1 - config.alpha / 2) else: z_crit = stats.norm.ppf(1 - config.alpha) ci_lower = diff - z_crit * se ci_upper = diff + z_crit * se # 判断显著性 is_significant = p_val < config.alpha # 计算实际统计功效 power_achieved = self._calculate_power( n_ctrl, n_treat, effect_size, config.alpha, config.test_type ) return ABTestResult( control_mean=ctrl_mean, treatment_mean=treat_mean, control_std=ctrl_std, treatment_std=treat_std, control_size=n_ctrl, treatment_size=n_treat, test_statistic=stat, p_value=p_val, effect_size=effect_size, ci_lower=ci_lower, ci_upper=ci_upper, is_significant=is_significant, power_achieved=power_achieved, ) def _proportion_test( self, control: np.ndarray, treatment: np.ndarray, test_type: TestType, ) -> tuple[float, float]: """比率型指标的两样本比例检验""" p1 = np.mean(treatment) p2 = np.mean(control) n1 = len(treatment) n2 = len(control) # 合并比例 p_pool = (np.sum(treatment) + np.sum(control)) / (n1 + n2) se = math.sqrt(p_pool * (1 - p_pool) * (1 / n1 + 1 / n2)) if se == 0: return 0.0, 1.0 z = (p1 - p2) / se if test_type == TestType.TWO_SIDED: p_val = 2 * (1 - stats.norm.cdf(abs(z))) elif test_type == TestType.ONE_SIDED_GREATER: p_val = 1 - stats.norm.cdf(z) else: p_val = stats.norm.cdf(z) return z, p_val def _calculate_power( self, n1: int, n2: int, effect_size: float, alpha: float, test_type: TestType, ) -> float: """计算实际统计功效""" if effect_size == 0: return alpha # 基于非中心t分布计算功效 ncp = effect_size * math.sqrt(n1 * n2 / (n1 + n2)) if test_type == TestType.TWO_SIDED: t_crit = stats.t.ppf(1 - alpha / 2, df=n1 + n2 - 2) power = (1 - stats.nct.cdf(t_crit, df=n1 + n2 - 2, nc=ncp) + stats.nct.cdf(-t_crit, df=n1 + n2 - 2, nc=ncp)) else: t_crit = stats.t.ppf(1 - alpha, df=n1 + n2 - 2) power = 1 - stats.nct.cdf(t_crit, df=n1 + n2 - 2, nc=ncp) return float(power) def multiple_comparison_correction( self, p_values: list[float], method: str = "bonferroni", ) -> list[float]: """多重比较校正""" if method == "bonferroni": # Bonferroni校正:最保守,控制FWER n = len(p_values) return [min(p * n, 1.0) for p in p_values] elif method == "bh": # Benjamini-Hochberg校正:控制FDR n = len(p_values) indexed = sorted(enumerate(p_values), key=lambda x: x[1]) adjusted = [0.0] * n for rank, (idx, p) in enumerate(indexed, 1): adjusted[idx] = min(p * n / rank, 1.0) # 确保单调性 for i in range(len(indexed) - 2, -1, -1): idx_curr = indexed[i][0] idx_next = indexed[i + 1][0] adjusted[idx_curr] = min(adjusted[idx_curr], adjusted[idx_next]) return adjusted else: return p_values

四、假设检验的局限与误用

4.1 P值的误读

P值是最容易被误读的统计量。P值=0.03的意思是"如果原假设为真,观察到当前或更极端结果的概率是3%",而不是"原假设为假的概率是97%"。这个区别在决策中影响很大——P值只衡量数据与原假设的不兼容程度,不直接衡量效应的大小或方向。

更实用的做法是同时报告效应量和置信区间。效应量告诉你差异有多大,置信区间告诉你估计的精度,两者结合才能支撑业务决策。

4.2 前提假设

每种检验方法都有前提假设:t检验假设正态分布和方差齐性,比例检验假设样本量足够大以适用正态近似,独立性假设要求样本之间互不影响。这些假设被违反时,结果可能不可靠。

应对策略:对正态性假设,用Shapiro-Wilk检验或QQ图验证,不满足时改用非参数检验(Mann-Whitney U检验);对方差齐性假设,用Welch t检验替代;对独立性假设,需要从实验设计层面保证随机分组。

4.3 不适合的场景

以下情况不建议用经典假设检验:

  • 样本量极小(<30):正态近似不成立,需要用精确检验(如Fisher精确检验)
  • 存在干扰因子:组间差异可能由混杂变量导致,需要因果推断方法而非简单假设检验
  • 序贯实验:边收集数据边检验会导致P值膨胀,需要用序贯检验方法

五、总结

假设检验是数据分析中从"观察"到"推断"的关键环节,但需要严格的工程约束才能可靠使用。样本量计算确保实验有足够的检测能力,前提假设检验确保方法选择的正确性,效应量和置信区间确保结论的业务可解释性,多重比较校正确保多指标测试的可靠性。

落地建议:先建立标准化的AB测试流程——从MDE确定、样本量计算、随机分组、SRM校验到统计检验和结论解读,每一步都有明确的规范和检查点。再逐步引入更高级的方法:方差齐性检验与Welch修正、非参数检验替代方案、多重比较校正策略。最后在业务需求驱动下探索因果推断方法,处理观察性数据中的混杂变量问题。统计方法的价值不在于给出"显著/不显著"的二值判断,而在于量化不确定性,让决策基于证据而非直觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询