Как стать автором
Обновить

Оффлайн А/Б тесты в ресторанах фастфуда. Часть 2: Анализ и интерпретация результатов A/B-тестов

Уровень сложностиСредний
Время на прочтение13 мин
Количество просмотров773

Методология — это не шаблон, это компас. А его стрелка — всегда на стороне ваших данных.

В первой части «Планирование и верификация оффлайн A/B-тестов» мы разобрали, как подготовить данные и убедиться, что группы для эксперимента сопоставимы. Мы провели тщательную верификацию: сравнили метрики, проверили распределения и постарались исключить искажения ещё до старта.

Теперь — самое важное.
Во второй части речь пойдёт о том, как анализировать полученные данные и не ошибиться с выводами. Мы обсудим методы, позволяющие скорректировать влияние внешних факторов, научимся контролировать ошибки первого и второго рода, выбирать подходящий статистический критерий и оценивать надёжность результатов.

Если первая часть была про чистоту эксперимента, то вторая — про силу аргументов.

Дополнительный анализ

В случае выявления расхождений по итогам верификации необходимо провести дополнительный анализ, чтобы определить их природу и возможные причины. Различия в корреляции целевой метрики между контрольной (КГ) и тестовой группами могут быть связаны с системными перекосами при мэтчинге, наличием выбросов или недостаточным учётом ключевых факторов, влияющих на метрику. Для выявления таких проблем полезно провести детальную аналитику на уровне отдельных пар ресторанов, что позволит оценить стабильность распределений и выявить возможные аномалии.

Анализ можно выполнить с помощью расчёта корреляции, среднего значения и медианы внутри каждой пары ресторанов, что даст более точное представление о локальных отклонениях. Визуализация данных также играет важную роль: построение графиков корреляции, box plot (графиков с усами) и распределений позволит наглядно определить, какие пары демонстрируют значимые расхождения.

В дополнение к этому полезно выполнить проверку однородности дисперсий между КГ и ТГ — например, с помощью критерия Левена. Это позволяет выявить рестораны, в которых наблюдаются существенные различия в разбросе целевой метрики. Такие расхождения могут свидетельствовать о повышенной чувствительности к нестабильному пользовательскому поведению или о наличии неучтённых сезонных факторов. Ниже представлено сравнение форм распределений в двух группах:

как видно, распределение в тесте (красная линия) более «плоское» и растянутое, что указывает на большую дисперсию:

Для количественной оценки различий можно рассчитать:

  • дисперсии в каждой группе;

  • соотношение дисперсий (test_var / control_var);

  • p-value критерия Левена для оценки статистической значимости различий.

Такой подход помогает выявить рестораны с потенциально нестабильным поведением. При необходимости их можно исключить из выборки или адаптировать метод мэтчинга, чтобы минимизировать влияние этих объектов на результат.

from scipy.stats import levene
# Создаем хранилище результатов
results = []
# Получаем уникальные рестораны
rest_ids = df['rest_id'].unique()
for rest_id in rest_ids:
    # Фильтруем по ресторану
    rest_data = df[df['rest_id'] == rest_id]
    
    control_gpc = rest_data[rest_data['group'] == 'control']['target']
    test_gpc = rest_data[rest_data['group'] == 'test']['target']
    
    # Проверка на достаточность данных
    if len(control_target) >= 2 and len(test_target) >= 2:
        # Дисперсии
        control_var = control_target.var()
        test_var = test_target.var()

        # Levene Test
        stat, p_value = levene(control_target, test_ target)

        # Соотношение дисперсий
        ratio = test_var / control_var if control_var != 0 else float('inf')

        results.append({
            'rest_id': rest_id,
            'test_var': test_var,
            'control_var': control_var,
            'variance_ratio': ratio,
            'p_value': p_value
        })

# Финальный DataFrame
results_df = pd.DataFrame(results)

# Заполняем NaN нулями (если где-то была проблема)
results_df.fillna(0, inplace=True)

# Сортировка по ratio (где в тесте разнос)
results_df = results_df.sort_values(by='variance_ratio', ascending=False)

# Выводим ТОП 20
print("ТОП РЕСТОРАНОВ ПО РАЗНОСТИ ДИСПЕРСИЙ:")
print(results_df.head(20))

Анализ устойчивости среднего на пре-периоде как инструмент выявления нестабильных объектов

При верификации контрольной (КГ) и тестовой (ТГ) групп в A/B-тестировании стандартные методы проверки сбалансированности могут не выявить всех скрытых источников искажений. Даже если ключевые метрики согласованы на глобальном уровне, возможны локальные нестабильности, связанные с флуктуациями средних значений в отдельных объектах. Для выявления таких аномалий используется метод анализа устойчивости среднего на пре-периоде, основанный на бутстрапировании.

Подход заключается в разбиении пре-периода на две равные части и сравнении средних значений целевой метрики в каждой из них. С помощью бутстрапа строятся доверительные интервалы для среднего, и проверяется, пересекаются ли они. Если интервалы не пересекаются, это говорит о высокой изменчивости метрики и потенциальной нестабильности объекта, способной внести шум в результаты теста. Метод, будучи статистически обоснованным, превосходит стандартные проверки на стационарность или медиану, так как учитывает как смещения, так и дисперсию, включая влияние выбросов.

На этапе верификации лучше оценивать устойчивость среднего в "сырых" условиях, без сглаживания сезонных эффектов (без дополнительной стратификации). Это позволяет на раннем этапе выявить объекты с нестабильным поведением, которое иначе могло бы быть замаскировано агрегированием.

Использование бутстрапа для оценки устойчивости среднего — это мощный способ выявить скрытые аномалии и заранее исключить нестабильные юниты, тем самым повышая надёжность эксперимента. Несмотря на простоту реализации, метод редко рассматривается в классической экспериментальной статистике, что делает его особенно ценным дополнением к современным подходам верификации A/B-тестов.

# 1. Фильтрация данных для пре-периода
pre_period_df = df[df['period'] == 'pre']

# 2. Бутстреп функция для получения среднего значения и доверительного интервала
def bootstrap_mean_ci(data, num_samples=1000, ci=0.95):
    boot_means = [np.mean(np.random.choice(data, size=len(data), replace=True)) for _ in range(num_samples)]
    lower_bound = np.percentile(boot_means, (1 - ci) / 2 * 100)
    upper_bound = np.percentile(boot_means, (1 + ci) / 2 * 100)
    return np.mean(boot_means), (lower_bound, upper_bound)

# 3. Проверка устойчивости среднего на пре-периоде
unstable_restaurants = []
for rest_id, group_data in pre_period_df.groupby('rest_id'):
    # Разделим пре-период на два равных отрезка и посчитаем для них доверительные интервалы среднего
    midpoint = len(group_data) // 2
    gp_early = group_data.iloc[:midpoint]['target']
    gp_late = group_data.iloc[midpoint:]['target']
    
    # Получаем доверительные интервалы для среднего на каждом этапе
    mean_early, ci_early = bootstrap_mean_ci(gp_early)
    mean_late, ci_late = bootstrap_mean_ci(gp_late)
    
    # Проверка на перекрытие доверительных интервалов
    if ci_early[1] < ci_late[0] or ci_late[1] < ci_early[0]:  # Интервалы не перекрываются
        unstable_restaurants.append(rest_id)

# Результат
print("Рестораны с неустойчивым средним target на пре-периоде:", unstable_restaurants)

Выбор метода

Итак, мы провели всестороннюю верификацию данных и можем с уверенностью сказать, что подобранные тестовая (ТГ) и контрольная (КГ) группы обладают максимальным сходством, а целевая метрика (ЦМ) распределена одинаково. Это означает, что влияние скрытых факторов сведено к минимуму, и ничто не исказит результаты эксперимента. В этом случае следующим логичным шагом является проведение А/А и A/B тестов на исторических данных, что позволит контролировать ошибки I и II рода и подтвердить корректность экспериментального дизайна.

Однако возможна иная ситуация: несмотря на тщательный подбор групп, в пре-периоде сохраняется систематическое смещение выборок, из-за чего данные не проходят верификацию. Тем не менее, если между КГ и ТГ наблюдается высокая корреляция по ключевым метрикам и ЦМ, это открывает возможность использования альтернативного метода пост-анализа — разностей в разностях (Difference-in-Differences, Diff-in-Diff). Этот подход позволяет нивелировать исходные различия между группами за счёт учета динамики изменений, обеспечивая более надёжную оценку эффекта тестируемого воздействия. Об этом подходе есть хорошая статья «Diff-in-diff: жизнь за пределами идеального эксперимента» .

Метод разницы средних. A\A и A\B тесты КОНТРОЛЬ ОШИБОК I и II РОДА.

Выбор метода анализа разницы средних

Для корректной оценки разницы средних необходимо выбрать подходящий статистический метод. Классический t-тест предполагает нормальность распределения, но в условиях оффлайн-бизнеса (выбросы, высокая дисперсия, влияние сезонности и офлайн-точек) это предположение часто нарушается. В таких случаях предпочтительнее использовать непараметрические подходы или бутстрап.

При этом важно учитывать, что данные могут иметь выраженную сезонную структуру — например, различие между буднями и выходными. В таких случаях рекомендуется применять стратифицированное бутстрапирование, которое позволяет сохранять внутренние закономерности данных. Возможные страты — по типу дня, наличию акций, когортам точек или другим релевантным признакам. Конкретные параметры стратификации зависят от контекста исходных данных, осознанный подход к их выбору — ключевой элемент корректного анализа.

t-тест

Если данные удовлетворяют требованиям, можно использовать двухвыборочный t-тест с равной дисперсией:

t = \frac{\overline{X_1} - \overline{X_2}}{s_p \cdot \sqrt{\frac{1}{n_1} + \frac{1}{n_2}}}, \quad s_p^2 = \frac{(n_1 - 1)s_1^2 + (n_2 - 1)s_2^2}{n_1 + n_2 - 2}

Но при нарушении предпосылок (например, гомоскедастичности или нормальности) этот тест даёт некорректные результаты.

Бутстрап

Бутстрап — универсальный метод, особенно актуальный при анализе отношений (например, среднего чека):

  • Повторно (с возвращением) выбираем наблюдения из оригинальной выборки.

  • Строим эмпирическое распределение статистики (например, разности средних).

  • На его основе оцениваем доверительный интервал и p-value.

CI = \left[ \Delta_{\text{mean}}^{\text{low}}, \Delta_{\text{mean}}^{\text{high}} \right], \quad p = \frac{\text{кол-во bootstrap итераций, где } \Delta \geq \Delta_{\text{наблюдаемое}}}{\text{общее число итераций}}

При этом обычный бутстрап не учитывает сезонность: выручка в выходные и будни может различаться. Это приводит к завышенной дисперсии. Рекомендуется использовать стратифицированный бутстрап, выделяя страты по дням недели, типу дня (будни/выходные), как упоминалось ране, или другим признакам.

Что делать с временной зависимостью?

Поскольку наши данные — это временные ряды, возникает проблема автокорреляции. Если её не учитывать, бутстрап может переоценивать статистическую значимость. Возможные варианты:

·       Блочный бутстрап — объединение последовательных дней в блоки длиной k:

\text{Block}_i = \{ x_i, x_{i+1}, \dots, x_{i+k-1} \}

Длина блока для блочного бутстрапа (например, 7 дней) зависит от периодичности данных, которую можно оценить через автокорреляционную функцию (ACF).

·       Парный бутстрап — если можно сформировать пары (например, ТГ/КГ по дням).

·       Кластерный бутстрап — если есть естественные группы (регионы, типы точек).

·       Расширенная стратификация — по неделям, акциям, погоде, когорте клиентов и т.д.

Если целью анализа является не только выявление разницы в средних, но и оценка изменений в структуре распределения (медиана, асимметрия, устойчивость к выбросам), можно использовать ранговый тест Вилкоксона (Wilcoxon signed-rank test). Это непараметрическая альтернатива парному t-тесту, не требующая нормальности распределения.

Пусть имеются парные наблюдения (xi,yi), тогда тест строится следующим образом:

  • Вычисляем разности d_i = x_i - y_i.

  • Исключаем нулевые разности

  • Присваиваем ранг ∣d_i∣: R_i​

  • Учитываем знак разности: S_i=sign(d_i)

  • Рассчитываем статистику:

W = \sum_i S_i \cdot R_i

Статистика W сравнивается с критическими значениями (или переводится в p-value).

Этот тест особенно полезен:

  • при наличии выбросов или асимметрии в распределениях;

  • когда метрика нестабильна или непрерывна, но не нормально распределена;

  • в условиях небольших выборок, где параметрические методы неустойчивы.

Однако стоит помнить, что мощность теста снижается при очень малом размере выборки, а также при большом количестве нулевых разностей.

Альтернативные подходы

Если бутстрап и его модификации не подходят, есть два мощных альтернативных пути:

Пермутационный тест — строим распределение разности средних при случайных перестановках групповых меток. Подходит для малых выборок, не требует предположений о распределении:

p = \frac{\text{кол-во перестановок, где } \Delta_{\text{перм}} \geq \Delta_{\text{наблюдаемое}}}{\text{общее число перестановок}}

Байесовские методы:

Приорная модель: учитываем исторические знания.

Иерархическая модель: полезна при вложенной структуре (дни внутри ресторанов, рестораны внутри городов):

y_{ij} \sim N(\theta_j, \sigma^2), \quad \theta_j \sim N(\mu, \tau^2)

Такая модель «подтягивает» нестабильные оценки к общему уровню и снижает дисперсию.

Скрытый текст

Важно: Сознательно не публикую конкретный скрипт, так как выбор метода не должен быть шаблонным. Он всегда зависит от структуры и специфики данных.

Ошибка I рода (ложноположительный результат)

Перед запуском A/B-теста важно убедиться, что выбранный метод статистически устойчив — то есть, не выдаёт ложных "успехов" там, где эффекта нет. Это проверяется с помощью A/A-теста, в котором сравниваются две "пустые" группы до вмешательства. Одна из ключевых метрик — частота ошибок I рода, или ложноположительных срабатываний. В рамках симуляции мы проводим несколько тысяч итераций (например, 1000–10000), чтобы получить устойчивую оценку распределения p-value и частоты ложноположительных срабатываний.

A/A-тест можно реализовать через бутстрапирование на пре-периоде. Если статистический критерий корректен, то частота ложных срабатываний не должна превышать уровень значимости alpha. Например, при α=0.05, мы ожидаем, что p-value ≤ 0.05 только в 5% случаев.

Важно: A/A-тест нельзя делать после предварительного разбиения на группы, иначе результат будет смещён. Суть A/A-теста — наблюдать естественное поведение выборки без вмешательства.

Теоретическое обоснование.

Если нулевая гипотеза верна (H0​), то p-value имеет равномерное распределение на [0,1]. Это фундаментальное свойство корректных статистических тестов:

p\text{-value} \mid H_0 \sim U[0,1], \quad F_{p\text{-value} \mid H_0}(x) = \mathbb{P}(p\text{-value} < x \mid H_0) = x

Это означает, что при многократных запусках эксперимента без эффекта, мы ожидаем равномерное распределение p-value. Это и есть основа контроля частоты ложных срабатываний.

def plot_pvalue_ecdf(pvalues, title=None):
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

    if title:
        plt.suptitle(title)

    sns.histplot(pvalues, ax=ax1, bins=20, stat='density')
    ax1.plot([0,1],[1,1], 'k--')
    ax1.set(xlabel='p-value', ylabel='Density')

    sns.ecdfplot(pvalues, ax=ax2)
    ax2.plot([0,1],[0,1], 'k--')
    ax2.set(xlabel='p-value', ylabel='Probability')
    ax2.grid()

mean = 0
std = 1
sample_size = 100

p_values = []
for _ in range(1000):
    a = np.random.normal(mean, std, sample_size)
    b = np.random.normal(mean, std, sample_size)
    _, p_val = ttest_ind(a, b)
#     p_val = np.random.uniform(0, 1)
    p_values.append(p_val)

plot_pvalue_ecdf(p_values, 'p-value ECDF. Null hypothesis')

После симуляции распределения p-value удобно визуализировать его с помощью эмпирической функции распределения (ECDF). Она показывает, какая доля значений p-value меньше или равна заданному порогу.

Интерпретация:

Чёрная пунктирная диагональ на ECDF-плоте соответствует идеальному равномерному распределению U[0,1], которое мы ожидаем при корректном тесте.

Если эмпирическая кривая лежит выше диагонали, это сигнализирует о завышенной частоте ложноположительных результатов — тест слишком «чувствительный» и часто находит "эффект", даже когда его нет (ошибка I рода).

Если кривая ниже диагонали — тест консервативен. Это может быть полезно в некоторых контекстах (например, при необходимости снижать риск фальшивых успехов), но может приводить к заниженной мощности (больше ошибок II рода).

Хорошее совпадение с диагональю означает, что тест не смещён, и p-value действительно имеет равномерное распределение — признак корректной настройки статистического метода.

Таким образом, построение и анализ ECDF по результатам A/A-тестов — это мощный способ валидировать применяемый статистический метод до начала A/B-экспериментов.

Ошибка II рода (ложноотрицательный результат)

Что означает, что мощность теста при заданном MDE равна 0.8? Это означает, что если на самом деле эффект, равный MDE, присутствует, то мы должны в среднем обнаруживать его в 80 случаях из 100. То есть если верна альтернативная гипотеза, то функция распределения 𝑝−value должна быть выпуклой и равняться мощности теста на заданном уровне значимости, т.е. она будет смещена влево, с накоплением значений ближе к нулю. Это отражает более частое получение малых p-value, когда альтернативная гипотеза действительно верна.

F_{p\text{-value} \mid H_1}(\alpha) = \mathbb{P}(p\text{-value} < \alpha \mid H_1) = 1 - \beta

Для контроля ошибки II рода в данных искусственно создаётся небольшое различие, и проверяется, способен ли тест его обнаружить. Ошибка II рода — это вероятность не выявить реальный эффект:

\beta = P(\text{не отвергнуть } H_0 \mid H_A \text{ верно})

Где H_A — альтернативная гипотеза о наличии эффекта. Если тест слишком часто пропускает реальные различия, необходимо увеличить выборку или изменить метод.

Цель AB теста — оценить, как изменения, примененные только в ТГ, повлияли на метрику (например, ср чек), сравнив это с КГ, где изменений нет.

Мощность теста зависит от трёх ключевых факторов: размера выборки, размера эффекта (MDE) и уровня значимости α. Увеличение размера выборки — наиболее прямолинейный способ повысить мощность.

Проведение power-анализа до запуска A/B-теста позволяет убедиться, что тест имеет разумную вероятность зафиксировать эффект, если он действительно существует. При недостаточной мощности теста результаты эксперимента могут ввести в заблуждение из-за высокой частоты ложноотрицательных выводов.

Финальный выбор метода: гибкость и контекст

В реальности данные редко соответствуют идеальным условиям. Несмотря на теоретические предпосылки, практика показывает: метод анализа нужно подбирать, исходя из структуры конкретных данных, а не из универсальных рекомендаций. Это особенно актуально в оффлайн-среде, где наблюдаются выбросы, сезонные колебания, различия по точкам, автокорреляция — всё это может искажать результат, если выбран не тот подход.

Если классический бутстрап не работает — например, из-за автокорреляции или неустранимой сезонности — важно рассмотреть альтернативные подходы:

  • Блочный бутстрап — помогает при автокорреляции, но блоки должны быть достаточно информативными. Иногда эффект размывается шумами внутри блоков — тогда имеет смысл проанализировать структуру шума и, возможно, переопределить границы блоков (например, строить их по неделям, а не дням).

  • Парный бутстрап — хорош, когда есть возможность устанавливать пары наблюдений (например, одни и те же рестораны в те же дни недели), особенно при высокой корреляции между КГ и ТГ.

  • Кластерный бутстрап — может быть полезен, если структура данных подразумевает естественные кластеры (например, по географии или типу ресторана). Здесь важно, чтобы внутри кластера метрики были однородны.

  • Стратифицированный подход — не обязательно по времени. Можно стратифицировать по объему продаж, типу заведения, дням акции, или даже по погоде, если она влияет на трафик. Главное — каждая страта должна быть достаточно «однородной» и при этом содержать достаточное количество наблюдений.

Если бутстрап и его разновидности не дают устойчивого результата, можно использовать, как уже упоминалось ране, пермутационные тесты, особенно если данных немного и требуется минимум предположений о распределении. Такой тест менее чувствителен к форме распределения и хорошо работает при сохранении структуры зависимости между наблюдениями. Также можно попробовать байесовские подходы. Но даже байесовские методы требуют тщательной проверки: модель может «забить» на эффект, если априоры выбраны неудачно или шум превышает ожидаемую вариативность. Независимо от метода, валидация эксперимента через симуляции и A/A-тесты — это не опция, а обязательный шаг для проверки устойчивости выводов.

Дополнительный анализ перед пост анализом А\Б теста

Анализ выбросов

Перед проведением пост-анализа A/B-теста важно дополнительно проанализировать данные на наличие выбросов. Это особенно критично в условиях нестабильных, "грязных" данных, как в нашем случае. Выбросы могут существенно искажать результаты, особенно при использовании средних значений. Поэтому рекомендуется:

Проверить значения метрик на экстремальные значения (например, через IQR, z-оценку или boxplot-анализ).

При обнаружении выбросов:

·       либо исключать их (с обоснованием и пояснением),

·       либо перейти к более робастным метрикам, таким как медиана или trimmed mean (усечённое среднее).

Важно: удаление выбросов допустимо только при чётком понимании природы выброса, и при этом оно не должно нарушать честность выборки.


Заключение

Выбор метода — это не просто вопрос статистики, это вопрос понимания данных. Это постоянный баланс между теоретической строгостью и практической применимостью. Да, можно начать с t-теста, но чаще всего он просто не выдерживает сложностей оффлайн-реальности. Бутстрап может стать спасением — но только если правильно подобраны страты и устранены зависимости. Если и он не работает — ищем дальше: пермутации, парные/кластерные схемы, байесовский подход.

Главное, что хочется подчеркнуть: в сложных данных нет одного правильного метода. Есть проверка гипотез, есть контроль ошибок, есть достоверность и устойчивость результатов — а к ним можно прийти разными путями. Чем лучше вы понимаете, как устроены ваши данные, тем точнее будет выбранный путь.

Даже с ограниченными данными можно проводить качественные A/B-тесты — если вдумчиво подходить к выбору метода, честно смотреть на данные и не бояться пробовать то, что не написано в учебнике.

Идеальных данных не бывает. Но бывают умные вопросы, хорошая верификация и здоровая паранойя в отношении метода — и именно они делают эксперимент осмысленным.

Пусть эта статья станет для кого-то отправной точкой, чтобы не просто "делать тесты", а выстраивать эксперименты, которым можно доверять.

Если у вас есть похожий опыт, свои подходы, идеи — делитесь! Давайте делать оффлайн-эксперименты лучше вместе ✌️


📚 Книги и статьи:

Теги:
Хабы:
+3
Комментарии1

Публикации

Работа

Data Scientist
46 вакансий

Ближайшие события