A/B 测试实战:从实验设计到统计推断的工程落地
A/B 测试实战从实验设计到统计推断的工程落地为什么 A/B 测试经常白做A/B 测试是数据驱动决策的基础但实际落地时很多测试要么得不出结论要么结论根本靠不住。常见问题包括样本量不够导致统计功效低有差异也测不出来、多个实验同时跑导致互相干扰辛普森悖论、还没跑完就提前停止偷窥问题导致假阳性。举个具体场景产品团队想验证“新按钮颜色能否提升点击率”。实验跑了一周新版本点击率从 3.2% 升到 3.5%p 值 0.04——看着挺显著。但仔细一看实验只覆盖了 2000 个用户统计功效只有 35%。这意味着就算真实差异存在也有 65% 的概率测不出来。这个“显著”结果大概率只是噪声。A/B 测试的关键在于从设计到推断的每一步都得经得起推敲。统计框架与实验设计A/B 测试主要分三步实验设计定样本量和分流、数据采集确保测量无偏、统计推断控制两类错误。任何一步出错结论都不可信。flowchart TB A[实验目标定义] -- B[效应量预估] B -- C[样本量计算] C -- D[分流策略设计] D -- D1[随机分流] D -- D2[分层分流] D -- D3[时间片轮转] D -- E[实验执行与监控] E -- E1[SRM 检验: 分流均衡性] E -- E2[指标监控: 中期检查] E -- E3[干扰检测: 实验污染] E -- F[统计推断] F -- F1[假设检验: p 值与置信区间] F -- F2[效应量估计: 点估计与区间估计] F -- F3[多重比较校正] F1 -- G[决策] F2 -- G F3 -- G样本量计算计算样本量主要看四个参数基线转化率p₁、最小可检测效应MDE、显著性水平α、统计功效1-β。MDE 最关键——它决定了“我们想测出多小的差异”。MDE 设太小样本量需求会爆炸设太大可能漏掉有价值的提升。分流策略简单随机分流在样本量有限时容易出现组间分布不均比如实验组里高端用户特别多。分层分流Stratified Randomization先按关键维度地区、用户等级分层再在每层内随机分流能保证组间分布一致。统计推断第一类错误假阳性没差异却测出显著由 α 控制。第二类错误假阴性有差异却没测出来由 β 控制。A/B 测试必须同时控制这两类错误不能光看 p 值。代码实现样本量计算import math from dataclasses import dataclass dataclass class SampleSizeResult: sample_per_group: int total_sample: int mde: float baseline_rate: float alpha: float power: float method: str class SampleSizeCalculator: def calculate_proportion(self, baseline_rate: float, mde: float, alpha: float 0.05, power: float 0.8) - SampleSizeResult: 比例类指标的样本量计算如转化率、点击率 if not (0 baseline_rate 1): raise ValueError(f基线率必须在 (0,1) 之间当前值: {baseline_rate}) if mde 0: raise ValueError(fMDE 必须大于 0当前值: {mde}) p1 baseline_rate p2 baseline_rate * (1 mde) if p2 1: raise ValueError(f目标率 {p2:.4f} 超过 1请减小 MDE) z_alpha self._z_critical(1 - alpha / 2) z_beta self._z_critical(power) p_bar (p1 p2) / 2 numerator ( z_alpha * math.sqrt(2 * p_bar * (1 - p_bar)) z_beta * math.sqrt(p1 * (1 - p1) p2 * (1 - p2)) ) ** 2 denominator (p2 - p1) ** 2 n_per_group math.ceil(numerator / denominator) return SampleSizeResult( sample_per_groupn_per_group, total_samplen_per_group * 2, mdemde, baseline_ratebaseline_rate, alphaalpha, powerpower, method两比例 Z 检验, ) staticmethod def _z_critical(cumulative_prob: float) - float: from scipy.stats import norm return norm.ppf(cumulative_prob)实验分流与 SRM 检验import hashlib import numpy as np from scipy.stats import chi2_contingency class ExperimentSplitter: def simple_split(self, user_ids: list[str], control_ratio: float 0.5) - dict: groups {control: [], treatment: []} for uid in user_ids: hash_val int( hashlib.md5(uid.encode()).hexdigest(), 16 ) bucket (hash_val % 10000) / 10000.0 if bucket control_ratio: groups[control].append(uid) else: groups[treatment].append(uid) return groups def stratified_split(self, df, user_col: str, stratify_col: str, control_ratio: float 0.5) - dict: groups {control: [], treatment: []} for stratum_value, stratum_df in df.groupby(stratify_col): user_ids stratum_df[user_col].tolist() stratum_groups self.simple_split(user_ids, control_ratio) groups[control].extend(stratum_groups[control]) groups[treatment].extend(stratum_groups[treatment]) return groups class SRMChecker: def check(self, control_count: int, treatment_count: int, expected_ratio: float 0.5, alpha: float 0.01) - dict: total control_count treatment_count expected_control total * expected_ratio expected_treatment total * (1 - expected_ratio) chi2, p_value, dof, _ chi2_contingency( np.array([ [control_count, treatment_count], [expected_control, expected_treatment] ]), correctionFalse, ) is_srm p_value alpha return { is_srm_detected: is_srm, p_value: round(p_value, 6), chi2_statistic: round(chi2, 4), control_count: control_count, treatment_count: treatment_count, observed_ratio: round(control_count / total, 4), expected_ratio: expected_ratio, recommendation: ( 分流正常可继续分析 if not is_srm else 检测到分流不均衡请排查分流逻辑或数据采集问题 ), }统计推断与效应量估计from scipy.stats import norm, ttest_ind import statsmodels.stats.proportion as proportion class ABTestAnalyzer: def analyze_proportion(self, control_success: int, control_total: int, treatment_success: int, treatment_total: int, alpha: float 0.05) - dict: p_control control_success / control_total p_treatment treatment_success / treatment_total z_stat, p_value proportion.proportions_ztest( count[treatment_success, control_success], nobs[treatment_total, control_total], alternativetwo-sided, ) ci_low, ci_high proportion.confint_proportions_2indep( count1treatment_success, nobs1treatment_total, count2control_success, nobs2control_total, alphaalpha, methodwald, ) relative_lift (p_treatment - p_control) / p_control return { is_significant: p_value alpha, p_value: round(p_value, 6), z_statistic: round(z_stat, 4), control_rate: round(p_control, 6), treatment_rate: round(p_treatment, 6), absolute_diff: round(p_treatment - p_control, 6), relative_lift: round(relative_lift, 4), ci_95: (round(ci_low, 6), round(ci_high, 6)), conclusion: self._make_conclusion( p_value, alpha, relative_lift ), } def _make_conclusion(self, p_value: float, alpha: float, lift: float) - str: if p_value alpha: return ( f未检测到显著差异p{p_value:.4f} f不能拒绝原假设。建议检查统计功效是否充足。 ) direction 正向 if lift 0 else 负向 return ( f检测到{direction}显著差异p{p_value:.4f} f相对提升 {lift:.2%}。 )权衡与边界维度固定时长测试序贯测试Sequential样本量需求固定提前计算可提前停止平均节省 20%–30% 样本假阳性控制严格α 即为实际水平需要校正否则假阳性膨胀实施复杂度低高需持续监控边界适用场景样本量充足时间不紧迫样本量有限需快速决策权衡一MDE 的设定。MDE 设太小样本量可能超出流量承载能力设太大可能漏掉有业务价值的小幅提升。建议把 MDE 定为“最小业务意义差异”——即对业务决策有实际影响的最小变化。权衡二多重比较的校正。同时测多个指标时假阳性率会随指标数量膨胀。5 个指标在 α0.05 下的整体假阳性率约为 23%。建议用 Bonferroni 或 BH 校正控制族错误率。权衡三实验时长与周期效应。时长过短可能覆盖不了完整的用户行为周期比如周末和工作日差异。建议实验时长至少覆盖一个完整周期通常 1–2 周。落地建议A/B 测试的核心是“先设计再执行先检验再推断”。样本量计算保障统计功效分流策略保障组间可比性SRM 检验保障数据质量统计推断控制两类错误——每一步都是结论可信的前提。具体落地可以分三步建立样本量计算模板确保每个实验启动前都有明确的功效保障。实现分流与 SRM 检验自动化实验启动后立即验证分流均衡性。构建统计推断工具包统一团队的分析方法和结论标准。一个结论不可靠的 A/B 测试比不做测试更危险。