ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python独立双样本t检验与z检验:场景选择、代码实现与避坑指南

Python独立双样本t检验与z检验:场景选择、代码实现与避坑指南 做Python数据分析的人十有八九都遇到过这种需求手里有两组数据可能是A/B测试跑出来的两个版本可能是两个门店同一时期的营业额也可能是两种治疗方案下患者的指标变化业务方丢给你一句话“帮我看看这两个到底有没有差别。”这时候独立双样本t检验和z检验就是你最常用的两把枪。网上的教程不少但大部分要么只给公式要么只给代码很少有人把“什么时候用哪个”和“为什么”讲透结果不少人跑完scipy.stats.ttest_ind连p值是单尾还是双尾都说不清楚。这篇内容我会从统计原理讲到Python实操把独立双样本t检验和z检验的适用场景、判断逻辑、代码实现和结果解读一次讲清楚顺手把我踩过的坑也一起交代了适合刚入门数据分析、或者用过统计库但一直没搞懂背后原理的读者。1. 先想明白独立双样本检验到底在解决什么问题1.1 独立两个字是整个检验的前提很多人跑假设检验翻车第一个坑就栽在“独立”上。所谓独立双样本意思是两个样本的观测值之间没有对应关系、互不影响通常来自两个不同的总体或两个不同的实验处理。比如把100个用户随机分成两组一组看A方案一组看B方案两组用户的行为可以被视为相互独立再比如拿南方城市和北方城市的门店营业额做比较只要不是同一批门店在不同时期的重复测量也算独立。最典型的反例是“前后测”同一批人在干预前测一次血压干预之后再测一次。这两次测量来自同一个人高度相关如果当成两组独立样本跑ttest_ind看起来很爽样本量直接翻倍但实际上配对信息被你丢掉了。正确做法是用scipy.stats.ttest_rel做配对检验。我见过有人把一份配对数据跑成独立t检验p值刚好卡在0.05附近报告都交了才发现实验设计根本不是独立样本结论直接作废。记住一句话能不能把数据拆成“独立的两组”由实验设计决定不由你的Excel表格怎么摆决定。1.2 t检验和z检验的核心区别方差已知还是未知区分t检验和z检验最本质的一条就是总体的方差到底是不是已知的。z检验的统计量长这样z (x̄₁ - x̄₂) / sqrt(σ₁²/n₁ σ₂²/n₂)里面用的是总体标准差σ。问题是现实工作中几乎不可能把总体标准差精确拿到手你手里通常只有样本标准差s所以你得用s去估计σ于是统计量变成t (x̄₁ - x̄₂) / sqrt(s₁²/n₁ s₂²/n₂)。用s估计σ会带来额外的“不确定性”sampling distribution不再严格服从标准正态分布而是服从t分布。t分布和正态分布的区别有点像你拿一把没标定过的尺子量东西正态分布假设你用的是标准尺误差边界是确定的t分布则承认你手里的尺子本身有误差而且样本越少你对这把尺子的信心就越低分布尾巴就会越厚需要更大的差异才能说“显著”。随着自由度增大s对σ的估计越来越准t分布越来越接近正态分布这也就是为什么自由度一大t检验和z检验结果几乎一样。1.3 大样本就能随便用z检验现实里别太教条很多人的记忆口诀是“样本量大于30用z检验小于30用t检验”。这个经验法则有一定道理但它的成立前提是中心极限定理样本量够大时即使原始数据不服从正态分布样本均值也近似服从正态分布此时用样本方差替代总体方差引入的误差可以被忽略。但“30”不是万能门槛如果数据严重偏态、重尾或者样本量差距悬殊30远远不够需要更大的样本才能撑起正态近似。实际操作中还有一个常见场景需要注意比例检验比如两个版本的转化率对比。比例检验通常直接用z检验因为二项分布在样本量够大时近似正态公式里用的是样本比例的标准误这是教科书和统计软件里都默认的做法。所以你在Python里选方案时代码层面大概是这样的思路连续变量小样本或方差未知优先t检验大样本连续变量、比例比较可以用z检验如果样本量大到一定程度用t和用z在结论上很少会打架真正要紧的是先确认两组数据到底是不是独立样本以及业务问题能不能用均值差异来回答。2. 动手前先自查数据三步检查不能省2.1 数据整理长格式是pandas的正确姿势在Python里做独立双样本检验第一步不是急着调检验函数而是先把数据整理成“长格式”一列是组别一列是数值。长格式的好处一堆groupby描述统计方便后续画箱线图方便而且scipy的检验函数可以直接接收两个数组从长格式里拆出来也顺手。import pandas as pd import numpy as np # 假设 a、b 是两个一维数组长度分别为 n1 和 n2 df pd.DataFrame({ group: [A] * len(a) [B] * len(b), value: np.concatenate([a, b]) }) desc df.groupby(group)[value].agg([count, mean, std]) print(desc)如果你手里的Excel是A组一列、B组一列那要先用melt转成两列结构wide pd.read_excel(data.xlsx) long wide.melt(var_namegroup, value_namevalue)这一步看起来简单但我见过太多人把宽格式数据直接塞进函数里结果不是长度对不上就是组标签错位。数据格式乱了后面检验代码写得再漂亮也没用。2.2 正态性检查t检验最基础的假设独立样本t检验要求两组数据近似正态或者样本量大到可以忽略非正态的影响。这里的“正态性”主要指均值抽样分布的正态性实际数据原始分布偏一点只要样本量不太小t检验往往还能扛得住。但脏活还是要做至少看看数据长什么样。from scipy import stats import matplotlib.pyplot as plt print(Shapiro A:, stats.shapiro(a)) print(Shapiro B:, stats.shapiro(b)) print(NormalTest A:, stats.normaltest(a)) print(NormalTest B:, stats.normaltest(b)) fig, ax plt.subplots(1, 2, figsize(10, 4)) stats.probplot(a, plotax[0]) ax[0].set_title(A组 QQ图) stats.probplot(b, plotax[1]) ax[1].set_title(B组 QQ图) plt.show()shapiro适合样本量不太大的情况normaltest是基于偏度和峰度的综合检验。这些检验的p值小于0.05时提示数据可能偏离正态但不要机械解读样本量巨大时轻微偏态也会让p小于0.05此时看QQ图更靠谱——如果散点大体贴着对角线就认为可接受如果出现明显的S形或尾部偏离就要警惕了。2.3 方差齐性检查决定你该用Student还是Welch另一个前置检查是方差齐性也就是看两组数据的总体方差是否大致相等。经典的独立样本t检验也就是Students t-test假设两组方差相等如果两组方差差别很大直接用等方差版本可能会让p值偏大或偏小这时候应该用Welchs t-test。print(Levene:, stats.levene(a, b)) print(Bartlett:, stats.bartlett(a, b))levene检验对非正态数据更稳健bartlett检验对正态假设更敏感。p小于0.05时提示方差不齐检验选项里用equal_varFalse即可。这里要专门说一下现代统计学界普遍推荐默认使用Welchs t-test不要先跑一个F检验再去决定选哪个。理由很简单F检验本身对正态性敏感而且“先检验再选检验”会让最终结果的显著性带上多重检验的色彩。你就直接跑ttest_ind(equal_varFalse)绝大多数情况下是更稳妥的做法。3. 核心代码t检验和z检验的Python实现与关键参数3.1 独立样本t检验ttest_ind用法与单双尾问题from scipy import stats t_stat, p_two_tailed stats.ttest_ind(a, b, equal_varFalse) print(ft {t_stat:.3f}, p(双尾) {p_two_tailed:.4f}) # 如果备择假设是“A组均值 B组均值” if t_stat 0: p_one_tailed p_two_tailed / 2 else: p_one_tailed 1 - p_two_tailed / 2 print(fp(单尾) {p_one_tailed:.4f})ttest_ind默认返回双尾p值这也是最容易搞错的地方。如果你研究的问题有明确方向比如“新方案是否比旧方案好”你需要的是单尾p值。简单除以2只在检验方向与实际观测差异方向一致时成立t统计量为正而你想检验“A大于B”那么单尾p 双尾p / 2如果t为负而你检验的是“A大于B”那么单尾p 1 - 双尾p / 2。这个细节一旦弄反结论直接翻车。等方差时自由度是n1n2-2Welch检验的自由度用Welch-Satterthwaite近似公式计算很可能是个小数比如18.73。scipy的ttest_ind不直接返回自由度想拿自由度可以用pingouin或手算n1, n2 len(a), len(b) var1, var2 np.var(a, ddof1), np.var(b, ddof1) df_welch (var1/n1 var2/n2)**2 / ((var1/n1)**2/(n1-1) (var2/n2)**2/(n2-1)) print(df_welch)看到自由度是小数不要慌这是Welch检验的正常现象报告里写成t(18.73) 2.31这样的形式就行。3.2 z检验的两条路手写公式和statsmodelsz检验在Python里最常用的入口是statsmodels两组连续变量的均值比较from statsmodels.stats.weightstats import ztest z_stat, p_z ztest(a, b, value0, usevarunpooled) print(fz {z_stat:.3f}, p {p_z:.4f})这里有个容易忽略的参数usevar默认是pooled也就是假设两组方差相等如果两组方差差异比较明显应该用unpooled等价于手写公式里的独立方差版本。手写z检验其实就三行理解了才能不被封装函数牵着走mean_diff np.mean(a) - np.mean(b) se np.sqrt(np.var(a, ddof1)/len(a) np.var(b, ddof1)/len(b)) z_manual mean_diff / se p_manual 2 * (1 - stats.norm.cdf(abs(z_manual))) print(fz(manual) {z_manual:.3f}, p {p_manual:.4f})这个公式的本质就是“差异除以标准误”标准误衡量的是这个差异估计有多稳定。z检验和t检验的差别只在于p值用的是正态分布尾部分数还是t分布尾部分数样本量一大两者几乎没有区别。比例检验是z检验的另一个常见应用场景比如比较两个版本的转化率from statsmodels.stats.proportion import proportions_ztest z_prop, p_prop proportions_ztest(count[120, 98], nobs[1000, 1000]) print(fz(proportion) {z_prop:.3f}, p {p_prop:.4f})3.3 效应量、置信区间与规范报告p值只能回答“有没有差异”回答不了“差异有多大”。尤其在样本量很大的时候一个只有0.5元的差异也可能被标成显著但业务上完全无所谓。所以规范的分析报告必须带上效应量和置信区间。效应量最常用的是Cohens d公式是两组均值差除以合并标准差n1, n2 len(a), len(b) var1, var2 np.var(a, ddof1), np.var(b, ddof1) pooled_sd np.sqrt(((n1-1)*var1 (n2-1)*var2) / (n1n2-2)) d (np.mean(a) - np.mean(b)) / pooled_sd print(fCohens d {d:.3f})经验参考0.2算小效应0.5算中等0.8以上算大效应。这只是经验法则不同领域标准会有差异。均值差的95%置信区间则给出差异可能的范围比单独一个点估计信息量大得多mean_diff np.mean(a) - np.mean(b) se_welch np.sqrt(var1/n1 var2/n2) df_welch (var1/n1 var2/n2)**2 / ((var1/n1)**2/(n1-1) (var2/n2)**2/(n2-1)) t_crit stats.t.ppf(0.975, df_welch) ci_low mean_diff - t_crit * se_welch ci_high mean_diff t_crit * se_welch print(f95% CI [{ci_low:.3f}, {ci_high:.3f}])到这里一个完整的结果表述长这样t(18.73) 2.31, p 0.032, 95% CI [0.32, 4.18], Cohens d 0.87。以后写分析报告照着这个格式来别人想质疑你都难。4. 完整实战两个落地页的平均支付金额到底差多少4.1 案例背景与数据模拟看一个具体场景某电商团队改版了落地页A版是旧方案B版是新方案。现在想对比两组访客的平均支付金额评估新版到底有没有带来提升。真实支付金额数据往往右偏比如多数人只买几十块少数人买几千块实际项目里可能要做对数变换或稳健处理。这里为了把检验流程讲清楚用正态分布模拟数据逻辑是一致的。import numpy as np import pandas as pd from scipy import stats rng np.random.default_rng(42) a rng.normal(100, 15, 60) # A组均值100标准差1560人 b rng.normal(115, 18, 55) # B组均值115标准差1855人我故意把B组均值设高15元样本量各50多这样差异稳定出现方便演示真实项目里差异可能是0.5元、2元流程一模一样。4.2 从描述统计到检验结论的完整代码把前面的步骤组合成一个完整流程# 1. 数据整理 df pd.DataFrame({group: [A]*len(a) [B]*len(b), value: np.concatenate([a, b])}) print(df.groupby(group)[value].agg([count, mean, std])) # 2. 正态性与方差齐性检查 print(Shapiro A:, stats.shapiro(a)) print(Shapiro B:, stats.shapiro(b)) print(Levene:, stats.levene(a, b)) # 3. 独立样本t检验Welch t_stat, p_two stats.ttest_ind(a, b, equal_varFalse) print(fWelch t {t_stat:.3f}, p {p_two:.4f}) # 4. z检验大样本对比 from statsmodels.stats.weightstats import ztest z_stat, p_z ztest(a, b, value0, usevarunpooled) print(fz {z_stat:.3f}, p {p_z:.4f}) # 5. 效应量与置信区间 n1, n2 len(a), len(b) var1, var2 np.var(a, ddof1), np.var(b, ddof1) pooled_sd np.sqrt(((n1-1)*var1 (n2-1)*var2) / (n1n2-2)) d (np.mean(a) - np.mean(b)) / pooled_sd print(fCohens d {d:.3f}) mean_diff np.mean(a) - np.mean(b) se_welch np.sqrt(var1/n1 var2/n2) df_welch (var1/n1 var2/n2)**2 / ((var1/n1)**2/(n1-1) (var2/n2)**2/(n2-1)) t_crit stats.t.ppf(0.975, df_welch) print(f95% CI [{mean_diff - t_crit*se_welch:.3f}, {mean_diff t_crit*se_welch:.3f}])固定随机种子后本地跑出来的结果很稳定t统计量在4.8左右p远小于0.001Cohens d接近0.995%置信区间完全不包含0。这个结果的含义是在样本数据里B组的平均支付金额明显高于A组这种差异用随机波动来解释是很牵强的。为了对比再生成一组“实际上没有差异”的数据a_no rng.normal(100, 16, 60) b_no rng.normal(102, 16, 55) t_no, p_no stats.ttest_ind(a_no, b_no, equal_varFalse) print(ft {t_no:.3f}, p {p_no:.4f})这时候p值一般会在0.3以上压根进不了0.05的显著性门槛。注意这不代表“两组没有差别”只能说明“当前样本没有足够的证据证明有差别”。样本量不足的时候真实差异也可能被判成不显著。4.3 结果怎么讲给业务方听统计结果不能只丢一个p值给业务方否则对方根本不知道这个“显著”有多大意义。比较规范的汇报说法是这样“在95%置信水平下新版落地页的平均支付金额比旧版高出约15元95%置信区间为[9.0, 21.0]Cohens d约0.9属于大效应。从抽样数据看新版带来提升的证据比较充分。”如果p不显著更准确的说法是“当前样本量还不足以支持新版和旧版有差异的结论”而不是直接说“新版没用”。还有一点经常会有人踩雷p值显著不等于实际重要。如果跑出来差异只有0.3元虽然p小于0.001业务上要为了这0.3元去改版吗不一定。判断业务价值要看置信区间下限是否超过成本线要看效应量大小还要看样本能不能代表目标用户群体。5. 常见坑与排查技巧实录5.1 p值最常见的三个误解第一个误解是把p值当成“两组有差异的概率”。p值的准确定义是假设两组真的没有差异出现当前这么极端或更极端数据的概率。它说的是数据的概率不是假设的概率。打个比方你连续中了十次彩票p值描述的是“在公平抽奖的前提下连续十次中奖有多离谱”而不是“这个抽奖绝对作弊的概率是多大”。第二个误解是p大于0.05就代表“证明没有差异”。不显著只是证据不足可能是因为真实差异确实很小也可能是因为样本量不够大。A/B测试里最常见的悲剧就是跑了一周没显著业务方直接下线新功能但其实再跑两周可能就显著了。要搞清楚是“没有差异”还是“检测不出差异”可以看统计功效。第三个误解是显著就等于大。样本量大了以后任何细小的差异都可能显著。所以永远要配套看效应量和置信区间单独一个p值什么都不是。我在报告里一直主张“三件套”p值、置信区间、效应量缺一个都不算完整结论。5.2 我从实战里踩过的六个坑第一个坑Excel宽格式直接跑检验。A组一列、B组一列看起来很方便但一旦组别数量变多、样本量不一致宽格式就会让代码变得很难写。更惨的是有人把两列数据直接用np.array拼接导致样本错位。建议永远先转成长格式再拆数组。第二个坑ttest_ind默认equal_varTrue。如果你是直接复制的教程代码很可能带着等方差假设跑学生t检验。方差不齐时p值可能被放大或缩小结论不稳定。我的习惯是直接用equal_varFalse省心。第三个坑单尾双尾搞混。建议在代码里明确写alternative参数不要留下那行除以2的操作让别人猜。很多统计库比如pingouin直接支持alternativegreater明确写出检验方向代码自解释性会好很多。第四个坑小样本跑ztest。样本量只有十几、二十几的时候t分布和正态分布差得还很明显用z检验会低估不确定性p值可能偏小。除非你确确实实知道总体方差否则小样本老老实实t检验。第五个坑把时间序列数据当独立样本。同一家门店连续N个月的销售额前后月份高度相关直接两组t检验等于把自相关当噪声结论可信度很低。可以考虑配对检验、时间序列方法或混合效应模型。第六个坑反复试到p小于0.05才收手。删异常值、换检验方法、改分组方式直到结果显著这叫p-hacking是严肃分析里的大忌。正确的做法是分析前就把清洗规则、假设检验和显著性阈值定好事后只做一次检验再看敏感性分析稳不稳定。5.3 用pingouin一行输出完整统计报告scipy适合用来理解原理做实际报告的时候我经常会用pingouin因为它一次性给出所有关键指标省得自己拼装import pingouin as pg res pg.ttest(a, b, pairedFalse, alternativetwo-sided) print(res)输出里会直接给出T值、自由度dof、p值、95%置信区间CI95%、Cohens d、贝叶斯因子BF10、统计功效power。这些字段齐全复制到报告里就能用。尤其是自由度scipy不直接给而pingouin帮你算好Welch自由度省事很多。我的工作流是理解阶段用scipy手搓一遍输出阶段用pingouin批量生成报告。6. 这些情况下你别用t/z检验6.1 不满足正态假设考虑Mann-Whitney U检验当数据严重偏态、样本量又小或者数据是有序等级数据的时候t检验的正态假设会比较勉强。这时候可以考虑Mann-Whitney U检验也叫Wilcoxon秩和检验。它不要求正态性检验的是两组数据的分布位置是否不同用的是秩而不是原始数值。u_stat, p_u stats.mannwhitneyu(a, b, alternativetwo-sided) print(fU {u_stat:.3f}, p {p_u:.4f})不过我不建议把所有非正态数据都直接扔给它。如果数据基本符合正态或接近正态t检验的统计功效更高也就是更有可能发现真实差异。非参数检验是备选方案不是万能钥匙。6.2 组数超过两个先ANOVA再Tukey三个以上的组别对比千万不能两两做t检验。假设有3个组要做3次比较即使各组真的没有差异至少出现一次假阳性的概率大约是1-(0.95)³14.3%组数越多这个概率越高。正确的做法是先做方差分析也就是ANOVA看整体有没有差异显著之后再做事后多重比较。from scipy import stats # 假设有a、b、c三组数据 f_stat, p_f stats.f_oneway(a, b, c) print(fF {f_stat:.3f}, p {p_f:.4f}) from statsmodels.stats.multicomp import pairwise_tukeyhsd tukey pairwise_tukeyhsd(np.concatenate([a, b, c]), groups) print(tukey)Tukey HSD会给出两两组别的均值差和调整过显著性水平的结论比盲目t检验靠谱得多。6.3 换一种决策语言贝叶斯因子如果你对频率学派的p值体系一直觉得绕可以试试贝叶斯因子的角度。pingouin里的ttest会顺便输出BF10也就是贝叶斯因子。BF10大于3一般算是中等证据支持有差异大于10算是强证据小于1/3则支持无差异。这个指标最直观的好处是它允许你说“数据更支持哪个假设”而不是晦涩的“拒绝零假设”。import pingouin as pg res pg.ttest(a, b, pairedFalse) print(res[BF10])贝叶斯方法也有自己的坑比如先验选择会影响结果但作为t检验的补充视角尤其在业务汇报里它往往比p值更容易让非技术的人理解。6.4 一张表不同场景该用哪种检验场景是否独立分布假设推荐检验Python入口两组连续数据方差未知正态可接受独立近似正态t检验默认Welchscipy.stats.ttest_ind两组连续数据大样本独立中心极限定理保证均值近似正态z检验statsmodels.stats.weightstats.ztest两组转化率/比例独立两组样本量都够比如np≥10比例z检验statsmodels.stats.proportion.proportions_ztest两组严重偏态、样本量小独立不要求正态Mann-Whitney Uscipy.stats.mannwhitneyu同一批人前后测配对差值近似正态配对t检验scipy.stats.ttest_rel三组及以上独立近似正态方差可接受ANOVA 事后多重比较scipy.stats.f_oneway pairwise_tukeyhsd这张表只是路线图真正决定检验方式的是你的数据生成过程也就是实验设计。看到两组数据先别急着敲代码先问自己样本独不独立抽样有没有偏差样本能不能代表总体这些问题想不清楚用什么检验都是白搭。我自己做了这么些年数据分析最大的体会是假设检验翻车八成不是公式算错而是业务场景没想清楚。两个样本到底独不独立抽样过程有没有引入偏差样本到底能不能代表总体这些前置问题想明白了写代码反而只要几分钟。还有一点不要迷信p值的0.05红绿灯同一个数据换一批样本结果可能就变了多跑几次敏感性分析看看结论稳不稳比盯着一个p值重要得多。希望这篇内容能让你少走点弯路下回再有人问你t检验和z检验的区别你也能拎得清。
返回列表