Фреймворк для дизайна A/B-теста
Сегодня мы рассмотрим простой базовый фреймворк для дизайна сплит-теста, который можно удобно использовать продуктовым аналитикам в своей работе. Разберем использование этого фреймворка, его теоретическую и математическую основу, и также поговорим о продуктовых аспектах заведения A/B-тестов — когда продакту и аналитику заводить A/B-тест не нужно. Вам понадобятся: представления о продуктовых метриках, знания python, первичные представления о математической статистике и чуточку воображения.
Варианты созрели для сравнения…
Содержание
С чего начать работу над дизайном теста
Любой сплит-тест начинается с обсуждения цели и проблемы, которую продакт и аналитик хотят решить в рамках его проведения: в процессе обсуждения может выясниться, что проводить тест совсем не нужно, и необходимые выводы можно получить на основе результатов исследования ретроспективных данных — всегда нужно помнить, что сплит-тесты — это дорого и долго, и если можно обойтись без них, то лучше не задействовать этот механизм.
Обсудив с вашим продуктовым менеджером проблемы и цели и убедившись, что проведение теста необходимо, перейдите к формализации тестируемых гипотез, созданию макетов и MVP тестируемых вариантов представления продукта для пользователя и техническим ограничениям, описание которых должно присутствовать в документации на дизайн A/B.
Технические ограничения могут исходить из:
потребностей продукта: выбраны корректные сегменты аудитории по соцдему, географии, используемым платформам (iOS, android или web), версиям приложения и т.д.,
здравого смысла: тест не может идти год и метрики подобраны с учетом этого, тестируемые гипотезы действительно влияют на результат и принятые по результатам теста решения можно будет физически раскатить в продукте на всех пользователей,
математических предпосылок и возможностей разработки: рассчитанные необходимые объемы выборок достяжимы, дизайн-макеты и MVP соответствуют тестируемым гипотезам, проведена проверка корректности системы логирования и системы сплитования.
Общий алгоритм действий
После того, как вы определились с целями теста и получили дизайн-макеты вносимых изменений, можно переходить к подготовке аналитической части. Общий алгоритм будет следующим:
Сформулируйте гипотезы в общем виде, исходя из продуктовой цели и переложите их на более математически точную формулировку.Например, изменение модели ранжирования статей в поиске приложения улучшает пользовательский опыт в приложении засчет улучшения полезности выдачи, а именно увеличивает: конверсию в переход на чтение статьи и timespent пользователя на странице статей в приложении, уменьшает число в поддержку жалоб на нерелевантные результаты поиска.
Выберите метрики, у вас должны быть:
— core-метрика или метрики — 1–2 ключевые метрики, на основе которых Вы в первую очередь будете принимать решение по результатам теста, например, конверсия и AOV (средний чек) или RPU, или CPU и GMV и так далее. Это должны быть метрики, которые подтверждают, или опровергают сформулированные в пункте 1 гипотезы;
— warning-метрика: метрика, при выходе (росте или падении) которой за определенный установленный порог и сохранении этого роста или падения на протяжении заранее выбранного времени тест отключается, например: при уменьшении GMV в тестовой группе на 5+% и сохранении этого падения на протяжении 2-х дней тест отключается до его окончания;
— proxy-метрика или метрики — это метрики, которые коллинеарны core-метрике или core-метрикам и косвенно служат подтверждению изменения core-метрик в том или ином направлении. Рroxy-метрики необходимы в случае, если core-метрики недостаточно чувствительны и не позволяют за разумное время определиться с результатами по тесту (или набрать необходимый объем выборок для core-метрик в принципе невозможно), тогда решение по тесту придется принимаются на основе proxy-метрик.Зафиксируйте значения alpha-уровня, мощности.Определитесь со значениями уровня статистической значимости alpha и мощности для ошибок I рода и II рода: стандартно выбирают alpha = 0,05 и (1-beta) = 80%.
Выберите значения ожидаемого лифта и MDE для заданной метрики или метрик, где лифт — это значение ожидаемой разницы (прироста или падения) метрики в тестовой группе относительно контрольной, выбранное на основе экспертных мнений или рассчитанное на основе ретроспективных данных. На основе лифта рассчитайте размер эффекта, также именуемого Cohen«s d, и зафиксируйте MDE (минимально детектируемый эффект) — его минимально достяжимое значение. Формулы для этих показателей представлены в следующем разделе — Quick Guide по математике.
Определите минимально необходимый размер выборки.Рассчитайте на основе зафиксированных ранее alpha-уровня, мощности, минимально детектируемого эффекта MDE необходимый размер выборки, если размер выборки недостяжим или требует неадекватного большого периода накопления данных, произведите симуляцию для нескольких вариантов размеров групп и MDE, выберете наиболее подходящий.
Рассчитайте время, необходимое для проведения теста. Исходя из полученных в предыдущем пункте значений размеров групп определите период проведения теста для накопления данных;
Убедитесь в корректно настроке логирования.Проверьте наличие логов и полноту и качество логирования до старта теста и трекайте показатели и качество логирования в процессе тестирования новой версии. Для этого настройте базовые дашборды, отчеты или витрины, которые обеспечат оперативный доступ к необходимым показателям и мониторинг прогресса тестирования (избегайте эффекта подглядывания и дождитесь накопления необходимого объема выборки)
Настройте оперативный мониторинг Warning-метрики и alerting по её выходам за установленный порог, threshold, что позволит в краткий период времени узнать о резком или долговременном падении или скачке Warning-показателя и при необходимости своевременно отключить тест.
Проведите проверку корректности системы сплитования и адекватности выбранных статистических критериев, выбрав две идентичные контрольные группы и проведя предварительный набор A/A-тестов с расчетом p-value, которое в случае корректности сплитования и выбора критериев будет распределено равномерно, чтобы заранее убедиться в корректности системы сплитования.
Запускайте тест, Вы готовы.
Quick Guide по математике, лежащей в основе A/B-тестирования
Гипотезы представляют собой утверждение о равенстве или различии метрик и тестируются на основе подсчета разницы статистик, метрик в группах, как правило состоящих из независимых наблюдений, которыми обычно выступают пользователи. Гипотезы могут быть односторонними и двусторонними.
Двусторонняя гипотеза формулируется следующим образом:
Нулевая и альтернативная гипотезы
При тестировании гипотез возможно возникновение ошибки, погрешности, вызванные тем, что мы работаем не со всей генеральной совокупностью, а с ограниченными выборками из неё, которые хотя и описывают генеральную совокупность, но всё же имеют определенные ограничения — содержат не все возможные варианты сочетаний параметров, не все значения метрик для элементов и т.д. В результате возникновения ошибки смещения может быть принята неверная гипотеза, типы ошибок и характер их возникновения указаны в таблице ниже.
Таблица. Типы ошибок при тестировании гипотез и характер их возникновения
p-value — это вероятность того, что значение статистики распределения будет такое же или будет более экстремальное по сравнению с ранее наблюдаемым. Решение о принятии или отклонении нулевой гипотезы на основе условия:
alpha — это уровень статистической значимости, или вероятность ошибки I рода, то есть вероятность отклонить нулевую гипотезу, когда она верна («ложная тревога»), beta — это вероятность ошибки II рода, то есть вероятность принять нулевую гипотезу, когда она неверна (пропустить цель). Визуально соотношение alpha и beta можно представить на гистрограмме:
Соотношение alpha, beta и размера эффекта Cohen«s d
Размер эффекта, как можно видеть на гистограмме, также зависит от уровня статистической значимости, выбранной мощности и дисперсии выборки, а значит её размеров. Таким образом размер эффекта можно вычислить исходя из заданных на старте расчетов параметров alpha, (1-beta) и межгрупповой дисперсии, или фактически ожидаемого лифта (прироста или падения) в метрике, и вычислить MDE по формуле:
Метрики разницы-лифта между группами, размера эффекта и MDE в тесте
Как можно видеть на графике гистограмм, вычисление MDE фактически сводится к вычислению разницы средних двух распределений метрики в двух группах при заданных alpha и beta при критическом уровне p-value = alpha. Исходя из этого, в качестве методики расчета можно выбрать симуляцию проведения большого числа A/A-тестов методом Монте-Карло на исторических данных, затем построить распределение статистики разницы средних в симуляциях (фактически аналог Эфроновского подсчета p-value) и выбрать на основе этого распределения минимально необходимый размер выборки, который будет равен числу пользователей в выборке симуляции.
Почему это так: если в выборке N пользователей на A/A-тесте группы показали текущее распределение размеров эффекта и вычисленный на его основе MDE, то при большем числе пользователей в группах они тем более его покажут, то есть MDE корректный.
Распределения p-value для решения о принятии или отклонении нулевой гипотезы
Принцип вычисления MDE и принятия решений на основе Эфроновского подсчета p-value показан на гистограмме: если при двусторонней гипотезе распределение статистики не касается нуля в процентилях [0; alpha*0.5] и [100-alpha*0.5;100], то принимается нулевая гипотеза.
Следует отметить, что в качестве статистики размера эффекта может выступать не только разница, но и отношений как средних величин, так и относительный прирост, и также в качестве исследуемой метрики могут выступать как классически поюзерные метрики, агрегированные на пользователя значения, так и конверсии. Перед расчетом конверсий их можно привести к поюзерной при помощи метода линеаризации или дельта-метода.
Зачем же в принципе нужно рассчитывать размеры выборок? Чтобы избежать ложного срабатывания критерия на недостаточном объеме данных, необходимо рассчитать объемы групп, меньше которых статистическая значимость не может быть достигнута и значение p-value ≤ alpha следует признавать случайным — это частая ошибка, вызванная подглядыванием за p-value — на протяжении теста с накоплением данных p-value может «гулять» вокруг alpha, пока не выйдет на плато. Примеры на практике:
Изменение p-value в течении теста — материал из статьи Uchi.ru
Проверка корректности системы сплитования и адекватности используемого критерия также происходит на основе проведения большого числа A/A-тестов, например, 100 или 1000. Если сплитование или критерий работают корректно, число A/A-тестов с принятой по правилу p-value ≤ alpha альтернативной гипотезой не должно превышать alpha-уровень и, соответственно, p-value должно быть распределено равномерно, примеры корректного и некорректного распределения p-value в тестах:
Сплитование и критерий работают корректно: p-value на 200 A/A-тестах для метрики распределен равномерно
Сплитование и критерий работают некорректно: p-value на 200 A/A-тестах для метрики, имеющей выброс, распределен ближе нормально
Теперь приступим к практической части расчета размера минимально детектируемого эффекта и необходимого объема выборок для его достяжения.
Расчет необходимого объема выборки
Процедура расчета необходимого объема выборки основана на симуляции методом Монте-Карло, в рамках которой происходит последовательная генерация идентичных выборок в режиме A/A теста путем выбора из генеральной совокупности наблюдений случайным образом с возвращением.
Этот метод называется bootstrap, при большом объеме данных и недостаточно производительности машин или кластера для расчетов его можно заменить на метод бакетирования или саббакетирования.
def bootstrap(test, control, iterations = 1000):
iterations = iterations
size = min(len(test), len(control))
t_boots = [random.choices(test, weights=None, k=size) for _ in range(iterations)]
c_boots = [random.choices(control, weights=None, k=size) for _ in range(iterations)]
return t_boots, c_boots
Для ratio-метрик метод будет иметь чуть измененный вид:
def bootstrap_ratio(test, control, iterations = 1000):
iterations = iterations
t_boots = [random.choices(test, weights=None, k=len(test)) for _ in range(iterations)]
c_boots = [random.choices(control, weights=None, k=len(control)) for _ in range(iterations)]
return t_boots, c_boots
Зададим функции агрегации и расчета статистики для сравнения разницы метрик в группах:
### 1. Функции аггрегации
def agg_sum(array):
return [sum(i) for i in array]
def agg_avg(array):
return [sum(i)/len(i) for i in array]
def agg_median(array):
return [np.median(i) for i in array]
### 2. Функции расчета статистики
def diff(test, control):
return [test_i - control_i for (test_i,control_i) in zip(test,control)]
def compare(test, control):
return [test_i/control_i for (test_i,control_i) in zip(test,control)]
def perc_compare(test, control):
return [(test_i/control_i-1)*100 for (test_i,control_i) in zip(test,control)]
У нас есть некоторый объем аудитории, рассчитанный за выбранны период времени — например, месяц — разобьём её на 100 бакетов случайным образом. Для расчета минимально необходимой выборки для проведения теста мы будем генерировать подвыборки разного объема, соответствующего 5%, 10%, 15% и так далее до 50% MAU и смотреть за изменением показателя минимального лифта и MDE:
# Процент из генеральной совокупности
slices = [5,10,20,30,40,50]
minimum_lift = []
mde = []
n_items = []
users = []
for i in slices:
test = data[data['bucket_id']=i) &\
(df['bucket_id']<2*i)][metric].to_list()
t_list, c_list = bootstrap(test, control)
# Разница средних
result = diff(agg_avg(t_list), agg_avg(c_list))
minimum_lift.append((np.quantile(result, 0.975) - np.quantile(result, 0.025))/2)
mde.append( minimum_lift/np.std(c_list))
n_items.append(i)
users.append(min(len(test), len(control)))
pd.DataFrame({'minimum_lift': minimum_lift, 'mde': mde,
'n_items': n_items, 'users': users})
В результате расчетов получаем таблицу со значениями минимального эффекта, разницы-лифта и доли аудитории, которую необходимо задествовать в тесте для достяжения данного эффекта, по этой таблице мы можем выбрать интересующее нас сочетания размера эффекта и объема групп, которое выглядит для нас наиболее приемлемым, вот пример такой таблицы:
Убедимся, что в симуляции A/A-теста действительно не наблюдаются статистически начимые изменения, для этого визуализируем результат на итерации с 10%:
plt.figure(figsize=(12,5))
plt.hist(result, bins=50)
plt.vlines([np.quantile(result,q=0.975),np.quantile(result,q=0.025),
np.quantile(result,q=0.5)],ymin=0,ymax=50,
linestyle='--', color='black')
plt.show()
Распределение метрики разницы средних в A/A-симуляции
Видим, как и ожидали, что распределение Эфроновской статистики проходит через ноль.
Сведем расчеты в единую функцию bootstrap:
def bootstrap(test, control, func_agg, func_diff,
ratio=False, n_iterations=1000):
function_agg = func_agg
function_diff = func_diff
size = min(len(test), len(control))
t_list = []
c_list = []
np.random.seed(seed=42)
for _ in range(n_iterations):
if ratio:
test_agg = np.random.choice(a=test, size=len(test), replace=True)
control_agg = np.random.choice(a=control, size=len(control), replace=True)
else:
test_agg = np.random.choice(a=test, size=size, replace=True)
control_agg = np.random.choice(a=control, size=size, replace=True)
t_list.append(function_agg(test_agg))
c_list.append(function_agg(control_agg))
t_list = np.array(t_list)
c_list = np.array(c_list)
result = function_diff(t_list, c_list)
ci_all = np.quantile(result, 0.025).round(2),
np.quantile(result, 0.975).round(2)
minimum_lift = (np.quantile(result, 0.975) - np.quantile(result, 0.025))/2
mde = minimum_lift/np.std(c_list)
users = min(len(test), len(control))
return ci_all, minimum_lift, mde, result, users
Теперь у нас есть универсальная функция, позволяющая рассчитать как необходимый размер групп для проведения теста, так и обсчитать результаты самого теста по его завершении.
Расчет необходимого времени работы теста
На основе рассчитанного размера выборки, минимально необходимого для теста, и априорном знании о динамике WAU и MAU и значениях (неаномальных) WAU и MAU за предыдущий месяц на сервисе можно рассчитать сроки проведения теста:
_, _, _, _, users = bootstrap(test, control, agg_avg, diff,
ratio=False, n_iterations=1000)
mau = data_monthly['user_id'].nunique()
wau = data_weekly['user_id'].nunique()
days_weeks = users/wau*7
days_m = users/mau*30
print(f'Period from {min_days} to {max_days} days needed'.format(
min_days=min(days_weeks, days_m),
max_days=min(days_weeks, days_m)))
Теперь мы можем оценить, сколько примерно времени необходимо для достяжения необходимого размера групп и наблюдений в них.
В качестве послесловия
Рассмотренный выше фреймворк довольно полно описывает процесс дизайна A/B-теста, универсален и охватывает большую часть аспектов построения грамотного процесса тестирования, не включая только вопросы тестирования множественных гипотез. Приведенный в статье метод определения объемов выборок применим для разных типов статистик. Надеюсь, материал был для вас полезен и интересен, и поможет на практике.
За вдохновение на работу над статьей и методами, разработанными в ней, особенно хочется отметить моего замечательного руководителя Дениса Самойлова из Яндекс.Доставки и мою коллегу Динару Исхакову, а также потрясающих экспертов в сфере split-тестирования Вита Черемисинова и Искандера Мирмахмадова. Всем Data Cheers :)
Полезная литература
https://expf.ru/ab_course
https://www.geo.fu-berlin.de/en/v/soga-py/Basics-of-statistics/Hypothesis-Tests/Introduction-to-Hypothesis-Testing/Critical-Value-and-the-p-Value-Approach/index.html
https://habr.com/ru/companies/uchi_ru/articles/500918/
https://www.microsoft.com/en-us/research/group/experimentation-platform-exp/articles/p-values-for-your-p-values-validating-metric-trustworthiness-by-simulated-a-a-tests/