Байесовский подход применен к А/Б-тесту конверсий с 3 группами. Лучшая группа выбирается сравнением апостериорных распределений. Способ применим для других метрик и большего количества вариантов.
Блокнот: https://github.com/andrewbrdk/Bayesian-AB-Testing/blob/main/appendices/Множественные_сравнения.ipynb .
Библиотеки
import numpy as np import pandas as pd import scipy.stats as stats import plotly.graph_objects as go np.random.seed(7)
В А/Б-тестах бывает больше 2 вариантов. "Проверка статистических гипотез" в таких случаях требует поправок на множественные сравнения [MultipleComp, FWER, Bonf]. В байесовском подходе лучшая группа выбирается сравнением апостериорных распределений, дополнительные поправки не требуются.
На три версии веб-страницы A, B и С зашло по человек. Кнопку "Продолжить" нажали
,
,
человек соответственно. С какой вероятностью конверсия каждого из вариантов лучшая?
Для каждого варианта нужно оценить вероятность наибольшей конверсии из всех групп: для А, аналогично для B и C. Вероятности можно оценить численно сравнением выборок апостериорных распределений. Для конверсий правдоподобие
задается биномиальным распределением, априорное распределение
- бета-распределением. В таком случае апостериорные распределения
также будут бета-распределениями [BayesABConv, BetaDist, SciPyBeta, ConjPrior].
На графике приведены апостериорные распределения каждой группы. Распределения пересекаются. Вероятности лучшей конверсии ,
,
.
Апостериорные распределения конверсий
def posterior_dist_binom(ns, ntotal, a_prior=1, b_prior=1): a = a_prior + ns b = b_prior + ntotal - ns return stats.beta(a=a, b=b) N = 1000 sa = 100 sb = 105 sc = 110 p_dist_a = posterior_dist_binom(ns=sa, ntotal=N) p_dist_b = posterior_dist_binom(ns=sb, ntotal=N) p_dist_c = posterior_dist_binom(ns=sc, ntotal=N) npost = 50000 samp_a = p_dist_a.rvs(size=npost) samp_b = p_dist_b.rvs(size=npost) samp_c = p_dist_c.rvs(size=npost) p_a_best = np.sum((samp_a > samp_b) & (samp_a > samp_c)) / npost p_b_best = np.sum((samp_b > samp_a) & (samp_b > samp_c)) / npost p_c_best = np.sum((samp_c > samp_a) & (samp_c > samp_b)) / npost xaxis_max = 0.2 x = np.linspace(0, xaxis_max, 1000) fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=p_dist_a.pdf(x), line_color='black', name='A')) fig.add_trace(go.Scatter(x=x, y=p_dist_b.pdf(x), line_color='black', line_dash='longdash', name='B')) fig.add_trace(go.Scatter(x=x, y=p_dist_c.pdf(x), line_color='black', line_dash='dot', name='C')) fig.update_layout(title='Апостериорные распределения', xaxis_title='$p$', yaxis_title='Плотность вероятности', xaxis_range=[0, xaxis_max], hovermode="x", height=500) fig.show() print(f"P Best:") print(f"P(Best A) = P(A>B & A>C) = {p_a_best}") print(f"P(Best B) = P(B>A & B>C) = {p_b_best}") print(f"P(Best C) = P(C>A & C>B) = {p_c_best}")
P Best: P(Best A) = P(A>B & A>C) = 0.14664 P(Best B) = P(B>A & B>C) = 0.30228 P(Best C) = P(C>A & C>B) = 0.55108

Количество правильно угаданных вариантов в серии экспериментов следующее. В группе A задается конверсия p = 0.1, в группах B и C конверсия выбирается случайно в диапазоне от
p. В группах генерируются данные с шагом n_samp_step. На каждом шаге в каждом варианте считаются апостериорные распределения и вероятность лучшей конверсии среди всех групп и др. Эксперимент останавливается, если в одной из групп вероятность наибольшей конверсии достигает
prob_stop=0.95 или сгенерировано максимальное количество точек n_samp_max. Проводится nexps экспериментов, считается доля правильно угаданных групп. В данном случае в nexps = 1000 правильно угадано 951. Точность 0.951 близка ожидаемой prob_stop = 0.95.
Nexp: 1000, Correct Guesses: 951, Accuracy: 0.951
Правильно угаданные варианты в серии экспериментов
def p_best(*args, n_post_samp=50_000): samp = [d.rvs(size=n_post_samp) for d in args] best_group = np.argmax(np.vstack(samp), axis=0) u = np.unique(best_group, return_counts=True) p_best = np.zeros(len(args)) for i, c in zip(u[0], u[1]): p_best[i] = c p_best = p_best / n_post_samp return p_best cmp = pd.DataFrame(columns=['A', 'B', 'C', 'best_exact', 'exp_samp_size', 'A_exp', 'B_exp', 'C_exp', 'best_exp', 'p_best']) p = 0.1 nexps = 1000 cmp['A'] = [p] * nexps cmp['B'] = p * (1 + stats.uniform.rvs(loc=-0.05, scale=0.1, size=nexps)) cmp['C'] = p * (1 + stats.uniform.rvs(loc=-0.05, scale=0.1, size=nexps)) cmp['best_exact'] = cmp.apply(lambda r: 'A' if r['A'] > r['B'] and r['A'] > r['C'] else 'B' if r['B'] > r['A'] and r['B'] > r['C'] else 'C', axis=1) n_samp_max = 30_000_000 n_samp_step = 10_000 prob_stop = 0.95 for i in range(nexps): pA = cmp.at[i, 'A'] pB = cmp.at[i, 'B'] pC = cmp.at[i, 'C'] exact_dist_A = stats.bernoulli(p=pA) exact_dist_B = stats.bernoulli(p=pB) exact_dist_C = stats.bernoulli(p=pC) n_samp_total = 0 ns_A = 0 ns_B = 0 ns_C = 0 while n_samp_total < n_samp_max: dA = exact_dist_A.rvs(n_samp_step) dB = exact_dist_B.rvs(n_samp_step) dC = exact_dist_C.rvs(n_samp_step) n_samp_total += n_samp_step ns_A = ns_A + np.sum(dA) ns_B = ns_B + np.sum(dB) ns_C = ns_C + np.sum(dC) post_dist_A = posterior_dist_binom(ns=ns_A, ntotal=n_samp_total) post_dist_B = posterior_dist_binom(ns=ns_B, ntotal=n_samp_total) post_dist_C = posterior_dist_binom(ns=ns_C, ntotal=n_samp_total) p_best_A, p_best_B, p_best_C = p_best(post_dist_A, post_dist_B, post_dist_C) best_gr = 'A' if p_best_A >= prob_stop else 'B' if p_best_B >= prob_stop else 'C' if p_best_C >= prob_stop else None if best_gr: cmp.at[i, 'A_exp'] = post_dist_A.mean() cmp.at[i, 'B_exp'] = post_dist_B.mean() cmp.at[i, 'C_exp'] = post_dist_C.mean() cmp.at[i, 'exp_samp_size'] = n_samp_total cmp.at[i, 'best_exp'] = best_gr cmp.at[i, 'p_best'] = max(p_best_A, p_best_B, p_best_C) break print(f'done {i}: nsamp {n_samp_total}, best_gr {best_gr}, P_best {max(p_best_A, p_best_B, p_best_C)}') cmp['correct'] = cmp['best_exact'] == cmp['best_exp'] display(cmp.head(20)) cor_guess = np.sum(cmp['correct']) print(f"Nexp: {nexps}, Correct Guesses: {cor_guess}, Accuracy: {cor_guess / nexps}")

Байесовский подход применен к А/Б-тесту конверсий с 3 группами. Лучшая группа выбирается сравнением апостериорных распределений. Способ применим для других метрик и большего количества вариантов.
Ссылки:
[BayesABConv] - Bayesian A/B-Testing, GitHub.
[BetaDist] - Beta Distribution, Wikipedia.
[Bonf] - Bonferroni Correction, Wikipedia.
[ConjPrior] - Conjugate Prior, Wikipedia.
[FWER] - Family-wise Error Rate, Wikipedia.
[MultipleComp] - Multiple Comparisons Problem, Wikipedia.
[SciPyBeta] - scipy.stats.beta, SciPy Reference.
