Ratio-метрики вроде CTR, ARPU или среднего чека часто ломают привычный подход к A/B-тестам, потому что в них числитель и знаменатель зависимы: у пользователя с большими "views" обычно больше и "clicks", у крупного покупателя выше и "revenue", и "orders". Если применить стандартный t-test как к обычной метрике, дисперсия получается оценена неверно и итоговое p-value начинает врать. Delta-method как раз помогает аккуратно оценить дисперсию метрики вида num / denom с учётом ковариации между числителем и знаменателем и дальше уже работать с этой метрикой как с обычной: считать размер выборки, uplift, доверительные интервалы и значимость. Удобная реализация для данных на уровне пользователя:
def est_ratio_var(num, denom):
mean_num, mean_denom = np.mean(num), np.mean(denom)
var_num, var_denom = np.var(num), np.var(denom)
cov = np.cov(num, denom)[0, 1]
ratio_var = (
(var_num / mean_denom ** 2)
- (2 * (mean_num / mean_denom ** 3) * cov)
+ ((mean_num ** 2 / mean_denom ** 4) * var_denom)
)
return ratio_var
Дальше посмотрим на CTR, где по каждому "user_id" есть "clicks" и "views". Для расчёта размера выборки можно сначала оценить дисперсию CTR в текущих данных, задать относительный MDE и перевести его в "cohen_d", а затем воспользоваться стандартной функцией мощности, например так:
rel_mde = 0.1
var = est_ratio_var(df["clicks"], df["views"])
control_std = var**0.5
control_mean = df["clicks"].sum() / df["views"].sum()
test_mean = control_mean * (1 + rel_mde)
cohen_d = (test_mean - control_mean) / control_std
n1 = tt_ind_solve_power(
effect_size=cohen_d,
alpha=0.05,
power=0.8,
ratio=9,
alternative="two-sided"
)
print(
f"Размер выборки delta-method: "
f"n1 = {round(n1)}, "
f"n2 = {round(9 * n1)}"
)
Когда эксперимент уже прошёл, по тем же группам можно оценить статистическую значимость uplift CTR. Для этого считаем итоговые "clicks", "views" по группам, через delta-method получаем дисперсии и дальше строим нормальную статистику и p-value:
def delta_method(group_A, group_B):
ratio_A = group_A["clicks"].sum() / group_A["views"].sum()
ratio_B = group_B["clicks"].sum() / group_B["views"].sum()
var_A = est_ratio_var(group_A["clicks"], group_A["views"])
var_B = est_ratio_var(group_B["clicks"], group_B["views"])
uplift = ratio_B - ratio_A
se = np.sqrt(var_B / len(group_B) + var_A / len(group_A))
t = uplift / se
p_val = (1 - stats.norm.cdf(abs(t))) * 2
return p_val
Delta-method особенно удобен тем, что оставляет привычную для бизнеса картинку: "uplift", стандартная ошибка, доверительный интервал и p-value, но при этом корректно учитывает зависимость в ratio-метриках. Для надёжности в реальных задачах полезно иногда сверяться с бутстрепом или линеаризацией, но как базовый рабочий инструмент delta-method уже даёт аккуратные и довольно устойчивые оценки. Если формат такого кода и пояснений заходит, можно отдельно разобрать "линеаризацию" или "бутстреп" для тех же CTR и ARPU.
@ProdAnalysis