github star gitee star atomgit star PyPI Downloads AI 编程 AI 交流群

大家好,我是正在实战各种AI项目的程序员晚枫。

今天学习假设检验,这是用数据验证猜想的科学方法。

A/B测试显示新界面转化率更高,是真的有效还是偶然?假设检验帮你做出判断。


什么是假设检验?

核心思想

通过样本数据,判断某个假设是否成立。

基本步骤

  1. 提出假设:原假设H₀(通常是无效果)vs 备择假设H₁
  2. 选择检验方法:根据数据类型选择t检验、卡方检验等
  3. 计算p值:在原假设成立时,观察到当前结果的概率
  4. 做出决策:p < 0.05则拒绝原假设,认为有显著差异

关键概念

  • p值:小于0.05通常认为"统计显著"
  • 置信度:1 - p,如95%置信度
  • 第一类错误:假阳性(实际没效果但认为有)
  • 第二类错误:假阴性(实际有效果但没发现)

t检验:比较两组均值

独立样本t检验

比较两个独立组的均值是否有差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from scipy import stats
import numpy as np

# 场景:A/B测试,新旧界面的转化率
old_version = np.random.normal(0.12, 0.02, 1000) # 旧版转化率12%
new_version = np.random.normal(0.15, 0.02, 1000) # 新版转化率15%

# 独立样本t检验
t_stat, p_value = stats.ttest_ind(new_version, old_version)

print(f"t统计量: {t_stat:.4f}")
print(f"p值: {p_value:.6f}")

if p_value < 0.05:
print("结论:新版本转化率显著高于旧版本(p < 0.05)")
else:
print("结论:差异不显著,可能是随机波动")

配对样本t检验

同一组对象前后对比。

1
2
3
4
5
6
7
8
9
10
11
# 场景:培训前后的考试成绩
before = np.array([65, 70, 72, 68, 75, 80, 78, 82, 70, 74])
after = np.array([72, 78, 75, 74, 82, 85, 83, 88, 76, 80])

t_stat, p_value = stats.ttest_rel(after, before)

print(f"平均提升: {(after - before).mean():.2f}分")
print(f"p值: {p_value:.6f}")

if p_value < 0.05:
print("培训效果显著!")

单样本t检验

与已知值比较。

1
2
3
4
5
6
7
# 场景:今年平均分是否高于去年(去年平均75)
this_year = np.array([78, 82, 75, 80, 77, 85, 79, 81, 76, 84])

t_stat, p_value = stats.ttest_1samp(this_year, 75)

print(f"今年平均分: {this_year.mean():.2f}")
print(f"p值: {p_value:.6f}")

卡方检验:分类变量关联性

卡方独立性检验

检验两个分类变量是否独立。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from scipy.stats import chi2_contingency
import pandas as pd

# 场景:性别和购买偏好是否有关
observed = pd.DataFrame({
'购买': [120, 150],
'未购买': [80, 50]
}, index=['男性', '女性'])

chi2, p_value, dof, expected = chi2_contingency(observed)

print("观察值:")
print(observed)
print("\n期望值(如果独立):")
print(pd.DataFrame(expected, index=observed.index, columns=observed.columns))
print(f"\n卡方值: {chi2:.4f}")
print(f"p值: {p_value:.6f}")

if p_value < 0.05:
print("结论:性别和购买行为有关联")
else:
print("结论:性别和购买行为无关")

卡方拟合优度检验

检验观测分布是否符合期望分布。

1
2
3
4
5
6
7
8
9
10
11
12
13
from scipy.stats import chisquare

# 场景:骰子是否公平
observed = [45, 55, 48, 52, 38, 62] # 各面出现次数
expected = [50, 50, 50, 50, 50, 50] # 公平情况下应该各50次

chi2, p_value = chisquare(observed, expected)

print(f"卡方值: {chi2:.4f}")
print(f"p值: {p_value:.6f}")

if p_value < 0.05:
print("骰子可能不公平!")

实战:完整的A/B测试分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

# 模拟A/B测试数据
np.random.seed(42)
group_a = np.random.binomial(1, 0.12, 5000) # A组:5000人,转化率12%
group_b = np.random.binomial(1, 0.135, 5000) # B组:5000人,转化率13.5%

# 基础统计
conv_a = group_a.mean()
conv_b = group_b.mean()

print("=== A/B测试结果 ===")
print(f"A组转化率: {conv_a:.2%}")
print(f"B组转化率: {conv_b:.2%}")
print(f"相对提升: {(conv_b - conv_a) / conv_a:.2%}")

# 假设检验
t_stat, p_value = stats.ttest_ind(group_b, group_a)

print(f"\nt统计量: {t_stat:.4f}")
print(f"p值: {p_value:.6f}")

# 效应量(Cohen's d)
pooled_std = np.sqrt(((len(group_a)-1)*group_a.var() + (len(group_b)-1)*group_b.var()) /
(len(group_a) + len(group_b) - 2))
cohens_d = (conv_b - conv_a) / pooled_std

print(f"效应量(Cohen's d): {cohens_d:.4f}")
print(" 小效应: 0.2, 中效应: 0.5, 大效应: 0.8")

# 置信区间
se = np.sqrt(conv_a*(1-conv_a)/len(group_a) + conv_b*(1-conv_b)/len(group_b))
ci_lower = (conv_b - conv_a) - 1.96 * se
ci_upper = (conv_b - conv_a) + 1.96 * se

print(f"\n95%置信区间: [{ci_lower:.4f}, {ci_upper:.4f}]")

# 结论
print("\n=== 结论 ===")
if p_value < 0.05:
print(f"✓ B组显著优于A组 (p={p_value:.4f} < 0.05)")
if cohens_d >= 0.2:
print(f"✓ 效应量为{cohens_d:.2f},实际意义明显")
print("建议:全面推广B方案")
else:
print("效应量较小,需权衡成本收益")
else:
print(f"✗ 差异不显著 (p={p_value:.4f})")
print("建议:继续观察或增加样本量")

# 可视化
fig, ax = plt.subplots(figsize=(10, 6))

groups = ['A组', 'B组']
conversions = [conv_a, conv_b]
colors = ['#3498db', '#e74c3c']

bars = ax.bar(groups, conversions, color=colors, alpha=0.7, edgecolor='black')

# 添加数值标签
for bar, conv in zip(bars, conversions):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{conv:.2%}', ha='center', va='bottom', fontsize=14, fontweight='bold')

ax.set_ylabel('转化率', fontsize=12)
ax.set_title(f'A/B测试结果 (p={p_value:.4f})', fontsize=14, fontweight='bold')
ax.set_ylim(0, max(conversions) * 1.2)

# 标注显著性
if p_value < 0.05:
ax.annotate('*', xy=(1, conv_b), xytext=(1, conv_b + 0.01),
fontsize=30, ha='center', color='red')

plt.tight_layout()
plt.savefig('ab_test_result.png', dpi=300)
plt.show()

性能对比:scipy vs 手动计算

1
2
3
4
5
6
7
8
9
10
11
from scipy import stats
import numpy as np

group_a = np.random.randn(1000) + 5
group_b = np.random.randn(1000) + 5.5

# scipy一行代码
%timeit stats.ttest_ind(group_a, group_b) # 约0.5ms

# 手动计算
%timeit ... # 需要写几十行代码

进阶用法

常用检验速查表

场景检验方法scipy函数
两组均值比较独立样本t检验ttest_ind
配对样本比较配对t检验ttest_rel
多组均值比较方差分析(ANOVA)f_oneway
分类变量关联卡方检验chi2_contingency
正态性检验Shapiro-Wilkshapiro
非参数两组比较Mann-Whitney Umannwhitneyu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 独立样本t检验
t_stat, p_value = stats.ttest_ind(group_a, group_b)

# 配对t检验(同一组人前后对比)
t_stat, p_value = stats.ttest_rel(before, after)

# 单样本t检验(样本与理论值比较)
t_stat, p_value = stats.ttest_1samp(sample, popmean=100)

# 方差分析(3组以上)
f_stat, p_value = stats.f_oneway(group_a, group_b, group_c)

# 卡方检验(分类变量)
from scipy.stats import chi2_contingency
table = pd.crosstab(df['gender'], df['purchase'])
chi2, p_value, dof, expected = chi2_contingency(table)

避坑指南

❌ 坑1:p值误解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# p值不是"原假设为真的概率"!
# p值是:如果原假设为真,观察到当前或更极端结果的概率

# p < 0.05 → 拒绝原假设 → 结果有统计显著性
# 但不代表效果一定大!还要看效应量

# 计算效应量(Cohen's d)
def cohens_d(group1, group2):
n1, n2 = len(group1), len(group2)
d = (group1.mean() - group2.mean()) / np.sqrt(
((n1-1)*group1.std()**2 + (n2-1)*group2.std()**2) / (n1+n2-2)
)
return d

effect = cohens_d(group_a, group_b)
print(f"效应量: {effect:.3f}")
# 0.2=小, 0.5=中, 0.8=大

❌ 坑2:多重比较问题

1
2
3
4
5
6
7
# 做20次检验,每次α=0.05,至少一次假阳性概率 = 1-0.95^20 = 64%!
# 解决方案:Bonferroni校正
from statsmodels.stats.multitest import multipletests

p_values = [0.01, 0.03, 0.04, 0.06, 0.12]
reject, corrected_p, _, _ = multipletests(p_values, method='bonferroni', alpha=0.05)
print(f"校正后显著的: {reject}")

实战案例:A/B测试分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import pandas as pd
import numpy as np
from scipy import stats

np.random.seed(42)
n = 5000

# A/B测试:新版页面 vs 旧版页面
ab_test = pd.DataFrame({
'user_id': range(n),
'group': np.random.choice(['control', 'treatment'], n),
'converted': np.where(
np.random.rand(n) < 0.35, 1, 0 # 基础转化率35%
)
})
# 新版多5%转化率
ab_test.loc[ab_test['group'] == 'treatment', 'converted'] = np.where(
np.random.rand(n//2) < 0.40, 1, 0
)

# 1. 各组转化率
conv_rate = ab_test.groupby('group')['converted'].mean()
print("=== A/B测试结果 ===")
print(conv_rate)
print(f"提升: {(conv_rate['treatment'] - conv_rate['control']) / conv_rate['control'] * 100:.1f}%")

# 2. 卡方检验
table = pd.crosstab(ab_test['group'], ab_test['converted'])
chi2, p_value, dof, expected = stats.chi2_contingency(table)
print(f"\n卡方检验: chi2={chi2:.2f}, p={p_value:.4f}")
print(f"结论: {'显著' if p_value < 0.05 else '不显著'}")

# 3. 置信区间
from statsmodels.stats.proportion import proportion_confint
for group in ['control', 'treatment']:
n_group = (ab_test['group'] == group).sum()
n_conv = ab_test[ab_test['group'] == group]['converted'].sum()
ci_low, ci_high = proportion_confint(n_conv, n_group, alpha=0.05)
print(f"{group}: {n_conv/n_group:.3f} [{ci_low:.3f}, {ci_high:.3f}]")

假设检验完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 标准的假设检验流程

def hypothesis_test(sample1, sample2, alpha=0.05):
'''完整的假设检验流程'''
from scipy import stats

# Step 1: 检查正态性
_, p1 = stats.shapiro(sample1[:5000]) # Shapiro限制样本量
_, p2 = stats.shapiro(sample2[:5000])
is_normal = (p1 > alpha) and (p2 > alpha)

# Step 2: 选择检验方法
if is_normal:
# 正态分布:用参数检验
# 检查方差齐性
_, p_var = stats.levene(sample1, sample2)
equal_var = p_var > alpha

# t检验
t_stat, p_value = stats.ttest_ind(sample1, sample2, equal_var=equal_var)
test_name = 't检验'
else:
# 非正态:用非参数检验
u_stat, p_value = stats.mannwhitneyu(sample1, sample2)
test_name = 'Mann-Whitney U检验'

# Step 3: 判断结果
significant = p_value < alpha

# Step 4: 计算效应量
effect_size = (sample1.mean() - sample2.mean()) / np.sqrt(
((len(sample1)-1)*sample1.std()**2 + (len(sample2)-1)*sample2.std()**2)
/ (len(sample1)+len(sample2)-2)
)

# Step 5: 输出报告
print(f"检验方法: {test_name}")
print(f"统计量: {t_stat if is_normal else u_stat:.4f}")
print(f"p值: {p_value:.4f}")
print(f"结论: {'拒绝原假设(有显著差异)' if significant else '不能拒绝原假设'}")
print(f"效应量(Cohen's d): {effect_size:.3f}")

return p_value, significant

# 使用
hypothesis_test(group_a, group_b)

下节预告

下一课我们将学习数据分布分析,了解正态分布和QQ图。

👉 继续阅读:数据分布分析-正态分布与QQ图


💬 加入学习交流群

扫码加入Python学习交流群,和数千名同学一起进步:

👉 点击加入交流群

群里不定期分享:

  • 数据分析实战案例
  • Python学习资料
  • 求职面试经验
  • 行业最新动态

推荐:AI Python数据分析实战营

🎁 限时福利:送《利用Python进行数据分析》实体书

👉 点击了解详情


课程导航

上一篇: 描述性统计-用数字概括数据特征

下一篇: 数据分布分析-正态分布与QQ图


PS:假设检验是数据驱动决策的基础。记住:p < 0.05只是门槛,还要结合实际业务意义。



📚 推荐教材

主教材《Excel+Python 飞速搞定数据分析与处理(图灵出品)》

💬 联系我

平台账号/链接
微信扫码加好友
微博@程序员晚枫
知乎@程序员晚枫
抖音@程序员晚枫
小红书@程序员晚枫
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!