Туториал: Дизайн и оптимизация эксперимента
Этот туториал посвящён возможностям библиотеки aboba для дизайна экспериментов. Вы узнаете, как оптимизировать параметры эксперимента, рассчитывать оптимальный размер выборки и проектировать временные эксперименты для достижения максимальной статистической мощности.
Понимание дизайна эксперимента
Дизайн эксперимента в aboba помогает ответить на ключевые вопросы ещё до запуска AB-теста:
- Сколько наблюдений мне нужно? — определить минимальный размер выборки для нужной статистической мощности
- Какие параметры выбрать? — найти оптимальную конфигурацию эксперимента
- Как долго должен идти эксперимент? — для экспериментов во времени подобрать подходящую длительность
- Какая ожидаемая мощность? — оценить ошибки I и II рода
Библиотека предоставляет три класса для дизайна экспериментов:
- BaseExperimentDesigner — максимум гибкости, с кастомными фабриками экспериментов
- BasicExperimentDesigner — упрощённый интерфейс для типовых сценариев
- TimeBasedDesigner — специализированный дизайнер для экспериментов с временными рядами
Пример 1: Кастомный дизайн эксперимента с BaseExperimentDesigner
Класс BaseExperimentDesigner даёт полный контроль над процессом дизайна эксперимента. Вы определяете фабричный класс, который создаёт эксперименты с разными параметрами.
Как устроен workflow
Дизайнер работает следующим образом:
- Создаёт эксперименты для различных комбинаций параметров.
- Запускает AA-тесты для измерения ошибки I рода (false positive rate, вероятность обнаружить эффект, когда его нет ).
- Запускает AB-тесты с синтетическим эффектом для измерения ошибки II рода (false negative rate, вероятность пропустить реальный эффект, т.е. 1− мощность).
- Находит оптимальные параметры, обеспечивающие баланс между этими ошибками.
Шаг 1: Создание фабрики экспериментов
import numpy as np
import pandas as pd
import scipy.stats as sps
import typing as tp
from aboba.design.base_designer import (
ExperimentDesignMetrics,
IntervalEstimate,
BaseExperimentDesigner
)
import aboba
class ExperimentSetup:
"""Фабрика, создающая эксперименты с разным размером выборки."""
def generate_data(self, n_samples: int) -> pd.DataFrame:
"""Сгенерировать синтетические данные из нормального распределения."""
data_a = sps.norm.rvs(size=n_samples, loc=0, scale=1)
data_b = sps.norm.rvs(size=n_samples, loc=0, scale=1)
data = pd.DataFrame({
'value': np.concatenate([data_a, data_b]),
'b_group': np.concatenate([
np.repeat(0, n_samples),
np.repeat(1, n_samples),
]),
})
return data
def generate_test(self) -> aboba.base.base_test.BaseTest:
"""Создать статистический тест."""
return aboba.tests.AbsoluteIndependentTTest(
value_column='value',
)
def generate_pipeline(self, n_samples: int) -> aboba.pipeline.Pipeline:
"""Создать пайплайн сэмплирования на основе размера выборки."""
# Выборка 10% данных, минимум 2 наблюдения на группу
group_size = max(n_samples // 10, 2)
return aboba.pipeline.Pipeline([
("GroupSplitter", aboba.splitters.GroupSplitter(
size=group_size,
column='b_group'
)),
])
def __call__(self, parameters: tp.Dict[str, tp.Any]) -> ExperimentDesignMetrics:
"""
Запустить эксперимент с заданными параметрами и вернуть метрики.
Метод вызывается дизайнером для каждой комбинации параметров.
"""
assert sorted(parameters.keys()) == ["n_samples"]
n_samples = parameters["n_samples"]
# Генерация компонентов эксперимента
data = self.generate_data(n_samples)
pipeline = self.generate_pipeline(n_samples)
test = self.generate_test()
# Создание эксперимента
experiment = aboba.experiment.AbobaExperiment()
# Запуск AA-теста для оценки ошибки I рода
aa_group = experiment.group(
"AA",
test=test,
data=data,
data_pipeline=pipeline,
n_iter=1000,
joblib_kwargs={"n_jobs": -1, "backend": "threading"}
).run()
alpha_level = 0.05
# Расчёт ошибки I рода: доля p-value ниже alpha
type_I_error = (aa_group.get_data()["pvalue"] < alpha_level).mean()
# Запуск AB-теста с синтетическим эффектом для оценки мощности
ab_group = experiment.group(
"AB",
test=test,
data=data,
data_pipeline=pipeline,
synthetic_effect=aboba.effect_modifiers.GroupModifier(
effects={1: 0.3},
value_column='value',
group_column='b_group',
),
n_iter=1000,
).run()
# Расчёт мощности: доля обнаруженных эффектов
type_II_error = (ab_group.get_data()["pvalue"] < alpha_level).mean()
return ExperimentDesignMetrics(
type_I_error=IntervalEstimate(
parameter_estimate=type_I_error
),
type_II_error=IntervalEstimate(
parameter_estimate=type_II_error
),
)
Шаг 2: Создание и запуск дизайнера
Теперь используем дизайнер для подбора оптимального размера выборки:
# Создаём дизайнер с ограничениями на размер выборки
designer = BaseExperimentDesigner(
experiment_design_factory=ExperimentSetup(),
constraints={
# Проверяем 20 разных размеров выборки от 10 до 100 000
"n_samples": np.array(np.logspace(1, 5, 20), dtype=np.int32)
}
)
# Подбираем оптимальные параметры перебором (brute force)
designer.optimize(method=BaseExperimentDesigner.OptimizerMethod.BRUTE_FORCE)
# Визуализируем результаты
designer.visualize()
-
ошибку I рода в зависимости от размера выборки (должна быть около 0.05)
-
статистическую мощность (1− ошибка II рода)
-
оптимальный размер выборки, который обеспечивает требуемую мощность при одновременном контроле ошибки I рода
Пример 2: Упрощённый дизайн с BasicExperimentDesigner
Для большинства сценариев достаточно BasicExperimentDesigner который не требует определения фабричного класса.
Шаг 1: Подготовка данных и теста
import numpy as np
import pandas as pd
import scipy.stats as sps
from aboba.design.basic_designer import BasicExperimentDesigner
from aboba.tests import AbsoluteIndependentTTest
from aboba.splitters import GroupSplitter
from aboba.pipeline import Pipeline
from aboba.effect_modifiers import GroupModifier
# Генерация примера данных
n_samples = 1000
data_a = sps.norm.rvs(size=n_samples, loc=0, scale=1)
data_b = sps.norm.rvs(size=n_samples, loc=0, scale=1)
data = pd.DataFrame({
'value': np.concatenate([data_a, data_b]),
'b_group': np.concatenate([
np.repeat(0, n_samples),
np.repeat(1, n_samples),
]),
})
Шаг 2: Определение теста и эффекта
# Задаём статистический тест
test = AbsoluteIndependentTTest(value_column='value')
# Задаём синтетический эффект, который будем проверять
synthetic_effect = GroupModifier(
effects={1: 0.3}, # Прибавить 0.3 к группе 1
value_column='value',
group_column='b_group',
)
Шаг 3: Создание дизайнера с ограничениями на параметры
Ключевая особенность — функция get_pipeline, которая создаёт пайплайны на основе переданных параметров:
# Создаём дизайнер с ограничениями по параметрам
designer = BasicExperimentDesigner(
data=data,
test=test,
get_pipeline=lambda params: Pipeline([
('GroupSplitter', GroupSplitter(
size=params['group_size'], # Используем значение параметра
column='b_group'
)),
]),
synthetic_effect=synthetic_effect,
n_iter=1000, # Число итераций для каждой комбинации параметров
constraints={
"group_size": [50, 100, 200, 500], # Проверяем эти размеры групп
}
)
Шаг 4: Оптимизация и анализ
# Подбираем оптимальные параметры
designer.optimize()
# Получаем лучшие параметры
best_params = designer.get_best_params()
print(f"Лучший размер группы: {best_params.parameters['group_size']}")
# Визуализируем результаты
designer.visualize()
-
Тестировать каждый размер группы (50, 100, 200, 500)
-
Выполнять 1000 итераций для каждого размера, чтобы оценить уровни ошибок
-
Находить размер группы, который обеспечивает лучший баланс между статистической мощностью и контролем ошибки I рода
-
Отображать визуализации, показывающие качество работы по всем протестированным параметрам
Пример 3: Проектирование временного эксперимента
TimeBasedDesigner специализируется на экспериментах с временными рядами, где требуется оптимизировать временные параметры, такие как длительность эксперимента и дата начала эффекта.
Понимание временных экспериментов
Временные эксперименты имеют свои особенности:
- Дата включения: момент, когда пользователи попадают в эксперимент
- Дата начала эффекта: момент, когда начинает проявляться эффект
- Длительность эксперимента: сколько времени проводится эксперимент
- Эффекты, меняющиеся во времени: эффекты, величина которых изменяется со временем
Шаг 1: Генерация временных рядов данных
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from aboba.design.time_based_designer import TimeBasedDesigner
from aboba.tests import AbsoluteIndependentTTest
from aboba.splitters import UserSplitter
from aboba.pipeline import Pipeline
from aboba.effect_modifiers import TimeBasedEffectModifier
from aboba.utils.time_based_data_generator import generate_time_based_data
# Генерируем синтетический временной ряд
# Данные на уровне пользователей: даты и платежи
data = generate_time_based_data(
n_users=200,
start_date='2024-01-01',
end_date='2024-12-31'
)
Сгенерированные данные включают:
-
user_id: уникальный идентификатор пользователя -
date: дата каждого наблюдения -
payment: сумма платежа (целевая метрика) -
is_in_b_group: принадлежность к группе -
inclusion_date: момент, когда пользователь вошёл в эксперимент
Шаг 2: Определение теста и фабрики пайплайна
# Задаём статистический тест
test = AbsoluteIndependentTTest(value_column='payment')
# Собираем пайплайн по параметрам
get_pipeline = lambda params: Pipeline([
('UserSplitter', UserSplitter(
group_column='is_in_b_group',
user_column='user_id',
size=params.get('user_sample_size', 50) #Используем параметр или значение по умолчанию
))
])
Шаг 3: Определение фабрики временного эффекта
Фабрика эффектов создаёт эффекты на основе временных параметров:
get_effect = lambda params: TimeBasedEffectModifier(
effect=5.0 * params.get('effect_multiplier', 1.0), # Масштабируем эффект
effect_start_date=pd.to_datetime(params['effect_start_date']),
value_column='payment',
date_column='date',
group_column='is_in_b_group',
inclusion_date_column='inclusion_date'
)
-
применяются только после
effect_start_date -
затрагивают только пользователей, которые были включены до начала эффекта
-
масштабируются параметром
effect_multiplier
Шаг 4: Создание дизайнера с временными ограничениями
# Создаём дизайнер с ограничениями по параметрам
designer = TimeBasedDesigner(
data=data,
test=test,
get_pipeline=get_pipeline,
get_effect=get_effect,
date_column='date',
experiment_duration='experiment_duration', # имя параметра
effect_start_date='effect_start_date', # имя параметра
n_iter=100,
constraints={
'effect_start_date': [
'2024-03-01',
'2024-04-01',
'2024-05-01'
],
'effect_multiplier': [0.8, 1.0, 1.2, 1.5],
'experiment_duration': [
pd.Timedelta(weeks=4),
pd.Timedelta(weeks=6),
pd.Timedelta(weeks=8)
]
}
)
Шаг 5: Оптимизация и визуализация
# Подбираем оптимальные параметры
designer.optimize()
# Получаем лучшие параметры
best_params = designer.get_best_params()
print(f"Лучшая дата начала эффекта: {best_params.parameters['effect_start_date']}")
print(f"Лучший множитель эффекта: {best_params.parameters['effect_multiplier']}")
print(f"Лучшая длительность эксперимента: {best_params.parameters['experiment_duration']}")
# Визуализируем все результаты
designer.visualize()
Расширенная визуализация: фиксированные параметры
Можно визуализировать результаты, зафиксировав некоторые параметры:
# Визуализация при фиксированных длительности эксперимента и дате начала эффекта
designer.visualize(
fixed_parameters={
'experiment_duration': pd.Timedelta(weeks=4),
'effect_start_date': '2024-03-01',
}
)
Это показывает, как оставшиеся параметры (например, effect_multiplier) влияют на качество результатов, когда остальные параметры зафиксированы.
Ключевые понятия в дизайне экспериментов
Ошибка I рода (False Positive Rate)
- Определение: вероятность обнаружить эффект, когда его на самом деле нет
- Цель: должна быть близка к уровню значимости (обычно 0.05)
- Оценивается с помощью: проведения AA-тестов (обе группы получены из одного распределения)
Ошибка II рода и статистическая мощность
- Ошибка II рода: вероятность не обнаружить реальный эффект
- Статистическая мощность: 1 − ошибка II рода (вероятность обнаружить реальный эффект)
- Цель: обычно стремятся к мощности 80% (ошибка II рода = 0.20)
- Оценивается с помощью: проведения AB-тестов с заранее заданными синтетическими эффектами
Стратегия оптимизации
Дизайнеры подбирают параметры, которые:
-
поддерживают ошибку I рода на уровне, близком к уровню значимости (без завышения)
-
максимизируют статистическую мощность (минимизируют ошибку II рода)
-
учитывают практические ограничения (размер выборки, длительность, стоимость)