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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
| # A/B测试统计分析
from scipy import stats
import numpy as np
class ABTestAnalysis:
"""A/B测试分析"""
def __init__(self):
self.confidence_level = 0.95
def analyze_conversion_test(self, control_data, variant_data):
"""
分析转化率A/B测试
control_data: 对照组数据 [0, 1, 1, 0, 1, ...]
variant_data: 变体组数据 [1, 1, 0, 1, 1, ...]
"""
# 基础统计
n_control = len(control_data)
n_variant = len(variant_data)
conversions_control = sum(control_data)
conversions_variant = sum(variant_data)
cr_control = conversions_control / n_control
cr_variant = conversions_variant / n_variant
# 计算绝对提升
absolute_lift = cr_variant - cr_control
# 计算相对提升
relative_lift = (cr_variant - cr_control) / cr_control * 100 if cr_control > 0 else 0
# Z检验
from statsmodels.stats.proportion import proportions_ztest, proportion_confint
count = np.array([conversions_control, conversions_variant])
nobs = np.array([n_control, n_variant])
z_stat, p_value = proportions_ztest(count, nobs)
# 置信区间
(lower_con, lower_treat), (upper_con, upper_treat) = proportion_confint(
count, nobs, alpha=1-self.confidence_level
)
# 计算显著性
is_significant = p_value < (1 - self.confidence_level)
# 计算需要样本量
required_sample_size = self.calculate_sample_size(
cr_control,
relative_lift / 100,
power=0.8
)
return {
'control': {
'conversions': conversions_control,
'visitors': n_control,
'conversion_rate': cr_control
},
'variant': {
'conversions': conversions_variant,
'visitors': n_variant,
'conversion_rate': cr_variant
},
'lift': {
'absolute': round(absolute_lift * 100, 2),
'relative': round(relative_lift, 2),
'confidence_interval': [
round(lower_con * 100, 2),
round(upper_con * 100, 2)
]
},
'statistical': {
'z_statistic': round(z_stat, 4),
'p_value': round(p_value, 4),
'is_significant': is_significant,
'confidence_level': self.confidence_level
},
'sample_size': {
'required_per_variant': required_sample_size,
'total_required': required_sample_size * 2,
'current_total': n_control + n_variant,
'power_achieved': self.calculate_power(n_control, cr_control, absolute_lift)
}
}
def calculate_sample_size(self, baseline_cr, mde, alpha=0.05, power=0.8):
"""
计算所需样本量
baseline_cr: 基线转化率
mde: 最小可检测效应
alpha: 显著性水平
power: 统计功效
"""
from statsmodels.stats.proportion import proportion_effectsize
from statsmodels.stats.power import NormalIndPower
effect_size = proportion_effectsize(
baseline_cr,
baseline_cr * (1 + mde)
)
power_analysis = NormalIndPower()
sample_size = power_analysis.solve_power(
effect_size=effect_size,
alpha=alpha,
power=power
)
return int(np.ceil(sample_size))
def calculate_power(self, sample_size, baseline_cr, effect_size):
"""计算当前样本量的统计功效"""
from statsmodels.stats.proportion import proportion_effectsize
from statsmodels.stats.power import NormalIndPower
es = proportion_effectsize(baseline_cr, baseline_cr + effect_size)
power_analysis = NormalIndPower()
power = power_analysis.power(
effect_size=es,
nobs1=sample_size,
alpha=0.05
)
return round(power, 2)
|