
两组用户的平均停留时长分别是3.2分钟和3.5分钟差了0.3分钟这个差异到底是因为产品改版真实生效了还是纯粹来自抽样波动类似的场景每天都在发生AB测试结果的显著性判断、不同方案转化率的对比、实验组和对照组指标的变化背后全都指向同一个问题——这批数据到底有没有显著差异。我刚开始做数据分析那几年遇到最多的情况不是不会调统计函数而是面对一组数据不知道应该用哪个检验方法。后来我把常用方法全部梳理了一遍总结出日常项目中出现频率最高的八类统计检验正态性检验、单样本t检验、独立样本t检验、配对样本t检验、单因素方差分析、卡方检验、Mann-Whitney U检验、Wilcoxon符号秩检验。这篇文章一方面讲清楚每个方法的使用逻辑另一方面给出可以直接复制的Python实现。多数代码基于SciPy和statsmodels注释里会说明适用条件和容易踩的坑方便直接套用。1. 先定方向再写代码统计检验的选用逻辑1.1 原假设与p值的通俗翻译统计检验整个体系本质上是一套反证法思维。我们先假设原假设H0成立——通常意味着没有差异或没有关联然后看当前数据在原假设成立的前提下出现的概率有多低。这个概率就是p值。p值小于0.05或者你设定的显著性水平意味着在原假设成立的条件下看到当前数据或者更极端情况的概率小于百分之五。普通情况下我们认为这样的小概率事件不太可能发生于是选择拒绝原假设认为差异是统计显著的。一个常见的误解是把p值理解为做错的概率或者两组差异大于实际差异的概率都不是。p值只是在说如果数据之间其实没区别你看到这种区别的可能性有多大。这事想清楚了后面解读结果就不容易跑偏。1.2 选检验的两条轴数据类型与比较目的面对一份数据先不要急着翻统计函数。先回答两个问题数据是什么类型你想回答什么问题数据类型大概是三类连续变量比如时长、金额、温度可以求均值、标准差。分类变量比如注册/未注册、男性/女性/其他只能数频次。有序变量比如满意度1到5分、等级A/B/C顺序有意义但差值不一定等距。比较目的也分几种把一组数据和某个已知标准对比比如当前系统耗时是否高于我们定的目标值。两组数据互相对比比如新页面和旧页面的点击率有没有区别。三组以上数据整体对比比如三种活动策略的收益是否一致。两个分类变量之间的独立性比如不同渠道的用户是否愿意付费。两个连续变量之间的相关性比如使用时长和付费金额有没有关联。把数据类型和目的叠在一起基本就能锁定方法。我把第三小节到第八小节要讲的八类检验按这个逻辑汇总成一张表检验目的数据类型分布前提对应方法本文章节数据是否服从正态分布一组连续变量无Shapiro-Wilk检验第3节单组均值与标准值比较一组连续变量正态或大样本单样本t检验第4节两组独立样本均值比较两组连续变量正态、方差齐独立样本t检验第4节同一组对象前后对比两组配对连续变量正态配对样本t检验第4节多组均值整体比较三组及以上连续变量正态、方差齐单因素方差分析第5节分类变量之间是否独立频数列联表期望频数不低于5卡方检验第6节两组独立数据分布比较连续或有序变量无正态要求Mann-Whitney U检验第7节配对数据差异比较配对连续或有序变量无正态要求Wilcoxon符号秩检验第7节这张表基本就是我的日常工作速查表。其中第8节还会额外补充相关性检验——它和差异比较不是一回事但工作中实在太常用我顺手也把实现写出来了。1.3 参数检验和非参数检验的路线分叉严格按照上表走第一步通常会遇到一个岔路口数据到底正不正态如果数据大致服从正态分布优先考虑参数检验比如t检验、方差分析。它们的统计功效更高对真实差异更敏感。如果数据明显偏斜、样本量很小、或者存在不少极端值就要转到非参数检验比如Mann-Whitney U和Wilcoxon符号秩检验。这类方法基于秩次而不是原始数值没那么容易被异常值带偏代价是检测真实差异的灵敏度会稍微降低。判断正态的方式不只有看——看直方图、QQ图也可以用第3节的正态性检验做定量判断。两条路结合着用比自己瞎猜强很多。2. 环境准备与样本构造没有数据就自己造一份2.1 需要装的依赖库本文的代码主要基于三个库NumPy生成随机样本、做数组运算SciPy提供绝大多数统计检验函数本文主角statsmodels补充多重比较、Tukey事后检验等功能pandas整理数据、构造列联表更顺手。安装方式很简单pip install numpy pandas scipy statsmodels建议使用3.9以上的Python环境。SciPy在1.9版本之后部分检验函数从返回元组改成了返回类似命名元组的对象取值方式稍微有变化我下文会提请注意。版本不同导致API不一致是老生常谈的坑。2.2 用随机种子造一份可复现的样本为了演示我直接生成三组实验数据模拟三个不同版本的活动页面在某指标上的表现。固定随机种子确保你复现出来的结果和我一致。import numpy as np import pandas as pd from scipy import stats np.random.seed(2024) # 模拟三组用户的行为指标比如人均点击量 group_a np.random.normal(100, 15, 30) # 均值100标准差15样本量30 group_b np.random.normal(110, 15, 30) # 均值110可以认为真实有差异 group_c np.random.normal(95, 18, 30) # 均值95标准差更大一些 data pd.DataFrame({ label: [A] * 30 [B] * 30 [C] * 30, value: np.concatenate([group_a, group_b, group_c]) })为什么要特意生成标准差不同的数据因为后面讲独立样本t检验时正好用得上方差齐性的讨论。现实数据里两组完全等方差的情况反而不多见提前把这种变量埋进去后面讲注意事项时就不用重新造数。2.3 为什么先看描述统计和可视化代码写进检验函数之前先花两分钟看一眼描述统计。这一步虽然简单却经常能发现明显问题某个组有缺失值、某个组的分布严重偏斜、甚至数据标签对错了。print(data.groupby(label)[value].describe())箱线图也是个高效工具import matplotlib.pyplot as plt data.boxplot(columnvalue, bylabel) plt.show()我见过不少人直接跳过这一步就跑t检验后来发现异常值全来自某个样本录入错误检验结果完全失真。描述统计和可视化不是研究员的例行公事它们是帮你发现脏数据的第一道防线。3. 正态性门槛决定走参数检验还是非参数路线3.1 Shapiro-Wilk检验怎么用正态性检验我日常主力用Shapiro-Wilk检验它对中小样本的表现比较可靠。它的原假设是样本来自正态分布。也就是说p值大于0.05时我们没有足够证据拒绝正态性可以继续使用参数检验。# 分别检验三个组 for label, group in data.groupby(label)[value]: w_stat, p_value stats.shapiro(group) print(f组 {label}: W{w_stat:.4f}, p{p_value:.4f})由于前面固定了随机种子只要数据没改这组的结果就是确定的。通常你会看到三组的p值都大于0.05说明用正态分布描述这些数据没有明显毛病后续的t检验和方差分析在正态前提上是站得住的。3.2 大样本陷阱与辅助判断一个容易漏掉的细节Shapiro-Wilk在样本量很大的时候会对轻微偏离正态极度敏感。当数据量到达几百上千即使分布只是稍微有点偏斜p值也往往会变得非常小导致检验频繁拒绝正态。这时候不要急着给数据判死刑。更合理的做法是配合直方图或QQ图做主观判断——如果图形上分布仍然近似钟形只有尾部有一点点歪那么大样本下的偏斜对t检验的结果影响通常有限。t检验对大样本的偏离其实有一定容忍度这依赖中心极限定理对样本均值近似正态的保证。另外提醒一点如果你用分段数据做假设检验正态性检验应该按组分别做而不是把所有数据合在一起做一次。合并后会把组间差异混进分布判断里得到的结果没有参考价值。3.3 正态性不通过时的下一步如果检验结果确实明确显示偏斜严重有几个选择使用非参数替代方法比如Mann-Whitney U检验或Wilcoxon检验对数据进行变换如取对数、开根号让变换后的数据更接近正态保留参数检验但谨慎解读当样本量足够大时t检验通常仍然不失稳健。我个人的习惯是能变换数据就变换变换之后再用一次Shapiro-Wilk确认如果变换也不行就直接上非参数检验。变换数据的好处是保留原始数据的量纲和解释性相对容易坏处是结果要回推到原始尺度时会有点绕。看你的业务场景取舍。4. t检验三兄弟均值比较的三个常用变体4.1 单样本t检验单样本t检验解决的最简单问题一组数据的均值是否等于某个参考值。举个例子你的客服平均响应时间目标是小于等于2分钟。现在采集了30个工单的响应时间想知道当前水平是否真的符合目标。原假设是总体均值等于2分钟。# 模拟一组响应时间数据均值大约在1.9左右 response_time np.random.normal(1.9, 0.4, 30) t_stat, p_value stats.ttest_1samp(response_time, popmean2.0) print(ft{t_stat:.4f}, p{p_value:.4f})这里popmean2.0就是我们的目标参考值。p值小于0.05就拒绝原假设说明当前平均响应时间不等于2分钟如果p值大于0.05说明没有足够证据认为它偏离目标。很多人会把这个检验误当成检验均值是否低于目标值严格来说这涉及单侧检验。如果你关心的是是否比目标更好或者是否比目标更差需要设置alternativeless或alternativegreater。双侧检验不问方向只问有没有差异。4.2 独立样本t检验与方差齐性独立样本t检验解决的是两组独立数据之间的均值是否存在显著差异。这里独立是关键意思是两组样本来自不同的对象A组和B组之间没有一一对应的关系。# 当前A组和B组 t_stat, p_value stats.ttest_ind(group_a, group_b) print(ft{t_stat:.4f}, p{p_value:.4f})我在2.2节特意给B组设了均值110所以这个检验大概率会给出显著的p值。这正好演示了真实差异能否被足够样本量检测出来的过程。但这里有个前提参数容易被忽略equal_var。默认情况下SciPy的ttest_ind假设两组方差相等。如果两组方差异常悬殊应该把它设置为equal_varFalse对应Welch t检验。判断方差是否齐性可以提前做一个Levene检验from scipy.stats import levene lev_stat, lev_p levene(group_a, group_b) print(fLevene检验: p{lev_p:.4f})如果Levene检验的p值小于0.05说明方差不齐跑t检验时记得用equal_varFalse。Welch t检验不假设方差相等自由度也会做相应修正在现实中通常比普通t检验更稳健我越来越多地直接用它。4.3 配对样本t检验配对t检验针对的是同一批对象的前后对比。比如记录同一批用户在使用新功能前后的使用时长或者同一批学生在培训前后的考试成绩。每个对象有两个关联的测量值不能把它们当成两组独立数据来处理。# 模拟25个用户的训练前和训练后成绩 before np.random.normal(70, 10, 25) after before np.random.normal(5, 4, 25) # 成绩平均提升了5分 t_stat, p_value stats.ttest_rel(before, after) print(ft{t_stat:.4f}, p{p_value:.4f})同学期成绩天然有正相关所以不能简单把培训前70分和培训后75分当成两组独立的人。把配对信息拿掉会低估检验的灵敏度也就是统计功效变差。ttest_rel利用的就是每对数据之间的差值信息把个体差异先削掉再做单样本的均值检验。4.4 t检验的误用边界我见过最经典的误用场景有两个。第一个是把配对数据当独立样本处理。比如同一个用户在不同周期的行为数据明明是强相关的却用ttest_ind硬跑一遍。这样做的后果是标准误被高估本来可能显著的结果变得不显著白白错过真实效果。第二个误用是忽略多重比较。组数多了之后有些人会直接拿三组数据两两之间各做一次t检验。这会产生多重比较膨胀的问题比较次数越多碰巧出现假阳性的概率越大。三组两两对比一共3次检验在显著性水平0.05下至少出现一次假阳性的概率已经接近14%这种事情后面第5节还会详细讲。5. 多组均值之间的整体比较单因素方差分析5.1 为什么不能拿三组数据两两做t检验假如有A、B、C三个组有人会直接跑三次t检验A对B、A对C、B对C。每一组比较都声称显著性水平0.05听起来每步都严谨但整体上的错误率会迅速膨胀。可以做个大概估算如果三次检验互相独立每次犯第一类错误的概率为0.05那么三次检验中至少犯一次错误的概率约为1减去0.95的三次方大约14.3%。组数越多这个值越离谱。方差分析ANOVA的价值就是用一个F检验整体回答这么多组的均值到底是否完全相等。它先把总变异拆成组间变异和组内变异两部分组间变化如果远大于组内随机波动F值就会变大对应的p值变小于是我们有证据认为至少有一组和其他组不同。5.2 f_oneway实现与解读三个组的整体比较直接一行代码f_stat, p_value stats.f_oneway(group_a, group_b, group_c) print(fF{f_stat:.4f}, p{p_value:.4f})f_oneway会输出F统计量和p值。p值小于0.05说明至少有一组均值和其他组不同但我们不知道是哪一组。这就是方差分析的作用边界它管整体不管细节。这里再多说一句做方差分析前最好还是看一下各组方差是否接近。如果组间方差差异很大f_oneway的结果可能不稳健。这种情况下可以选择Welch方差分析或者转用非参数的Kruskal-Wallis检验。后者的思路类似Mann-Whitney U检验但可以处理多组数据。5.3 事后比较与替代方案ANOVA显著之后接着就是问差异到底出在哪组之间。这时候只需要做一次多组间的两两比较但要用修正后的方法控制整体错误率。statsmodels里最常用的是Tukey HSDHonestly Significant Differencefrom statsmodels.stats.multicomp import pairwise_tukeyhsd all_values data[value].values all_labels data[label].values tukey_result pairwise_tukeyhsd(endogall_values, groupsall_labels, alpha0.05) print(tukey_result)输出会给出每两组的均值差、p值以及置信区间还自带是否拒绝原假设的判断。Tukey方法专门为整体ANOVA显著后找差异设计比盲目做多次t检验可靠得多。如果再严谨一点方差不齐时可以考虑Games-Howell事后检验不想深挖的话至少也要用Bonferroni校正把显著性水平除以比较次数。第9节会再讲这些。6. 分类变量之间的独立性卡方检验6.1 卡方检验到底在检验什么前面的方法都在处理连续变量换到分类变量时就要换思路。比如三个版本的落地页分别有注册和未注册的用户数我们关心的是版本类型和用户是否注册这两个分类变量之间存在关联吗这时把数据整理成列联表每个格子记录频数。原假设是行变量和列变量相互独立也就是注册率在不同版本之间没有差异。卡方检验的原理是把每个格子的实际频数和在独立假设下应有的期望频数做对比。实际频数偏离期望频数越远卡方统计量越大p值越小说明变量之间更可能有关联。6.2 列联表构造与chi2_contingency实操假设三个版本分别有45、35、40人注册未注册分别有55、65、60人contingency_table pd.DataFrame( { registered: [45, 35, 40], not_registered: [55, 65, 60] }, index[variant_A, variant_B, variant_C] ) from scipy.stats import chi2_contingency chi2_stat, p_value, dof, expected chi2_contingency(contingency_table) print(f卡方统计量{chi2_stat:.4f}, p{p_value:.4f}, 自由度{dof})这个例子中如果p值较大大于0.05说明注册比例在不同版本之间没有统计显著的差异如果p值小于0.05就说明版本和注册行为之间存在关联。注意expected输出的是期望频数表它有一张卡方值很大的指示作用下面马上讲。6.3 期望频数不足与Fisher精确检验使用卡方检验有一个经典限制单元格的期望频数不能太小。经验法则是所有期望频数最好大于等于5如果有超过20%的格子期望频数小于5卡方统计量的近似效果就会变差。遇到这种情况2x2列联表可以换Fisher精确检验小样本下它直接计算精确概率没有近似误差from scipy.stats import fisher_exact table_2x2 [[20, 10], [5, 30]] # 行是渠道A/渠道B列是购买/未购买 odds_ratio, p_value fisher_exact(table_2x2) print(f优势比{odds_ratio:.4f}, p{p_value:.4f})格子更多的列联表一般考虑合并类别或者用似然比卡方。我在实际项目中遇到期望频数不足时第一反应往往不是翻更复杂的检验而是去问业务方能不能把类别重新合并成有意义的粒度。类别合并本身也是一次决策闭着眼睛合并反而可能把真实信号磨掉。7. 不满足正态假设时的两条退路非参数检验7.1 Mann-Whitney U检验正态假设撑不住的时候最常用的是Mann-Whitney U检验。它用秩次代替原始数值把所有数据混在一起排大小再分别看两组的秩和是否偏离预期。u_stat, p_value stats.mannwhitneyu(group_a, group_b, alternativetwo-sided) print(fU{u_stat:.4f}, p{p_value:.4f})和t检验相比Mann-Whitney U不要求数据服从正态分布对离群值也更不敏感。如果两组数据的分布形状大致相似它可以被解释为中位数之间的差异检验。但要注意如果两组数据的分布形状差异很大显著结果可能来自形状不同而不是位置上确有差异解读时要小心。新版SciPy中mannwhitneyu返回的是对象而不是简单元组可以用u_stat.pvalue的方式取值老版本则直接解包。代码里两种写法都能兼容吗不完全兼容保险的做法是按你环境里的版本来。打印res对象看结构就行。7.2 Wilcoxon符号秩检验如果你手里是配对数据比如同一批用户的训练前后成绩而差值明显不正态那就用Wilcoxon符号秩检验。它和配对t检验逻辑相似只是把差值换成了差值的秩。w_stat, p_value stats.wilcoxon(before, after) print(fW{w_stat:.4f}, p{p_value:.4f})这个方法关注的是前后差值分布是否围绕0对称也就是干预是否产生了系统性变化。它是配对t检验最常用的非参数替代适用范围包含连续数据也可以用于有序分类数据。7.3 非参数检验的适用边界非参数检验不是万能药。它牺牲了一部分对真实差异的敏感度当数据确实服从正态分布时同样的样本量下参数检验更容易拒绝原假设。换句话说数据正常时优先用t检验数据不正常时硬用t检验又可能吃饱亏。先看分布再选路线这一步别省。我用过很多次非参数检验最常遇到的真实场景是业务指标重度右偏。比如用户停留时长总有少量用户挂着页面很久分布尾巴特别长。这种情况下取对数后可能近似正态或者直接Mann-Whitney U走起都比硬跑t检验靠谱。8. 容易漏掉的第九种相关性检验8.1 Pearson相关第3节到第7节讲的都是差异比较相关性检验回答的是另一个问题两组连续变量之间是否存在线性关联以及关联的强度。# 模拟两个相关变量 x np.random.normal(50, 10, 60) y 2 * x np.random.normal(0, 12, 60) # y随x线性增长 r_stat, p_value stats.pearsonr(x, y) print(fPearson r{r_stat:.4f}, p{p_value:.4f})r的范围在-1到1之间接近1表示强正相关接近-1表示强负相关接近0表示没有明显线性关系。p值回答的是这个相关是否显著不等于0。Pearson相关的使用前提是两变量近似正态分布且关系接近线性。数据明显偏态或存在极端离群值时一个异常点就可能把相关系数拉到很高这时候更倾向用Spearman。8.2 Spearman秩相关Spearman相关把原始值换算成秩次再做一次Pearson相关计算。它检验的是单调关系不管是线性还是非线性只要一个变量随另一个变量稳定上升或下降就能被捕捉到。rho_stat, p_value stats.spearmanr(x, y) print(fSpearman rho{rho_stat:.4f}, p{p_value:.4f})实际业务指标里很多关系并不严格线性。比如用户活跃天数和付费金额之间往往是边缘效应递减的关系线性相关可能捕捉不到Spearman却能稳定给出显著的正向关联。8.3 相关不等于因果这句话被说烂了但每次看到结果还是会有人忽略。两组变量相关可能只是因为它们共同受第三个变量影响。比如夏天冰淇淋销量和溺水人数都上升两者存在正相关但不能说冰淇淋导致溺水。在项目里解释相关性结果时我一般会额外检查业务逻辑和时间先后关系原因通常先于结果发生纯粹的横截面相关只能作为线索不能直接交给决策。要把一个相关性结论升级成因果结论需要更严格的实验设计这是比任何统计函数都难的部分。9. 踩坑经验p值之外还需要关心什么9.1 先画图再跑检验统计检验是对数据做减法可视化则是对数据做加法。一张箱线图能告诉你有很多离群点一个散点图能告诉你本来该是分组差异结果组内差异更大。这些信息在p值里不会自动体现。我现在拿到一批数据顺序永远是描述统计、箱线图或直方图、正式检验。画图花不了几分钟却能避免把脏数据直接送进检验流程以后得到一个看似精确、实则没意义的p值。9.2 样本量大时显著不等于重要p值对样本量极其敏感。样本量足够大的时候极其微小的差异也能显著。比如A组平均时长100.1秒B组100.2秒0.1秒的差异在几千样本量下完全可能算出p值小于0.05但业务上这个0.1秒基本不改变任何决策。这种时候建议算效应量。以两组均值为例Cohens d可以大致理解为两组均值差相当于多少个标准差def cohens_d(x, y): nx, ny len(x), len(y) df nx ny - 2 pooled_std np.sqrt( ((nx - 1) * np.std(x, ddof1) ** 2 (ny - 1) * np.std(y, ddof1) ** 2) / df ) return (np.mean(x) - np.mean(y)) / pooled_std d_value cohens_d(group_a, group_b) print(fCohens d {d_value:.4f})一般以0.2、0.5、0.8作为小、中、大效应的经验分界。显著性和效应量一起看结论才真正可用。9.3 多重比较校正和p-hacking的红线如果一次分析里同时做了多个检验比如对10个指标分别跑t检验就面临多重比较问题。10个指标、显著性水平0.05完全无差异的情况下出现至少一个假阳性的概率也接近40%。用statsmodels做一次简单校正from statsmodels.stats.multitest import multipletests p_values [0.03, 0.04, 0.01, 0.20, 0.45] reject, pvals_corrected, _, _ multipletests(p_values, alpha0.05, methodbonferroni) print(reject) print(pvals_corrected)Bonferroni方法简单粗暴把显著水平除以检验次数有点过于保守。更平衡的选择是FDR校正method参数换成fdr_bh即可。项目里有多个假设同时要验证的时候我会优先用FDR。除了统计方法更要紧的是做事底线。我见过有同事为了跑出好看的结果反复更换检验方法、删除离群点、直到p值小于0.05。这种操作在统计上叫p-hacking结果看起来干净实际上已经失去可重复性。做分析的底线应该是先定好检验方案和样本排除规则再动手跑数而不是先看结果再倒推方案。我自己的习惯是每次把脚本、随机种子、数据版本全部留下下次重新跑能复现才算数。