Как улучшить ваши A/B-тесты: лайфхаки аналитиков Авито. Часть 1
Всем привет! Я Дмитрий Лунин, работаю аналитиком в команде ценообразования Авито. Наш юнит отвечает за все платные услуги площадки. К примеру, услуги продвижения или платные размещения для профессиональных продавцов. Наша основная задача — сделать цены на них оптимальными.
Мы не только пытаемся максимизировать выручку Авито, но и думаем про счастье пользователей. Если установить слишком большие цены, то пользователи возмутятся и начнут уходить с площадки, а если сделать цены слишком маленькими, то мы недополучим часть оптимальной выручки. Низкие цены также увеличивают количество «спамовых» объявлений, которые портят поисковую выдачу пользователям. Поэтому нам очень важно уметь принимать математически обоснованные решения — любая наша ошибка напрямую отразится на выручке и имидже компании.
Одним из инструментов для решения наших задач является A/B-тестирование.
Это статья для вас, если хоть что-то из перечисленного далее вас заинтересовало:
Вы очень часто получаете не статистически значимые результаты в A/B-тестах, и вас это не устраивает. Вы хотите как-то изменить процедуру проведения и анализа A/B-тестов, чтобы начать детектировать больше статистически значимых результатов.
Для вас я расскажу про CUPED, про бутстрап-критерии для тестирования более чувствительных гипотез, про стратификацию, и про очень простой метод деления выборок на тест и контроль, который позволит вам значительно улучшить результаты будущих A/B-тестов. А ещё продемонстрирую результаты сравнения всех этих методов на наших данных.
Вы не получили статистически значимый результат и не знаете, что делать, ведь он ни о чём не говорит.
Я покажу, что это не так, и на самом деле вы можете получить некоторые инсайты даже из таких данных.
Чтобы сделать тест более устойчивым к выбросам, вы используете критерий Манна-Уитни, логарифмирование метрики или просто выкидываете выбросы.
Остановитесь! Я расскажу, к чему это может привести. А ещё поделюсь корректным методом борьбы с выбросами.
Вы задумывались, корректно ли работают статистические критерии, которые вы используете для анализа A/B-тестов. Или, к примеру, собираетесь начать использовать новый метод для анализа экспериментов, но не уверены в его корректности.
Я поделюсь, как проверить ваш метод, попутно доказав корректность всех методов, описанных в статье. А также развею миф, что T-test можно использовать только для выборок из нормального распределения.
Вы хотели бы отдавать стейкходерам более интерпретируемые результаты A/B-тестов. Не просто фразу: «выручка статистически значимо стала лучше, чем была», а ещё и численные значения плана +100±10 ₽.
Но даже такой результат не самый понятный и интерпретируемый: вы не можете сказать, 100 рублей — это много или мало для компании. Я расскажу, как решить эту проблему и сделать результаты эксперимента наиболее наглядными.
И в качестве последней плюшки: практически все описанные в статье методы будут иметь реализацию на Python. Если вы захотите использовать их у себя в компании, то у вас будет готовый стартовый пример.
Материала очень много, поэтому я разделил статью на две части. Большинство пунктов из списка выше будут затронуты в первом материале. Во второй части сосредоточимся на увеличении мощности ваших A/B–тестов.
В первой части статьи рассмотрим:
Гипотезы, которые мы проверяем в A/B-тестах.
Критерий T-test и как провалидировать, что вы можете использовать его или любой критерий на ваших данных.
О чём говорят серые метрики.
Как работать с выбросами в A/B-тестах.
Терминология
Иногда я буду использовать нашу терминологию, связанную с A/B-тестами. Чтобы она была вам понятна, приведу основные понятия. Не все они будут в статье, но их полезно знать:
Статистически значимый результат — результат, который статистически значимо лучше 0.
Прокрас теста — результат эксперимента статистически значимо отличается от 0, и у вас есть какой-то эффект.
Зелёный тест — метрика в A/B-тесте статистически значимо стала лучше.
Красный тест — метрика в A/B-тесте статистически значимо стала хуже.
Серый тест — результат A/B-теста не статистически значим.
Тритмент — фича или предложение, чьё воздействие на пользователей вы проверяете в A/B-тесте.
MDE — минимальный детектируемый эффект. Размер, который должен иметь истинный эффект от тритмента, чтобы эксперимент его обнаружил с заданной долей уверенности (мощностью). Чем меньше MDE, тем лучше.
Мощность критерия — вероятность критерия задетектировать эффект, если он действительно есть. Чем больше мощность критерия, тем он круче.
Предпериод — период до начала эксперимента.
Какие гипотезы мы проверяем в A/B-тестах
Давайте поговорим о том, что мы вообще хотим получить от A/B-теста: какие гипотезы проверяем и как лучше преподнести результаты стейкхолдерам, чтобы они были более понятными и интерпретируемыми. Это будет та основа, на которой будет строиться материал всех последующих разделов статьи.
Абсолютная постановка в A/B-тестах
Начнём с азов A/B-тестов: что мы вообще тестируем с математической точки зрения.
H_0 — нулевая гипотеза в A/B-тестировании, которую, в основном, мы хотим отвергнуть. Чаще всего эта гипотеза отвечает за то, что эффекта в A/B-тесте нет.
H_1 — альтернативная гипотеза в A/B-тестировании, которую, наоборот, мы хотим подтвердить. Эта гипотеза отвечает за то, что эффект в A/B-тесте есть.
T — тестовая выборка. Одно значение в выборке — значение целевой метрики у пользователя в тесте. Например, количество просмотров нашего сайта или суммарная выручка от пользователя.
C — контрольная выборка. —//— в контроле.
EC — математическое ожидание в контроле.
ET — математическое ожидание в тесте.
Черта над T и С означает среднее этой метрики на пользователях в тесте и в контроле.
Допустим, мы тестируем рост выручки от внедрения новой фичи. Факт существования эффекта доказывается от противного.
Пусть тестируемое изменение никак не влияет на пользователей. Но по результатам мы получили +10М рублей, насколько такое возможно? Чтобы это понять, посчитаем вероятность такого прироста в предположении, что эффекта нет. Эта вероятность называется p-value. Если вероятность (или p-value) мала, то наше изначальное предположение было неверным. А значит, эффект от тестируемой фичи есть.
Для отвержения нулевой гипотезы нам достаточно, чтобы p-value критерия было меньше некоторого α. Но оно плохо интерпретируемо, поэтому вместо него можно построить доверительный интервал для эффекта. Тогда гипотеза об отсутствии эффекта отвергается ⟺ 0 не лежит в доверительном интервале. Например, доверительный интервал для эффекта 10±5 эквивалентен тому, что эффект всё же есть, а если бы результат был 10±15, то эффект не обнаружен.
В наших A/B-тестах мы всегда строим доверительные интервалы: так результаты нагляднее для стейкхолдеров, нежели какие-то p-value, которые можно неправильно понять. Да и самим в таком виде проще анализировать результаты.
К примеру, наш A/B-тест привёл к росту выручки. Какой результат вы захотели бы отдать заказчику, да и сами проанализировать?
Мы получили 10М рублей, p-value=0.01, прирост выручки статистически значим.
Или:
Второй результат будет понятней и привлекательней для заказчика и для вас.
А теперь давайте посмотрим: 10М рублей — это большой прирост или нет?
Ранее выручка была 1000М рублей, а сейчас 1010М рублей. В таком случае мы не очень-то и приросли в деньгах.
В другом случае выручка была 20М рублей, а стала 30М. В таком случае это офигенный результат!
Абсолютный результат в обоих случаях один и тот же, но в реальности они имеют совершенно разный вес. Поэтому я предлагаю вместе с абсолютными числами смотреть и относительный прирост. Вместо «мы получили 10±5М рублей» говорить: по результатам теста «мы получили +20±10% (10М рублей)». В таком виде результаты становятся понятны и интерпретируемы для любого человека.
Ещё один плюс относительных метрик: результаты можно сравнивать в различных разрезах. Допустим, вы провели A/B-тест с выдачей скидок пользователям в Москве и в Петербурге. Получилось следующее:
Значит ли это, что акция в Москве успешнее, чем в Петербурге? Вообще-то не факт, посмотрим на относительный прирост денег:
Результаты-то получились полностью противоположными. Если мы смотрим на относительные метрики, то лучше понимаем структуру данных, а значит, менее вероятно совершим ошибку при анализе результатов.
Итог:
При анализе A/B-тестов считайте не только p-value, но и доверительные интервалы с численными оценками эффекта.
Считайте не только абсолютные метрики, но и относительные.
Выполнив эти два шага, вы сильно повысите наглядность и интерпретируемость результатов.
Относительная постановка в A/B-тестах
Посмотрим, как можно поменять проверяемую гипотезу в A/B-тестировании, чтобы сразу считать относительный эффект и строить для него доверительный интервал. Предлагаются такие гипотезы:
С точки зрения здравого смысла здесь всё корректно. Мы берём математическое ожидание разницы средних в двух группах и смотрим, какую часть это изменение составляет от среднего в контроле. Этот результат и будет той метрикой, которую я предлагаю считать в относительных A/B-тестах. Далее я покажу, как исправить формулы в критериях, чтобы научиться правильно строить в таком случае доверительные интервалы и считать p-value.
Теперь, когда мы разобрались с тестируемыми гипотезами, я предлагаю перейти к критерию, которым мы будем их проверять.
T-test
T-test — самый первый метод, который приходит в голову при анализе A/B-тестов. Посмотрим на основные формулы, из которых выводится критерий:
Смысл таков: среднее в тесте и в контроле, а также разница средних должны быть из нормального распределения. Это свойство следует из центральной предельной теоремы практически для любых выборок. Правда, выборки в таком случае должны быть достаточно большого размера. Про это я расскажу чуть дальше.
Чтобы получить доверительный интервал для истинного эффекта в A/B-тесте на уровне значимости 5%, нужно воспользоваться следующей формулой:
Есть очень распространенное заблуждение, что T-test работает только в том случае, если изначальные выборки X и Y из нормального распределения. На самом деле это не так, нам нужна только нормальность средних.
Небольшая оговорка
На самом деле, коэффициент 1.96 (или 0.975 квантиль нормального распределения) в доверительном интервале неверен и там должно стоять другое число, рассчитанное из квантилей распределения Стьюдента. Но на большом объёме данных истинный коэффициент практически не отличается от квантили нормального распределения, поэтому в качестве очень точной и простой аппроксимации можно использовать его.
У многих людей, не знакомых близко с T-test, в этот момент может возникнуть вопрос:, а как это проверить? Вдруг на самом деле T-test работает только для нормальных выборок, а я пытаюсь вас обмануть? Более того, я упоминал, что на самом деле этот критерий работает при условии выполнения центральной предельной теоремы (ЦПТ), а она работает не всегда, да и требует большого количества данных. Отсюда возникает главный вопрос:, а ваши данные подпадают под действие ЦПТ или нет? Можно ли на ваших данных применять T-test или нет? А любой другой критерий, который вы придумали?
Предлагаю обсудить, как можно проверить корректность любого метода на практике и провалидировать T-test. Осознав и реализовав самостоятельно идеи из следующего параграфа, вы сможете точно быть уверенными в тех критериях, которые используете в работе.
Следующий раздел очень важен. Все мы допускаем ошибки, но один неверный критерий, который вы реализовали, может катастрофически отразиться на результатах всех последующих A/B-тестов.
Алгоритм проверки статистических критериев
Идея простая:
Создаём как можно больше датасетов, поделённых на контроль и тест, без какого-либо различия между ними (обычный А/А-тест).
Прогоняем на них придуманный критерий.
Если мы хотим, чтобы ошибка первого рода была 5%, то критерий должен ошибиться на этих примерах лишь в 5% случаев. То есть 0 не попал в доверительный интервал.
Если критерий ошибся в 5% случаев, значит он корректный. Если ошибок статистически значимо больше или меньше 5%, то для нас плохие новости: критерий некорректен.
Если он ошибся меньше, чем в 5% случаев, это не так страшно. Это только означает, что критерий вероятней всего не очень точный, и в большем проценте случаев мы не задетектируем эффект. Использовать такой критерий на практике можно, но, вероятно, он будет проигрывать по мощности своим конкурентам.
Но если критерий ошибся больше, чем в 5% случаев, это ALERT, плохо, страшно, ужасно. Таким критерием нельзя пользоваться! Это значит, что вы будете ошибаться больше, чем вы рассчитываете, и в большем проценте случаев раскатите тритменты, которые на самом деле не ведут к росту целевой метрики.
Резюмируя: мы генерируем большое количество А/А-тестов и на них прогоняем наш критерий. На всякий случай скажу, что A/A-тесты — это тесты без различий в двух группах, когда мы сравниваем контроль с контролем.
Как создать подходящие датасеты? Есть два способа решения проблемы:
Создать датасеты полностью на искусственных данных.
Создать датасеты, основываясь на исторических данных компании.
Датасеты на искусственных данных. Сначала я предлагаю обсудить первый способ, а также подробнее описать план проверки критериев:
Первым делом надо выбрать распределение, которое будет описывать наши данные. К примеру, если у нас метрика конверсии, то это бернуллевское распределение, а если метрика — выручка, то лучше использовать экспоненциальное распределение в качестве самого простого приближения.
Следующим шагом надо написать код для нашего критерия. Для проверки нужно, чтобы он возвращал доверительный интервал для эффекта.
Завести счётчик bad_cnt = 0.
Далее в цикле размера N, где N — натуральное число от 1000 до бесконечности, чем оно больше, тем лучше:
Симулировать создание теста и контроля из распределения, выбранного на первом шаге.
Запустить на сгенерированных данных наш критерий со второго шага.
Далее проверить, лежит 0 в доверительном интервале или нет. Если нет, то увеличить счётчик bad_cnt на 1. Здесь мы проверяем, ошибся ли критерий на текущей симуляции, или нет.
Построить доверительный интервал для полученной конверсии bad_cnt / N. Вот статья на Википедии о том, как это сделать. Если 5% не принадлежит ему, значит, критерий некорректен, и он заужает или заширяет доверительный интервал. Здесь как раз и играет выбор значения N на четвёртом шаге. Чем оно больше, тем меньше доверительный интервал для конверсии ошибок, а значит, мы более уверены в своём критерии.
> Разбор плана на примере проверки корректности T-test
from collections import namedtuple
import scipy.stats as sps
import statsmodels.stats.api as sms
from tqdm.notebook import tqdm as tqdm_notebook # tqdm – библиотека для визуализации прогресса в цикле
from collections import defaultdict
from statsmodels.stats.proportion import proportion_confint
import numpy as np
import itertools
import seaborn as sns
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(font_scale=1.5, palette='Set2')
ExperimentComparisonResults = namedtuple('ExperimentComparisonResults',
['pvalue', 'effect', 'ci_length', 'left_bound', 'right_bound'])
# 2. Создание тестируемого критерия.
def absolute_ttest(control, test):
mean_control = np.mean(control)
mean_test = np.mean(test)
var_mean_control = np.var(control) / len(control)
var_mean_test = np.var(test) / len(test)
difference_mean = mean_test - mean_control
difference_mean_var = var_mean_control + var_mean_test
difference_distribution = sps.norm(loc=difference_mean, scale=np.sqrt(difference_mean_var))
left_bound, right_bound = difference_distribution.ppf([0.025, 0.975])
ci_length = (right_bound - left_bound)
pvalue = 2 * min(difference_distribution.cdf(0), difference_distribution.sf(0))
effect = difference_mean
return ExperimentComparisonResults(pvalue, effect, ci_length, left_bound, right_bound)
A/A-тест:
# 3. Заводим счётчик.
bad_cnt = 0
# 4. Цикл проверки.
N = 100000
for i in tqdm_notebook(range(N)):
# 4.a. Тестирую A/A-тест.
control = sps.expon(scale=1000).rvs(500)
test = sps.expon(scale=1000).rvs(600)
# 4.b. Запускаю критерий.
_, _, _, left_bound, right_bound = absolute_ttest(control, test)
# 4.c. Проверяю, лежит ли истинная разница средних в доверительном интервале.
if left_bound > 0 or right_bound < 0:
bad_cnt += 1
# 5. Строю доверительный интервал для конверсии ошибок у критерия.
left_real_level, right_real_level = proportion_confint(count = bad_cnt, nobs = N, alpha=0.05, method='wilson')
# Результат.
print(f"Реальный уровень значимости: {round(bad_cnt / N, 4)};"
f" доверительный интервал: [{round(left_real_level, 4)}, {round(right_real_level, 4)}]")
Результат вывода: реальный уровень значимости: 0.0501; доверительный интервал: [0.0487, 0.0514].
Посмотреть код на Гитхабе
Возможно, у кого-то будет вопрос, за что отвечает параметр scale в функции sps.expon: это истинное значение математических ожидания этой случайной величины, а также значение стандартного отклонения случайной величины.
Пример показывает, что для использования T-test выборка не обязательно должна быть из нормального распределения. Это миф!
Датасеты на исторических данных компании. У многих компаний есть логирование событий. К примеру, данные о транзакциях пользователей за несколько лет. Это уже один готовый датасет: вы делите всех пользователей на тест и контроль и получаете один «эксперимент» для проверки вашего критерия.
Осталось понять, как из одного большого датасета сделать N маленьких датасетов. Я расскажу, как мы это делаем в Авито, но описанная механика применима практически к любой компании.
Наши пользователи размещают объявления. Каждое объявление относится только к одной категории товаров и размещено только в одном регионе. Отсюда возникает незамысловатый алгоритм:
Разобьём все размещения пользователей на четыре (или N в общем случае) категории: автомобили, спецтехника, услуги и недвижимость. Теперь нашу метрику, к примеру, выручку, от каждого юзера можно тоже разбить на эти категории.
Поделим выбранную метрику по месяцам: выручка за ноябрь, выручка за декабрь и так далее.
Ещё все метрики можно поделить по субъектам РФ или по группе субъектов: выручка из Москвы, выручка из Хабаровска и так далее.
Теперь у нас есть пользователи в каждой из этих групп. Поделим их случайно на тест и контроль и получим финальные датасеты для валидации придуманных статистических критериев.
Давайте посмотрим на картинках, как такая схема увеличивает количество датасетов:
Здесь мы смогли разбить 1 метрику (опять-таки, выручку) на 16 метрик (выручка в ноябре в автомобилях, выручка в марте в недвижимости и так далее), и получить 16 датасетов. А если добавить ещё и разделение по субъектам РФ, которых больше 80, то мы получим уже 16×80 = 1280 датасетов для проверки. И это всего за 5 месяцев! При этом, как показывает наша практика, 1000 датасетов достаточно, чтобы отделить некорректный критерий от хорошего.
Мы рассмотрели два метода проверки алгоритма: на искусственных и на реальных данных. Когда и где стоит использовать тот или иной способ проверки?
Главные плюсы искусственных данных в том, что их сколько угодно, они генерируются быстро, и вы полностью контролируете распределение. Можно создать бесконечно много датасетов, и очень точно оценить ошибку первого рода вашего критерия. Плюс, мой опыт говорит, что на начальных этапах дебага нового критерия искусственные данные сильно лучше реальных. Главный минус — вы получили корректность вашего критерия только на искусственных данных! На реальных же данных критерий может работать некорректно.
У датасетов, полученных на настоящих данных, всё наоборот: собрать большое количество датасетов сложно, да и не всегда нормально построен процесс сбора логов. Но адекватная оценка корректности критерия для проверки гипотез в вашей компании возможна только таким способом. Всегда можно реализовать такой критерий, который будет правильно работать на искусственных данных. Но, столкнувшись в реальности с более шумными данными, он может начать ошибаться чаще, чем в 5% случаев. Поэтому важно убедиться, что именно на настоящих данных метод будет работать верно.
По такой же процедуре, что описана выше, можно подобрать минимальный размер выборок для A/B-теста в вашей компании. Например, вы хотите протестировать, можно ли на 100 юзерах запустить ваш A/B-тест. Вы создаёте 1000 датасетов с размером выборок, равным 100, и на них проверяете критерий.
Итого, мой совет по проверке критерия такой: сначала валидируйте критерий на искусственных датасетах, чтобы точнее оценить, не ошибается ли он на простых распределениях. И лишь после этого переходите к датасетам на реальных данных.
Как я писал выше, таким образом мы прогоняем наши критерии только на А/А-тестах. Но на самом деле можно эмулировать и A/B-тесты. Казалось бы, зачем, но про это я расскажу позже.
Как искусственно реализовать A/B-тест
Допустим, наш тритмент так повлиял на выборку, что среднее увеличилось в 2 раза, то есть прирост составил +100%. Чтобы это просимулировать, выполним следующие шаги:
Умножим выборку в тесте на 2.
Поменяем понятие того, что критерий ошибся. Раньше мы проверяли, лежит 0 в доверительном интервале, или нет. Но сейчас это не то, что нам нужно. Мы строим доверительный интервал для истинного повышения, поэтому именно этот прирост и должен принадлежать интервалу в 95% случаев.
Дополнительные замечания про искусственную реализацию A/B-тестов:
В общем случае можно умножать не только на 2, но и на любое Z значение. Тогда в доверительном интервале должно лежать значение Z — 1 в 95% случаев. Например, если вы домножаете выборку на 1.5, то прирост составляет +0.5 или +50%.
Можно генерировать A/B-тест не только умножением на константу. Есть много разных вариантов, например, не умножать, а добавлять константу. Или реализовать более сложные механики, имитирующие влияние тритмента на пользователей.
Далее я буду проверять примеры на искусственных данных. Но в конце второй статьи покажу корректность всех методов и на наших настоящих данных. А ещё сравню все описанные сейчас и в дальнейшем критерии между собой.
А теперь поговорим про относительный T-test критерий.
Относительный T-test критерий
Тут, всё просто: давайте возьмём доверительный интервал в абсолютном случае и поделим его на среднее в контроле.
По смыслу всё корректно. Точно так же, как мы эффект делим на среднее в контроле, чтобы получить относительный прирост, мы отнормируем значения границ доверительного интервала, поделив их на среднее в контроле. Проверим, что всё хорошо, на А/А-тесте:
A/A-проверка
# 2. Создание тестируемого критерия.
def relative_ttest(control, test):
mean_control = np.mean(control)
mean_test = np.mean(test)
var_mean_control = np.var(control) / len(control)
var_mean_test = np.var(test) / len(test)
difference_mean = mean_test - mean_control
difference_mean_var = var_mean_control + var_mean_test
difference_distribution = sps.norm(loc=difference_mean, scale=np.sqrt(difference_mean_var))
left_bound, right_bound = difference_distribution.ppf([0.025, 0.975])
left_bound = left_bound / np.mean(control) # Деление на среднее
right_bound = right_bound / np.mean(control) # Деление на среднее
ci_length = (right_bound - left_bound)
pvalue = 2 * min(difference_distribution.cdf(0), difference_distribution.sf(0))
effect = difference_mean
return ExperimentComparisonResults(pvalue, effect, ci_length, left_bound, right_bound)
# 3. Заводим счётчик.
bad_cnt = 0
# 4. Цикл проверки.
N = 100000
for i in tqdm_notebook(range(N)):
# 4.a. Тестирую A/A-тест.
control = sps.expon(scale=1000).rvs(1000)
test = sps.expon(scale=1000).rvs(1100)
# 4.b. Запускаю критерий.
_, _, _, left_bound, right_bound = relative_ttest(control, test)
# 4.c. Проверяю, лежит ли истинная разница средних в доверительном интервале.
if left_bound > 0 or right_bound < 0:
bad_cnt += 1
# 5. Строю доверительный интервал для конверсии ошибок у критерия.
left_real_level, right_real_level = proportion_confint(count = bad_cnt, nobs = N, alpha=0.05, method='wilson')
# Результат.
print(f"Реальный уровень значимости: {round(bad_cnt / N, 4)};"
f" доверительный интервал: [{round(left_real_level, 4)}, {round(right_real_level, 4)}]")
Результат вывода: реальный уровень значимости: 0.0507; доверительный интервал: [0.0494, 0.0521].
И вроде бы на этом стоит закончить, уровень значимости 5%, всё, как мы и хотели. Но давайте проверим, что происходит на искусственно сгенерированных A/B-тестах.
# 3. Заводим счётчик.
bad_cnt = 0
# 4. Цикл проверки.
N = 100000
for i in tqdm_notebook(range(N)):
# 4.a. Тестирую A/B-тест
control = sps.expon(scale=1000).rvs(1000)
test = sps.expon(scale=1000).rvs(1100)
test *= 2
# 4.b. Запускаю критерий.
_, _, _, left_bound, right_bound = relative_ttest(control, test)
# 4.c. Проверяю, лежит ли истинная разница средних в доверительном интервале.
if left_bound > 1 or right_bound < 1:
bad_cnt += 1
# 5. Строю доверительный интервал для конверсии ошибок у критерия.
left_real_level, right_real_level = proportion_confint(count = bad_cnt, nobs = N, alpha=0.05, method='wilson')
# Результат.
print(f"Реальный уровень значимости: {round(bad_cnt / N, 4)};"
f" доверительный интервал: [{round(left_real_level, 4)}, {round(right_real_level, 4)}]")
Результат вывода: реальный уровень значимости: 0.1278; доверительный интервал: [0.1258, 0.1299].
Что-то пошло сильно не так. Критерий ошибается не в 5% случаях, а в 12%. А это значит, что мы будем совершать в два раза больше ошибок, чем рассчитывали. Хочу ещё раз отметить: очень важно валидировать критерии! Чтобы убедиться в этом, можете сами запустить код отсюда.
Теоретическое обоснование полученного результата простое: мы не учли, что «С с чертой» — это оценка среднего, а не истинное математическое ожидание. Поэтому, когда мы делим на него, мы не учитываем шум, который возникает в знаменателе. А после проверки мы получили важный результат для относительной постановки T-test:
Но теперь надо придумать, как это исправить. Предлагаю перейти к такой случайной величине:
Утверждается, что её математическое ожидание — это именно то, что нам нужно в относительной постановке A/B-тестов.
Доказательство корректности
Вообще, математическое ожидание у такой статистики не равно величине, которую мы хотим оценить в гипотезе относительного A/B-тестирования. Но здесь на помощь приходит разложение в многочлен Тейлора:
Подробнее про этот переход можно почитать здесь. Единственное, надо помнить, что статистика среднего сходится к своему математическому ожиданию из усиленного закона больших чисел, поэтому разложение в многочлен Тейлора здесь работает.
Но надо понять, как посчитать дисперсию этой статистики. Для этого предлагается применить дельта-метод. В итоге, формула дисперсии будет такой:
А в случае выборок разного размера такой:
Выглядит сложно и страшно, поэтому вот код критерия и его проверка, которые вы можете использовать.
Реализация и проверка критерия
# 2. Создание тестируемого критерия.
def relative_ttest(control, test):
mean_control = np.mean(control)
var_mean_control = np.var(control) / len(control)
difference_mean = np.mean(test) - mean_control
difference_mean_var = np.var(test) / len(test) + var_mean_control
covariance = -var_mean_control
relative_mu = difference_mean / mean_control
relative_var = difference_mean_var / (mean_control ** 2) \
+ var_mean_control * ((difference_mean ** 2) / (mean_control ** 4))\
- 2 * (difference_mean / (mean_control ** 3)) * covariance
relative_distribution = sps.norm(loc=relative_mu, scale=np.sqrt(relative_var))
left_bound, right_bound = relative_distribution.ppf([0.025, 0.975])
ci_length = (right_bound - left_bound)
pvalue = 2 * min(relative_distribution.cdf(0), relative_distribution.sf(0))
effect = relative_mu
return ExperimentComparisonResults(pvalue, effect, ci_length, left_bound, right_bound)
А/B-проверка:
# 3. Заводим счётчик.
bad_cnt = 0
# 4. Цикл проверки.
N = 100000
for i in tqdm_notebook(range(N)):
# 4.a. Тестирую A/B-тест.
control = sps.expon(scale=1000).rvs(2000)
test = sps.expon(scale=1000).rvs(2100)
test *= 2
# 4.b. Запускаю критерий.
_, _, _, left_bound, right_bound = relative_ttest(control, test)
# 4.c. Проверяю, лежит ли истинная разница средних в доверительном интервале.
if left_bound > 1 or right_bound < 1:
bad_cnt += 1
# 5. Строю доверительный интервал для конверсии ошибок у критерия.
left_real_level, right_real_level = proportion_confint(count = bad_cnt, nobs = N, alpha=0.05, method='wilson')
# Результат.
print(f"Реальный уровень значимости: {round(bad_cnt / N, 4)};"
f" доверительный интервал: [{round(left_real_level, 4)}, {round(right_real_level, 4)}]")
Реальный уровень значимости: 0.0501; доверительный интервал: [0.0487, 0.0514].
Для A/A-теста: реальный уровень значимости: 0.05; доверительный интервал: [0.049, 0.052].
Посмотреть код на Гитхабе
Итак, относительный T-test критерий работает на искусственных данных. Но, возможно, у вас возник вопрос:, а не ухудшим ли мы таким образом мощность критериев? Вдруг дополнительный шум в знаменателе так расширит доверительный интервал, что критерий станет бесполезным? И если раньше, с обычным критерием, мы детектировали эффект в 80% случаев, а сейчас только в 50%, то, очевидно, мы не будем пользоваться относительным критерием: мощность всегда превыше всего.
Ответ: нет, этого не произойдёт. Вот практический пример:
absolute_power_cnt = 0
relative_power_cnt = 0
# 4. Цикл проверки.
N = 10000
for i in tqdm_notebook(range(N)):
X = sps.expon(scale=1000).rvs(10000)
Y = sps.expon(scale=1000).rvs(10000) * 1.01
_, _, _, rel_left_bound, rel_right_bound = relative_ttest(X, Y)
_, _, _, abs_left_bound, abs_right_bound = absolute_ttest(X, Y)
if rel_left_bound > 0:
relative_power_cnt += 1
if abs_left_bound > 0:
absolute_power_cnt += 1
print(f"Мощность относительного критрерия VS мощность абсолютного критерия: {relative_power_cnt / N} VS. {absolute_power_cnt /N}")
Мощность относительного критрерия VS мощность абсолютного критерия: 0.0933 VS. 0.0983
Как видно, результаты по мощности относительного и абсолютного критериев практически идентичны. Если хотите, можете прочесть теоретическое обоснование.
Теоретическое обоснование
Давайте построим доверительный интервал для статистики X, объявленной чуть выше, не через дельта-метод, а через бутстрап. Причём, так как эта статистика из нормального распределения (также из дельта-метода), то для него можно построить перцентильный доверительный интервал.
Что нас тогда будет интересовать? В каком проценте случаев насемплированный X < 0. Если процент будет меньше α, то эффект задетектирован.
То есть, если бы мы строили перцентильный доверительный интервал для абсолютной метрики, то процент случаев, когда T−C будет меньше 0, такой же, как и X < 0. А значит, отвержение гипотезы в относительном и абсолютном случаях будет происходить одновременно. И не может быть такого, что абсолютный критерий задетектировал эффект, а относительный — нет. По крайней мере, на большом объёме данных.
Итог: я показал, как правильно построить относительный T-test критерий. Теперь у вас есть бейзлайн-критерий для относительных A/B-тестов. На этом с T-test покончено. Давайте обсудим серые метрики или не статистически значимые результаты в A/B-тестах.
Серые метрики в A/B-тестах
Допустим, вы добавили новую фичу на сайте и решили проверить, приросла ли выручка.
Как в основном смотрят на результаты теста:
P-value = 0.4, результат не статистически значим.
Прирост: +10000 рублей на всю тестовую группу.
+1% выручки.
В этот момент аналитики часто думают: «Чёрт, результаты серые, ничего сказать нельзя. Может на самом деле и есть эффект, но мы его не видим. Выручка же положительна, давайте катить».
Но это на самом деле и из таких результатов можно вытащить инсайты. Для этого добавим в результаты доверительные интервалы:
P-value = 0.4, результат не статистически значим.
Прирост: +10000±40000 рублей на всю тестовую группу.
+1±4% выручки.
А теперь спросим себя: может ли в таком случае прирост на самом деле составлять не 10 000 рублей, как мы получили, а 100 000 рублей? Если бы у нас действительно был эффект в 100 000 рублей, то вероятность в таком случае получить прирост +10 000 рублей, равнялась бы 0! Поэтому мы можем говорить, что гипотеза о таком большом приросте несостоятельна. А то, что вероятность равна 0 следует из ширины доверительного интервала.
Более подробное объяснение
Для начала перейдём к метрике средней выручки на человека. Пусть у нас всего 10 000 человек, тогда средний прирост на пользователя +1±4 рубля, а истинный средний прирост выручки на человека +10 рублей, если истинная выручка +100 000. Как я писал выше, если выборка большого размера, то на ней работает ЦПТ, и среднее будет из нормального распределения.
Мы знаем, что оценка половины ширины доверительного интервала у нас примерно 4 рубля, а ещё знаем, что наши данные — из нормального распределения, у которого есть два параметра:
μ — математическое ожидание случайной величины из этого распределения;
σ — среднеквадратическое отклонение.
Тогда, если бы истинный эффект был 10 рублей, то мы знаем μ этого распределения. Осталось понять σ. Если вспомнить формулу доверительного интервала в T-test, то становится понятно, что σ=4/1.96=2.04. А значит, мы знаем все параметры этого распределения и можем его визуализировать:
Жёлто-зелёным обозначено предполагаемое распределение выручки. Какова вероятность для такого распределения получить значение меньшее, или равное единице (левее красной точки)? Она равна 0. Поэтому, гипотеза о приросте в 10 рублей несостоятельна. Здесь, кстати, видна роль ширины доверительного интервала: чем она меньше, тем меньше σ и тем менее «широким» будет распределение. А значит, менее состоятельно, что истинный прирост равняется 10 рублям.
Ещё раз повторим основные моменты того, как мы оценили гипотезу о несостоятельности большого прироста:
Нормальность этого распределения следует из ЦПТ.
Параметр μ — это то, что мы хотели бы увидеть в качестве прироста.
Параметр σ высчитывается при построении доверительного интервала.
Красная точка — полученный на тесте эффект.
Далее мы смотрим, в каком проценте случаев в построенном распределении мы получим наблюдаемый эффект (в те