
去年有回运营同学抱着一份数据来找我三个落地页版本各跑了小半个月回收了每版 300 条左右的用户评分开门见山就问“到底哪个版本该上线”。我的第一反应不是去看均值谁高谁低而是先问了自己一句这三组数据的差异到底是版本本身带来的还是抽样波动自己折腾出来的要回答这个问题靠的就是方差分析ANOVA具体到这个场景是单因素 ANOVA如果再把流量渠道也扯进来一起看那就得用上两因素 ANOVA。这篇我就把这两种方法一次讲透计算原理、软件实操、结果解读外加一堆教科书里不爱写的坑。如果你手头也遇到“三组以上均值做比较”的问题无论是用户评分、页面停留时长、实验测量数据还是农学产量数据这篇文章可以直接当作操作手册来看。我尽量少堆公式多讲人话保证你看完能自己跑出结果也知道该怎么跟别人解释清楚。1. 为什么“三组均值不一样”不能靠两两 t 检验1.1 三组数据做了三次 t 检验错在哪先把最经典的问题放前面。很多人拿到三组数据第一反应是“A 和 B 比一比、A 和 C 比一比、B 和 C 比一比”三次 t 检验下来有显著差异的就算赢。这个做法表面看没问题实际隐患很大。每一次 t 检验都把显著性水平 α 定在 0.05也就是检验一次有 5% 的概率把“本来没差异”的判断成“有差异”统计学上叫第一类错误假阳性。可当你连续做 3 次检验的时候至少出现一次假阳性的概率就不再是 5% 了而是1 - (1 - 0.05)³ ≈ 14.3%如果你有 6 组数据两两比较的次数是 C(6,2)15 次这个概率会飙升到 1 - 0.95¹⁵ ≈ 53.7%跟抛硬币差不多了。换句话说数据里本来全是噪声你靠多次 t 检验也能“搅”出一个显著结果来。这种效应在流行病学里叫“多重比较问题”在 A/B 测试流程里叫“误差膨胀”。方差分析第一个作用就是把所有组的均值放在同一个模型里做一次全局检验让整体的第一类错误率控制在设定水平附近而不是拆成很多次比较去碰运气。1.2 ANOVA 的零假设和它想省掉的麻烦单因素 ANOVA 的零假设写出来很简单H₀μ₁ μ₂ μ₃也就是假设三个版本的总体均值完全相等。备择假设是“至少有一组的总体均值不等于其他组”注意这里是“至少有一组不同”不是说每两组都不同。这个设计的巧妙在于我先用一次检验回答“分组到底有没有用”如果有用再去做事后检验post hoc test找具体是哪几组之间有差异如果全局检验都不显著就不需要再去做两两比较自然也就绕开了多重比较误差膨胀的问题。所以每次看到“三组及以上均值比较”的需求我都会建议直接走 ANOVA而不是把 t 检验做三遍。这也解释了为什么方差分析是所有多组比较问题的“第一道大门”。2. 单因素 ANOVA 的计算逻辑组间变异除以组内变异2.1 平方和的拆分SST SSA SSE很多教材喜欢一上来给公式我换个方式讲。方差分析的核心就一句话把数据的总波动拆成“组间差异引起的波动”和“组内本来就有的波动”两部分。举个例子三组落地页的购买意愿评分每个版本看 10 个人。把 30 个分数混在一起算一个总体均值每个分数和总体均值的差叫总离差。总离差的平方和记作 SST总平方和。这个总离差其实可以拆成两段该组均值和总均值之间的差记作组间离差反映的是“版本”这个分组因素造成的影响该分数和自己组均值之间的差记作组内离差反映的是同组内部个体之间的随机波动。写成式子就是SST SSA SSE其中 SSA 是组间平方和sum of squares betweenSSE 是组内平方和sum of squares error也叫误差平方和。如果三个版本的用户评分真的因为版本不同而产生系统性差异那么 SSA 会相对大如果版本之间其实没区别只是随机噪声在捣乱那么组间离差和组内离差本质上差不多大SSA 相对 SSE 就没有明显优势。所以 ANOVA 的直觉判断标准就变成组间变异够不够大大到不能轻易用随机波动来解释。2.2 自由度、均方和 F 值直接比较 SSA 和 SSE 的大小不太公平因为它们的“规模”不一样。组间平方和只由组数决定组内平方和由样本总量决定。所以要引入自由度把平方和除以各自的自由度得到“均方”Mean Square。具体规则组间自由度k - 1k 是组数组内自由度N - kN 是总样本量总自由度N - 1。于是MSA SSA / (k - 1)MSE SSE / (N - k)F 值就是把这两个均方放在一起比F MSA / MSEF 值越大说明组间变异相对于组内变异越突出也就越有理由怀疑“各组均值相等”这个零假设。注意F 值是一个比值它天然不受样本量大小的直接干扰因为自由度已经被考虑进去了。2.3 ANOVA 表的结构F 值的归宿常见统计软件输出的 ANOVA 表大致长这样来源平方和 SS自由度 df均方 MSFp 值组间SSAk-1MSAMSA/MSEp组内SSEN-kMSE总计SSTN-1p 值代表的是在零假设成立的前提下出现当前这么大 F 值甚至更大的概率。p 值小于 0.05我们就说“组间差异在 0.05 水平下显著”也就是分组这个因素确实对均值产生了影响。到这里单因素 ANOVA 的原理部分就闭环了一组数据、一个分组变量、一个连续因变量用 F 检验判断组间均值是否相等。3. Python 跑单因素 ANOVA从数据到 Tukey 事后检验3.1 准备长格式数据在 Python 里跑 ANOVA最常用的库是 statsmodels 和 scipy。先说数据格式这是新手最容易卡住的地方所有统计模型都要求“长格式”数据也就是每一行代表一个观测样本一列是因变量一列是分组变量。这是我构造的一组模拟数据三个版本A、B、C每个版本 10 条用户购买意愿评分分数范围 1 到 10import pandas as pd df pd.DataFrame({ version: [A] * 10 [B] * 10 [C] * 10, score: [8.9, 7.6, 8.3, 9.1, 7.8, 8.6, 8.0, 8.4, 7.9, 8.5, 7.2, 8.4, 7.0, 7.8, 7.5, 6.9, 8.1, 7.3, 7.7, 7.4, 6.8, 7.6, 6.5, 7.2, 6.9, 7.8, 6.6, 7.1, 6.7, 7.4] }) print(df.groupby(version)[score].agg([count, mean, std]))运行之后你会看到A 版均值最高约 8.31B 版约 7.53C 版约 7.06各组标准差在 0.6 到 0.8 之间波动程度接近。先别急着下结论均值高低只代表样本不代表总体。下一步用 ANOVA 看差异是否显著。3.2 方差分析与结果解读直接用 statsmodels 的 OLS 配合 anova_lm 来算from statsmodels.formula.api import ols from statsmodels.stats.anova import anova_lm model ols(score ~ C(version), datadf).fit() anova_table anova_lm(model) print(anova_table)输出结果类似这样来源dfsum_sqmean_sqFPR(F)C(version)27.973.9918.30.001Residual275.890.22这里 p 值远小于 0.05结论是三个版本的总体均值不全相等版本因素显著影响用户评分。但“不全相等”到底是谁和谁不相等A 和 B 差 0.78A 和 C 差 1.25B 和 C 差 0.47这三个差值哪些是真实的、哪些可能是噪声这就轮到事后检验出场了。3.3 Tukey HSDANOVA 显著之后到底谁和谁不同事后检验有很多种最常用的是 Tukey HSDHonestly Significant Difference。它的好处是控制了所有两两比较的整体错误率同时比 Bonferroni 校正更不保守适合“所有组两两之间都想比一比”的场景。from statsmodels.stats.multicomp import pairwise_tukeyhsd tukey pairwise_tukeyhsd(endogdf[score], groupsdf[version], alpha0.05) print(tukey)结果核心信息A vs B均值差约 0.78p 值小于 0.05显著A vs C均值差约 1.25p 值小于 0.001显著B vs C均值差约 0.47p 值大于 0.05不显著。这个案例特别适合说明一个常见误解ANOVA 显著不代表每组两两之间都显著。B 和 C 之间虽然从均值数字上看 B 比 C 高了 0.47 分但真实差异不足以排除抽样波动的可能。所以最后的业务结论是A 版显著优于 B 版和 C 版B 版和 C 版之间没有明显差异建议直接上 A 版。3.4 效应量别只盯 p 值p 值回答“是否有差异”效应量回答“差异有多大”。在单因素 ANOVA 里最常用的是 eta-squaredη²η² SSA / SST用上面的结果就是 7.97 / (7.97 5.89) ≈ 0.575意味着“版本”这个因素解释了约 57.5% 的评分变异。这是一个很大的效应量。业务汇报时我一般会把 p 值和效应量一起说“版本对评分有显著影响F(2,27)18.3, p0.001, η²0.575其中 A 版显著优于另外两个版本”。这样既回答了“是否显著”也回答了“影响多大”比单扔一个 p 值有信息量得多。4. 两因素 ANOVA主效应与交互效应的实战拆解4.1 版本之外再加流量渠道分析问题变成三个单因素 ANOVA 只处理一个分组因素可实际业务里影响结果的因素往往不止一个。还是刚才的例子落地页版本有 A、B、C 三个用户来源渠道有搜索、广告、社交三个。这时候我们关心的问题就从“版本有没有差异”变成三个版本主效应不同版本之间评分有没有显著差异渠道主效应不同流量渠道之间评分有没有显著差异交互效应版本和渠道是不是“搭配着起作用”交互效应是两因素 ANOVA 区别于单因素的关键看点。它回答的问题是某个版本是否只在特定渠道下才表现好而不是在所有渠道下都一致地好。我构造一个 3×3 的模拟示例每组 10 个样本总样本量 90。数据用 NumPy 生成并固定随机种子保证可复现import numpy as np import pandas as pd np.random.seed(2024) version np.repeat([A, B, C], 30) channel np.tile(np.repeat([search, ad, social], 10), 3) base {A: 8.2, B: 7.6, C: 7.1} interaction { (A, search): 0.2, (A, ad): -0.2, (A, social): 0.0, (B, search): -0.4, (B, ad): 0.5, (B, social): -0.1, (C, search): 0.0, (C, ad): -0.1, (C, social): 0.1, } score np.array([ base[v] interaction[(v, c)] np.random.normal(0, 0.6) for v, c in zip(version, channel) ]) df2 pd.DataFrame({version: version, channel: channel, score: score})先看各分组均值用透视表print(df2.pivot_table(indexversion, columnschannel, valuesscore, aggfuncmean))大致会得到下面这样的格局版本搜索广告社交A8.48.08.2B7.28.17.5C7.17.07.2注意看 B 版在搜索渠道只有约 7.2在广告渠道却到了 8.1比 A 版还略高一点。这就是交互效应的直观体现B 版不是全面差只是在某些渠道下差。4.2 Python 实现两因素 ANOVA用 statsmodels 的 ols 构建包含交互项的模型from statsmodels.stats.anova import anova_lm model2 ols(score ~ C(version) * C(channel), datadf2).fit() anova2 anova_lm(model2, typ2) print(anova2)typ2 表示使用 Type II 平方和。平衡设计下 Type I、II、III 结果几乎一致但为了严谨我习惯直接写 typ2。输出表里会看到三行核心结果C(version)版本主效应F 值显著C(channel)渠道主效应p 值通常不显著C(version):C(channel)交互效应p 值显著。这里我先提醒一句交互效应显著的情况下要非常小心地解读主效应。比如版本主效应显著只能说明“平均来看 A 最好”但这掩盖了“在广告渠道B 与 A 几乎打平”这个事实。如果业务方只听到“A 最好”就直接放弃 B 在广告渠道的投放那这个分析结论就是有误导性的。4.3 交互效应显著时怎么读表交互效应显著时正确的做法是把数据按其中一个因素拆开再分别看另一个因素。比如按渠道拆开在每个渠道内部单独跑单因素 ANOVA看看版本的差异方向是否一致。这个操作叫简单效应分析simple effects analysis。代码思路是这样for ch in df2[channel].unique(): sub df2[df2[channel] ch] model_sub ols(score ~ C(version), datasub).fit() table_sub anova_lm(model_sub) print(f渠道: {ch}) print(table_sub) print(---)结果会告诉你在搜索渠道版本差异非常显著A 遥遥领先在广告渠道版本差异可能变得不显著B 甚至略高于 A在社交渠道版本差异也存在但幅度比搜索渠道小。这组结果才是对业务真正有用的信息。上线策略不再是简单的“全部切 A”而是“搜索和社交渠道主推 A广告渠道可以继续测试 B 或做分渠道实验”。交互效应把“哪个版本更好”从一个一刀切问题变成了“哪个版本在哪个场景下更好”的精细化问题。4.4 简单效应分析把复杂结论拆给业务看再强调一遍简单效应分析并不是只拿这几个分组 p 值就完事了。如果要做严格的统计推断特别是写论文或做正式报告需要对简单效应做多重比较校正或者使用估计边际均值estimated marginal means做后续检验。业务探索阶段先画交互图再拆组能把大部分关键结论看清楚。画交互图的思路是横轴是渠道纵轴是评分均值不同版本用不同颜色或线型连起来import seaborn as sns import matplotlib.pyplot as plt sns.pointplot(datadf2, xchannel, yscore, hueversion, dodgeTrue) plt.show()如果几条线交叉明显交互效应就大概率显著如果几条线平行则交互效应基本可以忽略。这个图比任何数字都直观我每次分析前都会先画一张。5. 先确认假设再下结论正态性、方差齐性与独立性5.1 三条基本假设分别由谁保证ANOVA 不是无条件的。它有三条基本假设任何一条崩溃都可能导致 F 值和 p 值失真。第一条是独立性。观测之间要相互独立这个人打了 8 分不影响另一个人打几分。独立性主要由实验设计保证比如随机分配用户到不同版本、同一用户只进入一个组。如果同一批用户反复打分那数据就不是独立的普通 ANOVA 不适用。第二条是正态性。各组数据近似服从正态分布或者说残差近似正态。样本量比较大的时候中心极限定理可以帮忙每组只有几个样本时正态性就很重要。第三条是方差齐性。各组的总体方差大致相等。如果一组方差是另一组的几倍那么 F 检验会被方差大的组带偏显著结果可能不可靠。5.2 正态性与方差齐性的检验用 Python 做检验很简单。正态性用 Shapiro-Wilk 检验分组件逐个测from scipy import stats for v in df[version].unique(): stat, p stats.shapiro(df.loc[df[version] v, score]) print(f{v}: W {stat:.4f}, p {p:.4f})方差齐性用 Levene 检验group_data [df.loc[df[version] v, score].values for v in df[version].unique()] stat, p stats.levene(*group_data) print(fLevene: W {stat:.4f}, p {p:.4f})判断标准是p 值大于 0.05不拒绝原假设可以认为满足正态性和方差齐性。我一般还会看一眼箱线图或 Q-Q 图因为统计检验本身对样本量很敏感样本一大很容易测出“统计显著的非正态”虽然实际偏离程度并不影响结论。5.3 数据不满足假设时有哪些退路如果正态性、方差齐性明显不满足有几种替代方案第一种是 Welch ANOVA它不要求方差齐性是普通 ANOVA 的稳健替代。scipy 的 f_oneway 带上 equal_varFalse 就是 Welch 版本from scipy import stats a df.loc[df[version] A, score] b df.loc[df[version] B, score] c df.loc[df[version] C, score] stats.f_oneway(a, b, c, equal_varFalse)第二种是 Kruskal-Wallis 检验非参数方法不要求正态性也不要求方差齐性比较的是各组的中位数或分布位置stats.kruskal(a, b, c)事后检验可以配合 Dunn 检验用 scikit-posthocs 包做 Bonferroni 校正# pip install scikit-posthocs import scikit_posthocs as sp sp.posthoc_dunn(df, val_colscore, group_colversion, p_adjustbonferroni)第三种是对数据进行变换比如 Box-Cox 变换把偏态数据尽量掰成近似正态然后再做 ANOVA。实际业务里评分型数据一般用 Welch 或 Kruskal-Wallis 就能解决问题不必过度纠结。6. 两年跑 ANOVA 踩过的坑按出现频率排序6.1 不平衡数据下Type I/II/III 平方和选错三组样本量完全一样时Type I、II、III 平方和的结果几乎没有差别所以很多人根本不知道这类问题。但现实数据经常是不平衡的比如广告渠道用户多、社交渠道用户少各组样本量差一倍。这时候 statsmodels 的 anova_lm 默认用 Type I结果会依赖因素进入模型的顺序先放 version 和先放 channel 可能得到不同结论。我的建议是探索性分析直接用 typ2如果交互项显著且你要解释主效应再考虑 typ3但更稳妥的做法是绕开主效应直接做简单效应的分组分析别跟平方和类型死磕。6.2 把重复测量数据当成组间两因素两因素 ANOVA 的另一个高频坑是我见过有人拿“同一批用户的多次测量结果”直接塞进 ols 模型。比如 30 个用户每个用户同时体验了 A、B、C 三个版本记录了三个评分。这种情况下同一个人的三个评分根本不独立直接做普通两因素 ANOVA 自由度全错p 值大概率虚低。这种数据对应的是重复测量设计或被试内设计需要用混合效应模型mixed effects model或专门的重复测量 ANOVA。判断标准就一条数据里有没有“同一个体出现在两个及以上条件组中”有就不要用普通组间 ANOVA。6.3 转化率直接塞进 ANOVA还有一次我看到同事把转化率点击/曝光这种二分类结果按渠道算了一堆数值直接做了 ANOVA。严格来说转化率是二项分布数据不是连续正态数据直接用 ANOVA 会违反假设。更合理的方法是做卡方检验、逻辑回归或对比例数据做合适的转换。不是所有数字都能当因变量。ANOVA 的因变量最好是连续的、近似正态的测量值评分、时长、金额、产量等。遇到 0/1 转化数据先买张票去逻辑回归那边别硬挤方差分析。6.4 事后检验不校正等于白做回到文章开头的问题。即使 ANOVA 显著也不能直接拿 LSD 两两 t 检验的结果交差尤其当组数多于三组时。LSD 完全不控制多重比较结论容易虚胖。我的默认选择是 Tukey HSD如果实验设计里有明确对照组用 Dunnett如果比较次数少且想保守一点用 Bonferroni。反正记住一句话组数一多事后检验必须校正别裸奔。6.5 把“统计显著”当成“业务显著”最后这个坑几乎人人都踩过。p 值小于 0.05 只说明这个差异不太像随机波动不代表这个差异在实际业务里一定值得上线。样本量足够大时0.2 分的评分差异也可能统计显著但运营同学看完会说“这 0.2 分有什么意义”所以我做总结时一定会看效应量和实际业务量级。A 版比 B 版高 0.78 分如果这 0.78 分对应的是更高的下单意愿和复购率那就有上线的价值如果只是问卷评分转化漏斗下游根本接不住这个差异那统计显著也只能先放一放。最后分享一个小习惯我现在拿到任何多组比较需求第一步永远是画图箱线图看分布、均值图看趋势、交互图看组合效应全部看完才跑模型。好的统计结论永远建立在对数据的直觉理解之上ANOVA 只是给这个直觉提供一个不心虚的数学依据。这个习惯帮我避免了很多“结果显著但方向离谱”的尴尬希望你也能用上。