Стратификация: как не облажаться с A/B тестами

от автора

Привет, Хабр!

Представьте: вы запускаете A/B тест. Цель проста: проверить, работает ли новая кнопка лучше старой. Но тут же возникает мысль: «А вдруг мобильные юзеры и десктопные реагируют по‑разному? А что с новыми пользователями? Их мнение ведь явно не равноценное опытным юзерам». Без стратификации результат может быть так себе.

Что такое стратификация? Это способ сделать A/B тесты чуточку честнее. Берем выборку, делим ее на однородные группы — страты — по ключевым признакам (например, устройство и статус пользователя), а потом уже распределяем юзеров в группы А и Б.

Применять стратификацию стоит, если:

  1. Много сегментов: например, мобильные и десктопные пользователи, которые ведут себя, как север и юг: вроде люди, но с совершенно разными привычками.

  2. Важно сохранить баланс: вы же не хотите, чтобы в одной группе было 90% новичков, а в другой — одни ветераны интерфейсов.

  3. Выборка достаточно большая: если страт больше, чем участников, тест превращается в цирк с мизерными данными.

Но не надо стратифицировать все подряд. Для мелких тестов или быстрых проверок «а что, если сделать кнопку розовой?» — проще оставить все как есть.

Реализация стратификации в A/B тестах

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

import pandas as pd from sklearn.model_selection import train_test_split  # Создаём пример данных data = pd.DataFrame({     'user_id': range(1, 1001),     'is_new': [1 if x < 500 else 0 for x in range(1000)],  # Первые 500 новых пользователей     'device': ['mobile' if x % 2 == 0 else 'desktop' for x in range(1000)],     'conversion': [0]*1000  # Здесь будут результаты теста })  print(data.head()) 
   user_id  is_new  device  conversion 0        1       1  mobile           0 1        2       1 desktop           0 2        3       1  mobile           0 3        4       1 desktop           0 4        5       1  mobile           0

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

Теперь объединим категории is_new и device, чтобы создать страты. Это позволит учитывать два фактора при распределении пользователей.

# Создаём страты, объединяя категории data['stratum'] = data['is_new'].astype(str) + '_' + data['device']  print(data['stratum'].value_counts())
1_mobile      250 0_mobile      250 1_desktop     250 0_desktop     250 Name: stratum, dtype: int64 

Получили ровно четыре страты: новые мобильные, старые десктопные и так далее. Теперь разделяем всё это добро на группы A и B. Используем функцию train_test_split с параметром stratify, чтобы сохранить пропорции страт в обеих группах.

group_a, group_b = train_test_split(     data,     test_size=0.5,     stratify=data['stratum'],     random_state=42 )  print("Group A size:", group_a.shape[0]) print("Group B size:", group_b.shape[0])
Group A size: 500 Group B size: 500

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

Проверим, что страты распределились равномерно между группами A и B.

print("Group A strata distribution:") print(group_a['stratum'].value_counts(normalize=True))  print("\nGroup B strata distribution:") print(group_b['stratum'].value_counts(normalize=True))
Group A strata distribution: 1_mobile      0.25 0_mobile      0.25 1_desktop     0.25 0_desktop     0.25 Name: stratum, dtype: float64  Group B strata distribution: 1_mobile      0.25 0_mobile      0.25 1_desktop     0.25 0_desktop     0.25 Name: stratum, dtype: float64

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

Можно запускать тест и анализировать результаты. Для примера симулируем конверсии и проведем статистический тест:

import numpy as np from scipy.stats import chi2_contingency  # Симуляция конверсий np.random.seed(42) group_a['conversion'] = np.random.binomial(1, 0.10, size=group_a.shape[0])  # Группа A конверсия 10% group_b['conversion'] = np.random.binomial(1, 0.12, size=group_b.shape[0])  # Группа B конверсия 12%  # Подсчёт конверсий conversions_a = group_a['conversion'].sum() conversions_b = group_b['conversion'].sum()  print(f"Group A conversions: {conversions_a} / {group_a.shape[0]}") print(f"Group B conversions: {conversions_b} / {group_b.shape[0]}")
Group A conversions: 49 / 500 Group B conversions: 62 / 500

В группе A конверсия 9.8%, а в группе B — 12.4%. Теперь проверим, значима ли эта разница.

Проведём тест хи‑квадрат, чтобы определить, значима ли разница между группами.

# Создание таблицы сопряжённости contingency_table = pd.DataFrame({     'A': [conversions_a, group_a.shape[0] - conversions_a],     'B': [conversions_b, group_b.shape[0] - conversions_b] }, index=['Converted', 'Not Converted'])  print("Contingency Table:") print(contingency_table)  # Тест Хи-квадрат chi2, p, dof, ex = chi2_contingency(contingency_table)  print(f"\nChi2 Statistic: {chi2}") print(f"P-value: {p}")
Contingency Table:               A   B Converted    49  62 Not Converted 451 438  Chi2 Statistic: 2.0408163265306123 P-value: 0.15223670058397293

P‑value = 0.152 больше стандартного уровня значимости 0.05, что означает, что разница в конверсиях между группами A и B статистически не значима.

Иногда нужно учитывать больше факторов. Добавим ещё одну характеристику — географию пользователя.

# Добавим географию data['country'] = ['USA' if x < 333 else 'Canada' if x < 666 else 'UK' for x in range(1000)]  # Обновляем страты data['stratum'] = data['is_new'].astype(str) + '_' + data['device'] + '_' + data['country']  print(data['stratum'].value_counts())
1_mobile_USA        167 0_mobile_USA        167 1_mobile_Canada     167 0_mobile_Canada     167 1_mobile_UK         166 0_mobile_UK         166 1_desktop_USA       167 0_desktop_USA       167 1_desktop_Canada    167 0_desktop_Canada    167 1_desktop_UK        166 0_desktop_UK        166 Name: stratum, dtype: int64

Теперь у нас 12 страт, каждая с примерно 167 участниками. Чем больше факторов вы учитываете, тем более точным становится ваш тест.

Разделение с учетом новых страт:

# Разделение на группы A и B с учётом новых страт group_a, group_b = train_test_split(     data,     test_size=0.5,     stratify=data['stratum'],     random_state=42 )  print("Group A strata distribution:") print(group_a['stratum'].value_counts(normalize=True))  print("\nGroup B strata distribution:") print(group_b['stratum'].value_counts(normalize=True)) 
Group A strata distribution: 1_mobile_USA         0.083333 0_mobile_USA         0.083333 1_mobile_Canada      0.083333 0_mobile_Canada      0.083333 1_mobile_UK          0.083333 0_mobile_UK          0.083333 1_desktop_USA       0.083333 0_desktop_USA       0.083333 1_desktop_Canada    0.083333 0_desktop_Canada    0.083333 1_desktop_UK        0.083333 0_desktop_UK        0.083333 Name: stratum, dtype: float64  Group B strata distribution: 1_mobile_USA         0.083333 0_mobile_USA         0.083333 1_mobile_Canada      0.083333 0_mobile_Canada      0.083333 1_mobile_UK          0.083333 0_mobile_UK          0.083333 1_desktop_USA       0.083333 0_desktop_USA       0.083333 1_desktop_Canada    0.083333 0_desktop_Canada    0.083333 1_desktop_UK        0.083333 0_desktop_UK        0.083333 Name: stratum, dtype: float64

Каждая из 12 страт равномерно распределена между группами A и B.


Стратификация — штука мощная, но будем честны: иногда она ни к чему. Если у вас выборка меньше, чем число страт, или нужно быстро проверить гипотезу «а что если добавить котиков в интерфейс?», — не усложняйте себе жизнь. Иногда достаточно простого случайного деления, и мир продолжит вращаться.

Какие нестандартные способы стратификации вы использовали? Делитесь своими кейсами в комментариях.

27 декабря в Otus пройдет открытый урок «Визуализация данных. Основные „финансовые“ графики, работа с mplfinance». На нем научимся строить графики в формате, принятом для анализа финансовых данных. Рассмотрим, что такое свечные графики; научимся строить дополнительные линии на графиках и доверительные интервалы. Записаться.

Все темы открытых уроков можно посмотреть в календаре.


ссылка на оригинал статьи https://habr.com/ru/articles/867666/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *