Показана численная близость -значений
-теста,
-теста и
-критерия Манна-Уитни в А/Б-тестах вероятностям лучшей группы байесовских моделей. Соотношения выполняются несмотря на различия в определениях.
- P-значения
- Т-тест
- Тест
- U-критерий Манна-Уитни
- Ссылки
Блокнот: https://github.com/andrewbrdk/Bayesian-AB-Testing/blob/main/appendices/Связь_с_p-значениями.ipynb
import numpy as np
import pandas as pd
import scipy.stats as stats
import plotly.graph_objects as go
np.random.seed(7)P-значения
-значения используют в проверках нулевых гипотез. В этом методе формулируют гипотезу
об изучаемом процессе и выбирают статистический тест
- случайную в��личину с известным распределением
в предположении
. Считают реализацию величины
в данных - тестовую статистику
[TestStat]. Вероятность получить фактическое или более экстремальное значение тестовой статистики называют односторонним
-значением
[PVal, TailedTests]. Если вероятность достаточно мала, гипотезу
отвергают, если нет - оставляют.

Решение о гипотезе принимают по
-значению
, тогда как вероятность гипотезы с учетом собранных данных оценивается величиной
. По соотношению Байеса
. Т.е. для выбора гипотезы нужно считать вероятности получить данные в рамках конкурирующих гипотез и сравнивать друг с другом с учетом априорных вероятностей.
В А/Б-тестах распространены -тест средних,
-тест пропорций и
-критерий Манна-Уитни. Далее показано как при определенных условиях
-значения этих тестов численно близки байесовским вероятностям параметров одной группы больше другой. Соотношения выполняются несмотря на различия в определениях.
Т-тест
Средние сравнивают -тестами [TTest]. Пусть есть выборки размера
из двух случайных величин
. В предположении одинаковых точных средних
для отношения разности выборочных средних к стандартной ошибке разности средних
ожидают
-распределение [WelchT]. При достаточно большом количестве данных оно близко стандартному нормальному
[TDist]. По выборкам считают фактическое отношение
. Вычисляют вероятность получить
или большее значение - одностороннее
-значение
. Если оно меньше заданного уровня значимости, средние в группах считают неравными.
В А/Б-тесте нужно выбрать группу с большим средним. Вместо -значения
интересна вероятность среднего
больше
при условии собранных данных
, где
- оценки точных средних соответствующих распределений. Эту вероятность можно оценить байесовским моделированием. Будет строиться оценка среднего разности
. По центральной предельной теореме распределение выборочных средних близко нормальному. Поэтому в качестве правдоподобия выбирается нормальное распределение
. Используется модель с одним случайным параметром - средним
, дисперсия выбирается
на основе данных. Моделирование применяется к выборочным средним, а не исходным выборкам, поэтому для обновления параметров используется только одна точка
. Для сопряженности априорное распределение также выбирается нормальным
. Апостериорное распределение будет нормальным с обновленными средним и дисперсией
[ConjPrior]. При достаточно широком априорном распределении
апостериорное распределение приближенно
. Вероятность среднего в одной группе больше другой можно записать как положительную область нормального распределения с центром в точке
и единичной дисперсией
.
По симметрии нормального распределения . Поэтому
-значение одностороннего
-теста близко вероятности среднего одной группы больше другой.
Для демонстрации ниже заданы два нормальных распределения с разными средними. По выборке байесовская оценка вероятности сравнивается с
-значением
-теста. Используется односторонний
-тест с разными дисперсиями групп (
equal_var=False, alternative) [ScipyTTestInd]. Вероятность
закрашена темным,
закрашена светлым. По свойствам нормального распределения площади этих областей совпадают. Таким образом
-значение численно близко байесовской оценке вероятности. Стоит помнить, что они не эквивалентны - у них разные определения.
Т-распределение и апостериорное среднего разности
def posterior_diff_scaled(sampA, sampB, mu0=None, s20=None):
delta = sampB.mean() - sampA.mean()
s2delta = sampA.var() / sampA.size + sampB.var() / sampB.size
mu0 = mu0 or 0
s20 = s20 or 30 * s2delta
s2n = s2delta * s20 / (s2delta + s20)
mun = mu0 * s2n / s20 + delta * s2n / s2delta
return stats.norm(loc=mun/np.sqrt(s2n), scale=1)
muA = 0.1
muB = 0.115
sigma = 1.3
exactA = stats.norm(muA, sigma)
exactB = stats.norm(muB, sigma)
N = 30000
sampA = exactA.rvs(size=N)
sampB = exactB.rvs(size=N)
a = 'greater' if np.mean(sampA) > np.mean(sampB) else 'less'
t_stat, p_value = stats.ttest_ind(sampA, sampB, equal_var=False, alternative=a)
t_stat = np.abs(t_stat)
post_dist = posterior_diff_scaled(sampA, sampB)
mean_b_gt_a = 1 - post_dist.cdf(0)
xaxis_min = -7
xaxis_max = 8
x = np.linspace(xaxis_min, xaxis_max, 1000)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=stats.norm.pdf(x, loc=0, scale=1),
line_color='black', opacity=0.8, name='$\mathrm{Norm}(0, 1)$'))
fig.add_trace(go.Scatter(x=x[x>t_stat], y=stats.norm.pdf(x[x>t_stat], loc=0, scale=1),
line_color="rgba(0, 0, 0, 0.7)", name='$P(x>x_0 | H_0)$', fill="tozeroy", fillcolor="rgba(0, 0, 0, 0.7)"))
fig.add_trace(go.Scatter(x=x, y=post_dist.pdf(x),
line_color='black', opacity=0.2, name='$\mathrm{Norm}(x_0, 1)$'))
fig.add_trace(go.Scatter(x=x[x<0], y=post_dist.pdf(x[x<0]),
line_color="rgba(128, 128, 128, 0.2)", name='$P(\mu_{\Delta} < 0 | \overline{\Delta})$', fill="tozeroy", fillcolor="rgba(128, 128, 128, 0.2)"))
fig.add_trace(go.Scatter(x=[0, 0], y=[0, max(stats.norm.pdf(x, loc=0, scale=1))*1.1],
line_color='black',
mode='lines+text', text=['', '0'], textposition="top center",
line_dash='dash', showlegend=False))
fig.add_trace(go.Scatter(x=[t_stat, t_stat], y=[0, max(stats.norm.pdf(x, loc=0, scale=1))*1.1],
line_color='black',
mode='lines+text', text=['', '$x_0$'], textposition="top center",
line_dash='dash', showlegend=False))
fig.update_layout(title=r'$T\text{-распределение и апостериорное среднего разности}$',
xaxis_title='$x$',
yaxis_title='Плотность вероятности',
xaxis_range=[xaxis_min, xaxis_max],
hovermode="x",
template="plotly_white",
height=500)
fig.show()
print(f'p-value P(x>x0 | H0): {p_value}')
print(f'1 - p: {1 - p_value}')
print(f'Bayes P(pb > pa): {mean_b_gt_a}')
Численную близость -значения и байесовской вероятности
можно проверить по количеству правильно угаданных вариантов в серии экспериментов. В каждом эксперименте задается два нормальных распределения. В группе
среднее фиксировано
mu=0.1, в выбирается случайно в диапазоне
от
mu. В группы добавляются данные по n_samp_step точек за шаг. На каждом шаге считается -тест. Эксперимент останавливается, если
достигает
prob_stop=0.95 или использовано максимальное количество точек n_samp_max. Длительность эксперимента не фиксируется заранее. Миниальный размер выборки n_samp_min + n_samp_step. При остановке эксперимента для сравнения с -значением считается байесовское апострериорное распределение и вероятность
. Процедура повторяется
nexps раз, считается доля правильно угаданных групп во всех экспериментах. Из nexps = 1000 завершено 880, в них правильно угадано 818 вариантов. Точность 0.93 близка prob_stop = 0.95. -значения в каждом эксперименте близки байесовским вероятностям. При недостаточном
n_samp_min доля угаданных вариантов будет меньше prob_stop. Для байесовской модели вместо минимального количества точек можно задать меньшее значение - эффект будет близок заданию минимального размера выборки.
Nexp: 1000, Finished: 880, Correct Guesses: 818, Accuracy: 0.930
Доля групп с большим средним, верно угаданных t-тестом
cmp = pd.DataFrame(columns=['A', 'B', 'best_exact', 'exp_samp_size', 'A_exp', 'B_exp', 'best_exp', 'p_best_bayes', '1-pval'])
mu = 0.1
nexps = 1000
cmp['A'] = [mu] * nexps
cmp['B'] = mu * (1 + stats.uniform.rvs(loc=-0.05, scale=0.1, size=nexps))
cmp['best_exact'] = cmp.apply(lambda r: 'B' if r['B'] > r['A'] else 'A', axis=1)
n_samp_max = 3_000_000
n_samp_step = 10_000
n_samp_min = 100_000
prob_stop = 0.95
for i in range(nexps):
muA = cmp.at[i, 'A']
muB = cmp.at[i, 'B']
exact_dist_A = stats.norm(loc=muA, scale=1)
exact_dist_B = stats.norm(loc=muB, scale=1)
n_samp_current = n_samp_min
sampA = exact_dist_A.rvs(n_samp_max)
sampB = exact_dist_B.rvs(n_samp_max)
post_dist = None
mean_b_gt_a_bayes = np.nan
while n_samp_current < n_samp_max:
n_samp_current += n_samp_step
a = 'greater' if np.mean(sampA[:n_samp_current]) > np.mean(sampB[:n_samp_current]) else 'less'
t_stat, p_value = stats.ttest_ind(sampA[:n_samp_current], sampB[:n_samp_current], equal_var=False, alternative=a)
p_best_t = 1 - p_value
best_gr = 'A' if p_best_t >= prob_stop and a == 'greater' else 'B' if p_best_t >= prob_stop and a == 'less' else None
if best_gr:
post_dist = posterior_diff_scaled(sampA[:n_samp_current], sampB[:n_samp_current])
mean_b_gt_a_bayes = 1 - post_dist.cdf(0)
cmp.at[i, 'A_exp'] = sampA[:n_samp_current].mean()
cmp.at[i, 'B_exp'] = sampB[:n_samp_current].mean()
cmp.at[i, 'exp_samp_size'] = n_samp_current
cmp.at[i, 'best_exp'] = best_gr
cmp.at[i, 'p_best_bayes'] = max(mean_b_gt_a_bayes, 1 - mean_b_gt_a_bayes)
cmp.at[i, '1-pval'] = 1 - p_value
break
print(f'done {i}: nsamp {n_samp_current}, best_gr {best_gr}, Bayes P(b>a) {mean_b_gt_a_bayes:.4f}, T-test p-val {p_value:.4f}')
cmp['correct'] = cmp['best_exact'] == cmp['best_exp']
display(cmp.head(30))
finished = np.sum(cmp['best_exp'].notna())
cor_guess = np.sum(cmp['correct'])
print(f"Nexp: {nexps}, Finished: {finished}, Correct Guesses: {cor_guess}, Accuracy: {cor_guess / finished}")
Тест Хи-квадрат
Конверсии могут сравнивать -тестом [Chi2Test]. Статистика
Пирсона [Chi2Pearson] для мультиномиальных распределений определена
, где
- общее количество наблюдений,
и
- фактическое и ожидаемое количество наблюдений
-категории при доле
-категории
. Для биномиального распределения
. По центральной предельной теореме
стремится к стандартному нормальному распределению, квадрат этой величины совпадает с
. Распределение суммы квадратов
нормальных случайных величин называют
-распределением с
степенями свободы
[Chi2Dist]. Поэтому статистика
стремится к
-распределению.
Для А/Б-теста конверсий с двумя группами в предположении одинаковых ожидаемых конверсий тестовую статистику можно привести к виду
,
,
. При большом количестве точек распределение
близко
. Т.к. распределение
получается возведением в квадрат стандартного нормального распределения, область
-значения
соответствует областям
. По симметрии нормального распределения площади
и
одинаковы.
Для байесовской оценки вероятности конверсии одной группы больше другой правдоподобие в каждой группе задается биномиальным распределением
, априорное - бета-распределением
. Апостериорное будет бета-распределением с обовленными параметрами
. При типичных значениях
бета-распределение близко нормальному. Распределение разности конверсий также будет нормальным. При равномерных априорных распределениях и слабой разнице между группами
с параметрами
,
как в
-тесте. Вероятность конверсии одной группы больше другой будет площадью этого распределения в положительной области
.
По симметрии . Поэтому
.
Соотношение проверяется по выборке из двух распределений Бернулли с конверсиями
и
. Данные для
-теста задаются в виде таблицы со строками
и
[ScipyChi2Con].
-значение
-теста не позволяет выбрать между
и
, поэтому дополнительно сравниваются средние в выборках. Распределение
на первом графике ниже. Закрашенная область соответствует
-значению
. На втором графике закрашенные темные области
и
соответствуют области
-значения при возведении в квадрат. Серый график - нормальное распределение
. Закрашенная область серого графика приближенно равна
. Площади закрашенной серой и каждой из темных областей совпадают. Связь
-значения и байесовской оценки вероятности численно выполняется.
Распределение Хи-квадрат, p-значение и байесовская модель
#np.random.seed(27)
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)
def diff_ba_scaled(post_dist_A, post_dist_B):
m = post_dist_B.mean() - post_dist_A.mean()
v = post_dist_A.var() + post_dist_B.var()
return stats.norm(loc=m/np.sqrt(v), scale=1)
pA = 0.1
pB = pA * 1.03
exactA = stats.bernoulli(pA)
exactB = stats.bernoulli(pB)
N = 10000
sampA = exactA.rvs(size=N)
sampB = exactB.rvs(size=N)
SA = np.sum(sampA)
SB = np.sum(sampB)
t = np.array([
[SA, N - SA],
[SB, N - SB]
])
chi2_stat, p_value_chi2, dof, expected = stats.chi2_contingency(t, correction=False)
chi = np.sqrt(chi2_stat)
p_A_samp = SA / N
p_B_samp = SB / N
pb_gt_pa_chi = 1 - p_value_chi2 / 2
pb_gt_pa_chi = pb_gt_pa_chi if p_B_samp > p_A_samp else 1 - pb_gt_pa_chi
post_dist_A = posterior_dist_binom(ns=SA, ntotal=N)
post_dist_B = posterior_dist_binom(ns=SB, ntotal=N)
diff_dist_scaled = diff_ba_scaled(post_dist_A, post_dist_B)
pb_gt_pa_bayes = 1 - diff_dist_scaled.cdf(0)
xaxis_min = 0
xaxis_max = 5
x = np.linspace(xaxis_min, xaxis_max, 1000)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=stats.chi.pdf(x, df=1),
line_color='black', opacity=0.8, name=f'$\chi^2_1$'))
fig.add_trace(go.Scatter(x=[chi2_stat, chi2_stat], y=[0, max(stats.chi.pdf(x, df=1))*1.1],
line_color='black',
mode='lines+text', text=['', '$\chi^2$'], textposition="top center",
line_dash='dash', showlegend=False))
fig.add_trace(go.Scatter(x=x[x>chi2_stat], y=stats.chi.pdf(x[x>chi2_stat], df=1),
line_color='black', opacity=0.8, name='$P_{\chi_1^2}(x > \chi^2)$', fill="tozeroy", fillcolor="rgba(0, 0, 0, 0.7)"))
fig.update_layout(title='Хи-квадрат',
xaxis_title='$x$',
yaxis_title='Плотность вероятности',
xaxis_range=[xaxis_min, xaxis_max],
hovermode="x",
template="plotly_white",
height=500)
fig.show()
xaxis_min = -5
xaxis_max = 5
x = np.linspace(xaxis_min, xaxis_max, 1000)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=stats.norm.pdf(x, loc=0, scale=1),
line_color='black', opacity=0.8, name='$\mathrm{Norm}(0, 1)$'))
fig.add_trace(go.Scatter(x=[0, 0], y=[0, max(stats.norm.pdf(x, loc=0, scale=1))*1.1],
line_color='black',
mode='lines+text', text=['', '0'], textposition="top center",
line_dash='dash', showlegend=False))
fig.add_trace(go.Scatter(x=[chi, chi], y=[0, max(stats.norm.pdf(x, loc=0, scale=1))*1.1],
line_color='black',
mode='lines+text', text=['', '$\chi$'], textposition="top center",
line_dash='dash', showlegend=False))
fig.add_trace(go.Scatter(x=[-chi, -chi], y=[0, max(stats.norm.pdf(x, loc=0, scale=1))*1.1],
line_color='black',
mode='lines+text', text=['', '$-\chi$'], textposition="top center",
line_dash='dash', showlegend=False))
fig.add_trace(go.Scatter(x=x[x>chi], y=stats.norm.pdf(x[x>chi], loc=0, scale=1),
line_color='black', opacity=0.8, name='$P(\mathrm{Norm}(x > \chi \cup x < -\chi | 0, 1)$', fill="tozeroy", fillcolor="rgba(0, 0, 0, 0.7)"))
fig.add_trace(go.Scatter(x=x[x<-chi], y=stats.norm.pdf(x[x<-chi], loc=0, scale=1),
line_color='black', opacity=0.8, name='$P(\mathrm{Norm}(x > \chi | 0, 1)$', fill="tozeroy", fillcolor="rgba(0, 0, 0, 0.7)",
showlegend=False))
fig.add_trace(go.Scatter(x=x, y=diff_dist_scaled.pdf(x),
line_color='black', opacity=0.2, name=r'$\mathrm{Norm}(\chi, 1)$'))
fig.add_trace(go.Scatter(x=x[x<0], y=diff_dist_scaled.pdf(x[x<0]),
line_color="rgba(128, 128, 128, 0.2)", name=r'$P(\mathrm{Norm}(x < 0 | \chi, 1))$', fill="tozeroy", fillcolor="rgba(128, 128, 128, 0.2)"))
fig.update_layout(title=r'$P\text{-значение и байесовская модель}$',
xaxis_title='$x$',
yaxis_title='Плотность вероятности',
xaxis_range=[xaxis_min, xaxis_max],
hovermode="x",
template="plotly_white",
height=500)
fig.show()
print(f'Bayes P(pb > pa): {pb_gt_pa_bayes:.5g}')
print(f"Chi2: 1-pval/2: {pb_gt_pa_chi:.5g}")

-значение
-теста используется для выбора вариантов с большей конверсией в серии экспериментов. В каждом эксперименте 2 группы, конверсия
фиксирована,
выбирается случайно в диапазоне
от
. В каждой группе добавляются данные по
n_samp_step точек за шаг. На каждом шаге вычисляется -значение
-теста и
. Эксперимент останавливается, если оценка вероятности конверсии одной группы больше другой превышает
prob_stop или набрано максимальное количество точек n_samp_max. Всего из nexps=1000 завершено 986, верно угадано 940 варианта. Доля 0.953 близка prob_stop = 0.95.
Nexp: 1000, Finished: 986, Correct Guesses: 940, Accuracy: 0.953
Доля групп с большей конверсией, верно угаданных chi2-тестом
cmp = pd.DataFrame(columns=['A', 'B', 'best_exact', 'exp_samp_size', 'A_exp', 'B_exp', 'best_exp', 'p_best_bayes', 'p_best_chi'])
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['best_exact'] = cmp.apply(lambda r: 'B' if r['B'] > r['A'] else 'A', axis=1)
n_samp_max = 5_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']
exact_dist_A = stats.bernoulli(p=pA)
exact_dist_B = stats.bernoulli(p=pB)
n_samp_total = 0
ns_A = 0
ns_B = 0
while n_samp_total < n_samp_max:
dA = exact_dist_A.rvs(n_samp_step)
dB = exact_dist_B.rvs(n_samp_step)
n_samp_total += n_samp_step
ns_A = ns_A + np.sum(dA)
ns_B = ns_B + np.sum(dB)
p_A_samp = ns_A / n_samp_total
p_B_samp = ns_B / n_samp_total
t = np.array([
[ns_A, n_samp_total - ns_A],
[ns_B, n_samp_total - ns_B]
])
chi2_stat, p_value_chi, dof, expected = stats.chi2_contingency(t, correction=False)
pb_gt_pa_chi = 1 - p_value_chi / 2
pb_gt_pa_chi = pb_gt_pa_chi if p_B_samp > p_A_samp else 1 - pb_gt_pa_chi
best_gr = 'B' if pb_gt_pa_chi >= prob_stop else 'A' if 1 - pb_gt_pa_chi >= prob_stop else None
if best_gr:
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)
pb_gt_pa_bayes = 1 - diff_ba_scaled(post_dist_A, post_dist_B).cdf(0)
cmp.at[i, 'A_exp'] = p_A_samp
cmp.at[i, 'B_exp'] = p_B_samp
cmp.at[i, 'exp_samp_size'] = n_samp_total
cmp.at[i, 'best_exp'] = best_gr
cmp.at[i, 'p_best_bayes'] = max(pb_gt_pa_bayes, 1 - pb_gt_pa_bayes)
cmp.at[i, 'p_best_chi'] = max(pb_gt_pa_chi, 1 - pb_gt_pa_chi)
break
print(f'done {i}: nsamp {n_samp_total}, best_gr {best_gr}, P_best Bayes {max(pb_gt_pa_bayes, 1 - pb_gt_pa_bayes):.4f}, Chi (1-pval/2): {1 - p_value_chi/2:.4f}')
cmp['correct'] = cmp['best_exact'] == cmp['best_exp']
display(cmp.head(30))
finished = np.sum(cmp['best_exp'].notna())
cor_guess = np.sum(cmp['correct'])
print(f"Nexp: {nexps}, Finished: {finished}, Correct Guesses: {cor_guess}, Accuracy: {cor_guess / finished}")
U-критерий Манна-Уитни
Для выборок размера из двух случайных величин
статистика Манна-Уитни [MannWhitneyU] определена попарным сравнением элементов. Для непрерывных распределений вероятность совпадения элементов в выборках нулевая. В этом случае
-статистика определена как количество пар
, где элемент
больше
:
,
- индикаторная функция. Статистику также можно записать в виде
, где
- сумма рангов элементов
в объединенной выборке. Эквивалентность определений можно увидеть следующим образом: слагаемое
соответствует минимальной сумме рангов если все элементы
меньше
и считается как сумма арифметической прогрессии. Если наибольший элемент
больше
элементов
, то
и
. При большом количестве точек отношение
к общему количеству пар
стремится к вероятности точки из распределения
больше
.
В предположении одинаковых распределений можно посчитать среднее
и дисперсию
-статистики. Величина
будет стремиться к нормальному распределению. От количества пар удобно перейти к вероятности
.
-значение вычисляется как
при
или как площадь области
при
.
В байесовском подходе вероятность можно оценить сравнением апостериорных предиктивных распределений. Для этого нужно задать модели исходных распределений, построить апостериорные распределения параметров, апостериорные предиктивные распределения и по ним оценить
. Другой вариант, не требующий предположений о распределениях - моделировать вероятность
точки
больше
. Данными будут пары
, в каждой паре точка
больше
с вероятностью
, по общему количеству пар
и количеству пар
с
больше
нужно оценить
. Такая задача похожа на моделирование конверсий. Если при формировании пар из исходных выборок каждую точку
сравнивать с каждой точкой
, пары с одинаковыми
или
будут зависимы. Для независимости каждая точка
сравнивается только с одной точкой
. Аналогично конверсиям правдоподобие задается биномиальным распределением
, априорное - бета-распределением. Апостериорное также будет бета-распределением. Вероятность точки одной группы больше другой равна области
больше 0.5:
.
В -статистике каждая точка
сравнивается с каждой
, в байесовской модели точки
участвуют только в одном сравнении. Из-за меньшего использования данных дисперсия байесовской модели шире
-статистики.
-значение не будет близко совпадать с байесовской вероятностью
. При большом количестве точек
. В байесовской модели при слабом отличии между группами
,
Поэтому
.
Проверка связи -значения с байесовской вероятностью сделана на примере нормальных распределений. Вероятность точки из распределения
больше
равна вероятности разности
больше нуля
. Разность нормальных распределений также нормальное распределение со средним, равным разности исходных средних, и дисперсией, равной сумме дисперсий [NormalSum].
На первом графике ниже показаны исходные нормальные распределения со средними mu1=0, mu2=0.1 и единичной дисперсией. Темная линия на втором графике - в предположении эквивалентности распределений. Темная закрашенная область соответствует
-значению [ScipyMannWhitneyU]. Светлая линия показывает байесовскую модель, светлая закрашенная область - байесовскую вероятность
. Байесовская модель шире
из-за большей дисперсии. Площадь светлой области обычно больше темной, что соответствует
.
U-статистика и байесовская модель
#np.random.seed(34)
mu1, sigma1 = 0.0, 1
mu2, sigma2 = 0.1, 1
exactA = stats.norm(loc=mu1, scale=sigma1)
exactB = stats.norm(loc=mu2, scale=sigma2)
p_b_gt_a_exact = 1 - stats.norm.cdf(0, loc=mu2-mu1, scale=np.sqrt(sigma1**2 + sigma2**2))
nA = nB = 1000
sampA = exactA.rvs(nA)
sampB = exactB.rvs(nB)
U, pval = stats.mannwhitneyu(sampA, sampB, alternative='greater')
pval = pval if U / (nA * nB) > 0.5 else 1 - pval
varu = nA * nB * (nA + nB + 1) / 12
p_u = stats.norm(loc=0.5, scale=np.sqrt(varu/(nA*nA*nB*nB)))
ua = U / (nA*nB)
a0 = 1
b0 = 1
S = np.sum(sampB > sampA)
post_theta = stats.beta(a0 + S, b0 + nB - S)
x = np.linspace(-7, 7, 1000)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=x, y=exactA.pdf(x),
mode='lines', name='A', line_color='black'))
fig.add_trace(go.Scatter(
x=x, y=exactB.pdf(x),
mode='lines', name='B', line_color='blue', opacity=0.7))
fig.update_layout(
title="A, B",
template="plotly_white"
)
fig.show()
xaxis_min = 0.4
xaxis_max = 0.6
x = np.linspace(xaxis_min, xaxis_max, 1000)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=p_u.pdf(x),
line_color='black', opacity=0.8, name=f'$U_A/N_A N_B$'))
fig.add_trace(go.Scatter(x=x[x<ua], y=p_u.pdf(x[x<ua]),
line_color='black', opacity=0.8, name=r'$p = P(x < u_A | H_0)$',
fill="tozeroy", fillcolor="rgba(0, 0, 0, 0.7)"))
fig.add_trace(go.Scatter(x=x, y=post_theta.pdf(x),
line_color='black', opacity=0.3, name=r'$P(\theta | S, N)$'))
fig.add_trace(go.Scatter(x=x[x<0.5], y=post_theta.pdf(x[x<0.5]),
name=r'$P(\theta < 0.5 | S, N)$',
line_color="rgba(128, 128, 128, 0.3)",
fill="tozeroy", fillcolor="rgba(0, 0, 0, 0.3)"))
fig.add_trace(go.Scatter(x=[1-ua, 1-ua], y=[0, max(p_u.pdf(x))*1.1],
line_color='black',
mode='lines+text', text=['', '$1-u_A$'], textposition="top center",
line_dash='dash', showlegend=False))
fig.add_trace(go.Scatter(x=[ua, ua], y=[0, max(p_u.pdf(x))*1.1],
line_color='black',
mode='lines+text', text=['', '$u_A$'], textposition="top center",
line_dash='dash', showlegend=False))
fig.add_trace(go.Scatter(x=[0.5, 0.5], y=[0, max(p_u.pdf(x))*1.1],
line_color='black',
mode='lines+text', text=['', '$0.5$'], textposition="top center",
line_dash='dash', showlegend=False))
fig.update_layout(
title=r'$U\text{-статистика и байесовская модель}$',
template="plotly_white"
)
fig.show()
print(f'CDF(u_a): {p_u.cdf(ua):.5f}')
print(f'scipy min(pval, 1-pval): {min(pval, 1-pval):.5f}')
print(f'Bayes P(theta < 0.5) {post_theta.cdf(0.5):.5f}')
print()
print(f'P(B>A) exact: {p_b_gt_a_exact:.5f}')
print(f'1 - U/(NA NB): {1-ua:.5f}')
print(f'Bayes E[theta] {post_theta.mean():.5f}')

Для байесовской модели и -статистики проверяется доля правильно угаданных вариантов с большей вероятностью
в серии экспериментов. В каждом эксперименте задается два нормальных распределения. Среднее
фиксировано
mua = 0.1, среднее выбирается случайно в пределах
от
. Вначале проверяется байесовская модель. Всего проводится
nexp экспериментов, в группы добавляются данные по n_samp_step точек за раз. На каждом шаге считается байесовсая вероятность . Эксперимент останавливается, если
или
превышает
prob_stop. В обоих подходах - байесовском и -тесте - нужно задавать минимальный размер выборки для выполнения заданного уровня точности. В байесовской модели вместо минимальной выборки задаются априорные параметры
a0 = 100000, b0 = 100000. Из 1000 экспериментов завершено 860, корректно угадано 827. Точность 0.96 близка заданной prob_stop = 0.95. При остановке в каждом эксперименте считается -статистика, она как правило превышает байесовскую вероятность.
Nexp: 1000, Finished: 860, Correct Guesses: 827, Accuracy: 0.962
Доля групп, верно угаданных байесовской моделью
cmp = pd.DataFrame(columns=['A', 'B', 'best_exact', 'exp_samp_size', 'A_exp', 'B_exp', 'best_exp', 'p_best_bayes', 'p_best_u'])
mua = 0.1
nexps = 1000
cmp['A'] = [mua] * nexps
cmp['B'] = mua * (1 + stats.uniform.rvs(loc=-0.05, scale=0.1, size=nexps))
cmp['best_exact'] = cmp.apply(lambda r: 'B' if r['B'] > r['A'] else 'A', axis=1)
n_samp_max = 5_000_000
n_samp_step = 10000
prob_stop = 0.95
for i in range(nexps):
mua = cmp.at[i, 'A']
mub = cmp.at[i, 'B']
exact_dist_A = stats.norm(loc=mua)
exact_dist_B = stats.norm(loc=mub)
n_samp_total = 0
sampA = exact_dist_A.rvs(n_samp_max)
sampB = exact_dist_B.rvs(n_samp_max)
while n_samp_total < n_samp_max:
n_samp_total += n_samp_step
a0 = 100000
b0 = 100000
Ub = np.sum(sampB[:n_samp_total] > sampA[:n_samp_total])
post_u_ewise = stats.beta(a0 + Ub, b0 + n_samp_total - Ub)
pb_gt_pa_bayes = 1 - post_u_ewise.cdf(0.5)
best_gr = 'B' if pb_gt_pa_bayes >= prob_stop else 'A' if 1 - pb_gt_pa_bayes >= prob_stop else None
if best_gr:
U, pval = stats.mannwhitneyu(sampA[:n_samp_total], sampB[:n_samp_total], alternative='greater')
cmp.at[i, 'A_exp'] = sampA[:n_samp_total].mean()
cmp.at[i, 'B_exp'] = sampB[:n_samp_total].mean()
cmp.at[i, 'exp_samp_size'] = n_samp_total
cmp.at[i, 'best_exp'] = best_gr
cmp.at[i, 'p_best_bayes'] = max(pb_gt_pa_bayes, 1 - pb_gt_pa_bayes)
cmp.at[i, 'p_best_u'] = max(pval, 1 - pval)
break
print(f'done {i}: nsamp {n_samp_total}, best_gr {best_gr}, P_best Bayes {pb_gt_pa_bayes:.4f}, U p-val: {pval:.4f}')
cmp['correct'] = cmp['best_exact'] == cmp['best_exp']
display(cmp.head(30))
finished = np.sum(cmp['best_exp'].notna())
cor_guess = np.sum(cmp['correct'])
print(f"Nexp: {nexps}, Finished: {finished}, Correct Guesses: {cor_guess}, Accuracy: {cor_guess / finished}")
Для проверки правильно угаданных вариантов по -статистике выставлен больший шаг - добавляется по
n_samp_step = 200_000 точек в каждой группе. Такой шаг также учитывает минимальный размер выборки. Из 300 экспериментов завершено 262, верно угадано 249 групп. Доля верно угаданных вариантов 0.95 совпала с prob_stop=0.95.
Nexp: 300, Finished: 262, Correct Guesses: 249, Accuracy: 0.950
Доля групп, верно угаданных U-тестом
cmp = pd.DataFrame(columns=['A', 'B', 'best_exact', 'exp_samp_size', 'A_exp', 'B_exp', 'best_exp', 'p_best_bayes', 'p_best_u'])
mua = 0.1
nexps = 300
cmp['A'] = [mua] * nexps
cmp['B'] = mua * (1 + stats.uniform.rvs(loc=-0.05, scale=0.1, size=nexps))
cmp['best_exact'] = cmp.apply(lambda r: 'B' if r['B'] > r['A'] else 'A', axis=1)
n_samp_max = 5_000_000
n_samp_step = 200_000
prob_stop = 0.95
for i in range(nexps):
mua = cmp.at[i, 'A']
mub = cmp.at[i, 'B']
exact_dist_A = stats.norm(loc=mua)
exact_dist_B = stats.norm(loc=mub)
n_samp_total = 0
sampA = exact_dist_A.rvs(n_samp_max)
sampB = exact_dist_B.rvs(n_samp_max)
while n_samp_total < n_samp_max:
n_samp_total += n_samp_step
U, pval = stats.mannwhitneyu(sampA[:n_samp_total], sampB[:n_samp_total], alternative='greater')
pb_gt_pa_u = 1 - U / n_samp_total / n_samp_total
best_gr = 'B' if pval >= prob_stop else 'A' if 1 - pval >= prob_stop else None
if best_gr:
a0 = 100000
b0 = 100000
Ub = np.sum(sampB[:n_samp_total] > sampA[:n_samp_total])
post_u_ewise = stats.beta(a0 + Ub, b0 + n_samp_total - Ub)
pb_gt_pa_bayes = 1 - post_u_ewise.cdf(0.5)
cmp.at[i, 'A_exp'] = sampA[:n_samp_total].mean()
cmp.at[i, 'B_exp'] = sampB[:n_samp_total].mean()
cmp.at[i, 'exp_samp_size'] = n_samp_total
cmp.at[i, 'best_exp'] = best_gr
cmp.at[i, 'p_best_bayes'] = max(pb_gt_pa_bayes, 1 - pb_gt_pa_bayes)
cmp.at[i, 'p_best_u'] = max(pval, 1 - pval)
break
print(f'done {i}: nsamp {n_samp_total}, best_gr {best_gr}, P_best Bayes {pb_gt_pa_bayes:.4f}, U p-val: {pval:.4f}')
cmp['correct'] = cmp['best_exact'] == cmp['best_exp']
display(cmp.head(30))
finished = np.sum(cmp['best_exp'].notna())
cor_guess = np.sum(cmp['correct'])
print(f"Nexp: {nexps}, Finished: {finished}, Correct Guesses: {cor_guess}, Accuracy: {cor_guess / finished}")
Для -теста,
-теста и
-критерия Манна-Уитни показаны байесовские модели с вероятностью лучшей группы численно близкой
-значениям. Соотношения выполняются несмотря на различия в определениях
-значений и байесовских вероятностей.
Ссылки
[Chi2Dist] - Chi-squared Distribution, Wikipedia.
[Chi2Pearson] - Pearson’s Chi-squared Test, Wikipedia.
[Chi2Test] - Chi-squared Test, Wikipedia.
[ConjPrior] - Conjugate Prior, Wikipedia.
[MannWhitneyU] - Mann–Whitney U Test, Wikipedia.
[NormalSum] - Sum of Normally Distributed Random Variables, Wikipedia.
[PVal] - P-value, Wikipedia.
[ScipyChi2Con] - scipy.stats.chi2_contingency, SciPy Reference.
[ScipyMannWhitneyU] - scipy.stats.mannwhitneyu, SciPy Reference.
[ScipyTTestInd] - scipy.stats.ttest_ind, SciPy Reference.
[TailedTests] - One- and Two-tailed Tests, Wikipedia.
[TDist] - Student’s t-distribution, Wikipedia.
[TestStat] - Test Statistic, Wikipedia.
[TTest] - Student’s t-test, Wikipedia.
[WelchT] - Welch’s t-test, Wikipedia.
