Как Sample Ratio Mismatch спасает A/B тесты от ложных выводов

Если вы когда-либо работали с A/B тестированием, то знаете, что этот процесс не всегда идет гладко. Казалось бы, что тут может быть сложного? Разделили аудиторию, дали половине синюю кнопку, половине — зеленую и ждем результатов. Но в реальной жизни стратегия «разделяй и властвуй» не всегда работает идеально. Вмешиваются случайные факторы, баги, и вот тут Sample Ratio Mismatch (SRM) становится помощником.

SRM как защитник точности A/B тестирования

Sample Ratio Mismatch, или несоответствие пропорции выборок, — это ситуация, когда фактическое распределение пользователей между контрольной и тестовой группами отклоняется от запланированного. Например, если мы ожидаем разделение аудитории поровну — 50/50, но в итоге получаем 48/52 или даже 60/40, это явный признак SRM. Такое отклонение может указывать на технические проблемы или ошибки в рандомизации, которые искажают результаты эксперимента, делая их ненадежными. SRM помогает выявить подобные аномалии, вовремя сигнализируя, что результаты теста требуют дополнительной проверки и, возможно, корректировки.

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

Проверка SRM с помощью статистики

Для проверки SRM часто используют χ²-тест (хи-квадрат тест), который проверяет, насколько наблюдаемое распределение пользователей соответствует ожидаемому. Это простой, но надежный способ обнаружить отклонения. Вот пример кода:

import scipy.stats as stats

# Пусть у нас есть данные по количеству пользователей в группах:
control_group_size = 15752
variant_group_size = 15257

# Ожидаемое распределение (50/50)
expected_ratios = [0.5, 0.5]
observed_counts = [control_group_size, variant_group_size]

# Расчёт χ²-статистики
chi2, p_value = stats.chisquare(f_obs=observed_counts, f_exp=[sum(observed_counts) * r for r in expected_ratios])

print(f"Chi2: {chi2}, p-value: {p_value}")

Если p-value меньше 0.05 (в классическом варианте), у вас проблемы. Это значит, что вероятность случайности такого рассогласования мала, и где-то в тесте сидит баг. Может, это ошибка рандомизации? Или баг в доставке вариаций? Причин может быть много, но одно ясно точно: тестировать нужно заново

SRM для A/B/n тестов и динамических распределений

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

import pandas as pd

# Пример данных трафика по часам
data = {
    'hour': range(24),
    'control_visitors': [int(1000 + (i % 5) * 50) for i in range(24)],
    'variant_visitors': [int(1000 - (i % 5) * 50) for i in range(24)]
}
df = pd.DataFrame(data)

# Расчёт суммарного количества посетителей
df['total_visitors'] = df['control_visitors'] + df['variant_visitors']

# Проверка SRM по каждому часу
for i, row in df.iterrows():
    chi2, p_value = stats.chisquare(f_obs=[row['control_visitors'], row['variant_visitors']], 
                                    f_exp=[row['total_visitors'] * 0.5, row['total_visitors'] * 0.5])
    if p_value < 0.05:
        print(f"Час {row['hour']}: обнаружено SRM! p-value={p_value:.3f}")

P.S: ошибочно полагать, что плохой SRM всегда сигнализирует о критических проблемах. Например, если вы тестируете трафик в будни и выходные, естественные флуктуации могут быть причиной временного отклонения от идеального распределения. Но если проблемы появляются регулярно, стоит обратить внимание: лучше быть настороженным, чем потом объяснять руководству, почему ваши результаты не совпадают с реальностью.

Как автоматизировать SRM и не упустить проблему

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

Пример:

import smtplib
from email.mime.text import MIMEText
import os
from time import sleep
import logging
import scipy.stats as stats

# Настройка логирования
logging.basicConfig(level=logging.INFO, filename='srm_monitor.log', 
                    format='%(asctime)s - %(levelname)s - %(message)s')

# Конфигурация сервера SMTP
SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.example.com')
SMTP_PORT = int(os.getenv('SMTP_PORT', 587))
EMAIL_FROM = os.getenv('EMAIL_FROM', 'your_email@example.com')
EMAIL_TO = os.getenv('EMAIL_TO', 'alert_recipient@example.com')
RETRY_ATTEMPTS = 3

def send_alert(message):
    """
    Функция отправки уведомлений по электронной почте. В случае ошибки пытается повторить отправку.
    """
    msg = MIMEText(message)
    msg['Subject'] = 'ALERT: SRM detected in A/B Test'
    msg['From'] = EMAIL_FROM
    msg['To'] = EMAIL_TO
    
    for attempt in range(RETRY_ATTEMPTS):
        try:
            with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
                server.starttls()  # Использовать TLS для безопасной отправки
                server.login(EMAIL_FROM, os.getenv('EMAIL_PASSWORD'))
                server.sendmail(EMAIL_FROM, [EMAIL_TO], msg.as_string())
            logging.info("Alert sent successfully.")
            break
        except Exception as e:
            logging.error(f"Failed to send alert on attempt {attempt + 1}: {e}")
            sleep(2)  # Задержка перед повторной попыткой
        else:
            logging.error("All attempts to send alert failed.")

def monitor_srm(control, variant):
    """
    Функция мониторинга SRM. Проверяет распределение и отправляет уведомление при обнаружении SRM.
    """
    chi2, p_value = stats.chisquare(f_obs=[control, variant], f_exp=[(control + variant) * 0.5] * 2)
    logging.info(f"SRM Check: Control={control}, Variant={variant}, p-value={p_value:.3f}")
    
    if p_value < 0.05:
        send_alert(f"SRM detected: p-value = {p_value:.3f}")
        logging.warning(f"SRM issue detected with p-value: {p_value:.3f}")

# Пример автоматического запуска с помощью планировщика задач
def scheduled_check():
    """
    Пример запуска мониторинга через планировщик.
    """
    control_group_size = 15752
    variant_group_size = 15257
    monitor_srm(control_group_size, variant_group_size)

# Вызов функции, если скрипт запускается напрямую
if __name__ == '__main__':
    scheduled_check()

Задаем SMTP_SERVER, EMAIL_FROM, EMAIL_PASSWORD и другие параметры в переменных окружения, чтобы скрипт мог гибко использовать любые учетные данные.

Добавляем скрипт в cron или другой планировщик, чтобы мониторинг SRM выполнялся на регулярной основе.

Логи позволят отслеживать все проверки SRM. При обнаружении проблемы будет получено уведомление по почте, которое поможет оперативно реагировать на изменения в тесте

Что в итоге

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

А в рамках специализации «Системный аналитик» в ноябре пройдут следующие открытые уроки, на которые приглашаются все желающие:

© Habrahabr.ru