Python实战:用卡方检验分析A/B测试结果(附完整代码)
Python实战:用卡方检验分析A/B测试结果(附完整代码)
在数据驱动的决策时代,A/B测试已经成为市场营销和产品优化的标配工具。但面对测试结果时,许多从业者常陷入"看数字说话"的误区——点击率提高2%是否真的意味着策略有效?转化率下降1%是否应该立即叫停测试?这些问题的答案往往隐藏在统计检验的细节中。
卡方检验(Chi-square test)正是解决这类问题的利器。不同于t检验等参数检验方法,卡方检验专为分类数据设计,能够准确判断不同策略组间的差异是否具有统计显著性。本文将带你从实际业务场景出发,用Python完整实现卡方检验的全流程,并深入解读每个统计量的业务含义。
1. 卡方检验的业务应用场景
在开始代码实战前,我们需要明确卡方检验最适合解决哪些业务问题。根据不同的数据特征和业务目标,卡方检验主要应用于三大场景:
1.1 广告效果对比分析
假设某电商平台同时投放了两种风格的广告素材:
- 素材A:突出价格优势
- 素材B:强调产品品质
经过一周的投放,收集到以下用户行为数据:
| 广告组 | 点击次数 | 未点击次数 | 总计 |
|---|---|---|---|
| 素材A | 1200 | 8800 | 10000 |
| 素材B | 1800 | 8200 | 10000 |
此时卡方检验可以回答:两种广告素材的点击率差异是否具有统计显著性?
1.2 营销渠道质量评估
某APP在三个获客渠道的注册转化数据如下:
| 渠道 | 注册用户 | 未注册用户 | 总计 |
|---|---|---|---|
| 应用商店 | 1500 | 8500 | 10000 |
| 社交媒体 | 800 | 9200 | 10000 |
| 搜索引擎 | 1200 | 8800 | 10000 |
通过卡方检验可以判断:不同渠道的注册转化率是否存在显著差异?
1.3 产品功能改版验证
某SaaS产品对仪表盘进行了改版,收集到新旧版本的用户留存数据:
| 版本 | 7日留存用户 | 未留存用户 | 总计 |
|---|---|---|---|
| 旧版 | 450 | 2550 | 3000 |
| 新版 | 600 | 2400 | 3000 |
卡方检验能帮助我们确认:新版设计是否真的提升了用户留存?
注意:卡方检验要求每个单元格的期望频数不小于5。当样本量较小时,建议使用Fisher精确检验替代。
2. Python实现卡方检验完整流程
现在让我们进入实战环节,使用Python的scipy.stats库完整实现卡方检验。我们将以一个真实的电商促销案例贯穿整个流程。
2.1 数据准备与列联表构建
假设我们在黑色星期五促销期间测试了两种优惠策略:
- 策略A:全场8折
- 策略B:满300减60
收集到的购买行为数据如下:
import pandas as pd
from scipy.stats import chi2_contingency
# 构建列联表
data = {
'策略A': [620, 1380], # [购买, 未购买]
'策略B': [750, 1250]
}
df = pd.DataFrame(data, index=['购买', '未购买'])
print("列联表:")
print(df)
输出结果:
列联表:
策略A 策略B
购买 620 750
未购买 1380 1250
2.2 执行卡方检验
使用chi2_contingency函数一键完成检验:
# 执行卡方检验
chi2, p, dof, expected = chi2_contingency(df, correction=False)
print(f"\n卡方统计量:{chi2:.4f}")
print(f"P值:{p:.6f}")
print(f"自由度:{dof}")
print("\n期望频数表:")
print(pd.DataFrame(expected, index=df.index, columns=df.columns))
输出结果:
卡方统计量:16.0796
P值:0.000061
自由度:1
期望频数表:
策略A 策略B
购买 685.000000 685.000000
未购买 1315.000000 1315.000000
2.3 结果可视化
用matplotlib直观展示观测值与期望值的差异:
import matplotlib.pyplot as plt
import numpy as np
# 准备数据
observed = df.values
expected_df = pd.DataFrame(expected, index=df.index, columns=df.columns)
# 绘制对比图
fig, ax = plt.subplots(1, 2, figsize=(12, 5))
# 观测值热力图
im1 = ax[0].imshow(observed, cmap='Blues')
ax[0].set_title('观测频数')
fig.colorbar(im1, ax=ax[0], fraction=0.046, pad=0.04)
# 期望值热力图
im2 = ax[1].imshow(expected, cmap='Oranges')
ax[1].set_title('期望频数')
fig.colorbar(im2, ax=ax[1], fraction=0.046, pad=0.04)
# 添加数值标签
for i in range(2):
for j in range(2):
ax[0].text(j, i, observed[i,j], ha='center', va='center', color='black')
ax[1].text(j, i, f"{expected[i,j]:.0f}", ha='center', va='center', color='black')
plt.tight_layout()
plt.show()
这段代码会生成并排的两张热力图,直观展示观测频数与期望频数的差异。
3. 统计结果深度解读
得到检验结果后,我们需要从统计和业务两个层面进行解读。
3.1 统计指标解析
-
卡方统计量(16.08):衡量观测值与期望值偏离程度的综合指标。值越大说明差异越显著。
-
P值(0.000061):远小于常用的显著性水平0.05,说明我们有足够证据拒绝原假设(即两种策略效果无差异)。
-
自由度(1):由(行数-1)×(列数-1)计算得出,决定卡方分布的形状。
-
期望频数表:显示如果两种策略效果相同时的理论分布。对比观测表可以发现:
- 策略B的实际购买数(750)明显高于期望值(685)
- 策略A的实际购买数(620)低于期望值
3.2 业务决策建议
基于统计结果,我们可以得出以下业务洞见:
-
策略效果差异:满减策略(B)的转化效果显著优于直接折扣(A),购买率高出约3.3个百分点。
-
收益估算:
- 策略A转化率:620/2000 = 31%
- 策略B转化率:750/2000 = 37.5%
- 每1000名用户可多获得65笔订单
-
实施建议:
- 在全渠道推广满减策略
- 可进一步测试不同满减门槛的效果
- 需要监控客单价变化,评估对整体营收的影响
3.3 效应量测量
除了统计显著性,我们还需要评估实际差异的大小(效应量)。常用的指标有:
-
Phi系数(适用于2×2列联表):
n = df.values.sum() phi = np.sqrt(chi2 / n) print(f"Phi系数:{phi:.3f}")输出:
Phi系数:0.063 -
Cramer's V(适用于任意大小的列联表):
cramers_v = np.sqrt(chi2 / (n * min(df.shape[0]-1, df.shape[1]-1))) print(f"Cramer's V:{cramers_v:.3f}")输出:
Cramer's V:0.063
根据Cohen的标准:
- 0.1以下:小效应
- 0.3左右:中等效应
- 0.5以上:大效应
本例中效应量较小,说明虽然统计显著,但实际差异幅度有限。
4. 进阶应用与常见陷阱
掌握了基础用法后,我们来看几个实际应用中需要注意的高级问题。
4.1 多重检验校正
当同时比较多组策略时,直接使用卡方检验会导致多重比较问题。解决方案:
from statsmodels.stats.multitest import multipletests
# 假设我们测试了5种策略,得到5个p值
p_values = [0.04, 0.01, 0.15, 0.003, 0.07]
# 使用Benjamini-Hochberg方法校正
rejected, corrected_p, _, _ = multipletests(p_values, method='fdr_bh')
print("原始p值:", p_values)
print("校正后p值:", corrected_p)
print("是否拒绝:", rejected)
4.2 样本量不足的处理
当单元格期望频数小于5时,可以考虑:
-
合并类别:将稀疏类别合并
# 原始数据 data = [[10, 20, 5], [30, 40, 2]] # 合并最后两列 data_merged = [[10, 25], [30, 42]] -
使用精确检验:
from scipy.stats import fisher_exact oddsratio, p_value = fisher_exact([[5, 15], [10, 20]]) print(f"Fisher精确检验p值:{p_value:.4f}")
4.3 连续变量分箱策略
当原始数据是连续变量时,需要合理分箱:
# 假设有用户浏览时长和转化数据
df = pd.DataFrame({
'浏览时长': np.random.exponential(scale=300, size=1000),
'转化': np.random.choice([0,1], size=1000, p=[0.8,0.2])
})
# 等宽分箱
df['时长分组'] = pd.cut(df['浏览时长'], bins=5)
# 构建列联表
contingency_table = pd.crosstab(df['时长分组'], df['转化'])
print(contingency_table)
4.4 纵向追踪数据的处理
对于同一用户在不同时间点的重复测量数据,需要使用McNemar检验而非卡方检验:
from statsmodels.stats.contingency_tables import mcnemar
# 用户改版前后使用情况
table = [[50, 25], # 改版前使用,改版后继续使用
[15, 10]] # 改版前不使用,改版后开始使用
result = mcnemar(table, exact=True)
print(f"McNemar检验p值:{result.pvalue:.4f}")
5. 完整案例:电商促销策略评估
让我们通过一个综合案例巩固所学内容。假设某电商平台测试了三种促销策略:
# 数据收集
data = {
'限时折扣': [320, 680],
'满减优惠': [380, 620],
'赠品促销': [290, 710]
}
df = pd.DataFrame(data, index=['下单', '未下单'])
# 卡方检验
chi2, p, dof, expected = chi2_contingency(df)
# 效应量计算
n = df.values.sum()
cramers_v = np.sqrt(chi2 / (n * (min(df.shape)-1)))
# 可视化
plt.figure(figsize=(10,4))
plt.subplot(121)
sns.heatmap(df, annot=True, fmt='d', cmap='Blues')
plt.title('观测频数')
plt.subplot(122)
sns.heatmap(pd.DataFrame(expected, index=df.index, columns=df.columns),
annot=True, fmt='.1f', cmap='Oranges')
plt.title('期望频数')
plt.tight_layout()
plt.show()
print(f"卡方检验结果:χ²({dof}) = {chi2:.2f}, p = {p:.4f}")
print(f"Cramer's V效应量:{cramers_v:.3f}")
5.1 业务解读
-
统计结论:p值小于0.05,说明三种促销策略的效果存在显著差异。
-
效果排序:
- 满减优惠转化率最高(38%)
- 限时折扣次之(32%)
- 赠品促销最低(29%)
-
实施建议:
- 优先推广满减优惠
- 可测试不同满减门槛(如满200减30 vs 满300减50)
- 赠品策略可能需要优化赠品选择
5.2 后续分析方向
-
细分分析:按用户画像(新/老用户、消费层级等)进行分层检验
-
收益成本分析:结合客单价和促销成本计算ROI
-
长期影响:监测使用促销策略用户的复购率和生命周期价值
更多推荐


所有评论(0)