Привет, Хабр!
Представьте: вы запускаете A/B тест. Цель проста: проверить, работает ли новая кнопка лучше старой. Но тут же возникает мысль: «А вдруг мобильные юзеры и десктопные реагируют по‑разному? А что с новыми пользователями? Их мнение ведь явно не равноценное опытным юзерам». Без стратификации результат может быть так себе.
Что такое стратификация? Это способ сделать A/B тесты чуточку честнее. Берем выборку, делим ее на однородные группы — страты — по ключевым признакам (например, устройство и статус пользователя), а потом уже распределяем юзеров в группы А и Б.
Применять стратификацию стоит, если:
-
Много сегментов: например, мобильные и десктопные пользователи, которые ведут себя, как север и юг: вроде люди, но с совершенно разными привычками.
-
Важно сохранить баланс: вы же не хотите, чтобы в одной группе было 90% новичков, а в другой — одни ветераны интерфейсов.
-
Выборка достаточно большая: если страт больше, чем участников, тест превращается в цирк с мизерными данными.
Но не надо стратифицировать все подряд. Для мелких тестов или быстрых проверок «а что, если сделать кнопку розовой?» — проще оставить все как есть.
Реализация стратификации в 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/
Добавить комментарий