← Назад к вопросам
Какой дизайн эксперимента сделаешь для введения стикеров?
2.0 Middle🔥 61 комментариев
#A/B тестирование#Опыт и проекты
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Дизайн эксперимента для введения стикеров
Этот вопрос проверяет понимание того, как правильно тестировать новую фичу. Стикеры — это интерактивный элемент, который может влиять на engagement, retention, monetization и другие ключевые метрики. Нужен тщательный дизайн эксперимента. Рассмотрим полный подход от гипотезы к анализу.
Шаг 1: Формулировка гипотезы
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Hypothesis:
statement: str
hypothesis_type: str
expected_impact: dict
success_criteria: dict
# Формулируем нашу гипотезу
hypothesis = Hypothesis(
statement="Введение стикеров увеличит engagement и retention пользователей",
hypothesis_type="Behavioral / Engagement",
expected_impact={
'primary_metric': 'daily_active_messages_count',
'expected_lift': 0.10, # 10% увеличение
'secondary_metrics': ['session_duration', 'retention_day7', 'daily_active_users']
},
success_criteria={
'statistical_significance': 0.05,
'minimum_effect_size': 0.08, # 8% это OK
'confidence_level': 0.80
}
)
print("=== ГИПОТЕЗА ===")
print(f"H0 (Нулевая): Стикеры НЕ влияют на engagement")
print(f"H1 (Альтернативная): Стикеры УВЕЛИЧИВАЮТ engagement на {hypothesis.expected_impact['expected_lift']*100}%")
print(f"\nПервичная метрика: {hypothesis.expected_impact['primary_metric']}")
print(f"Вторичные метрики: {hypothesis.expected_impact['secondary_metrics']}")
Шаг 2: Расчёт размера выборки
from statsmodels.stats.power import tt_ind_solve_power
import numpy as np
def calculate_sample_size_for_experiment(
baseline_mean: float,
baseline_std: float,
expected_lift_pct: float,
alpha: float = 0.05,
power: float = 0.80
) -> dict:
"""
Рассчитать требуемый размер выборки
Args:
baseline_mean: базовое среднее значение метрики (напр., среднее кол-во сообщений)
baseline_std: стандартное отклонение
expected_lift_pct: ожидаемый лифт в %
alpha: уровень значимости (обычно 0.05)
power: статистическая мощность (обычно 0.80)
"""
# Базовые метрики (предполагаемые для мессенджера со стикерами)
# Среднее кол-во сообщений в день: 5
# Стандартное отклонение: 3
target_mean = baseline_mean * (1 + expected_lift_pct)
effect_size = (target_mean - baseline_mean) / baseline_std
# Рассчитываем требуемый размер выборки
n_per_group = tt_ind_solve_power(
effect_size=effect_size,
alpha=alpha,
power=power,
ratio=1.0, # Одинаковые группы
alternative='two-sided'
)
return {
'effect_size': effect_size,
'n_per_group': int(n_per_group),
'total_users': int(n_per_group * 2),
'baseline_mean': baseline_mean,
'target_mean': target_mean
}
# Расчёты для стикеров
results = calculate_sample_size_for_experiment(
baseline_mean=5, # 5 сообщений в день
baseline_std=3, # Стандартное отклонение
expected_lift_pct=0.10 # 10% лифт
)
print("\n=== РАЗМЕР ВЫБОРКИ ===")
print(f"Базовое среднее: {results['baseline_mean']:.1f} сообщений/день")
print(f"Ожидаемое среднее: {results['target_mean']:.2f} сообщений/день")
print(f"Effect size (Cohen's d): {results['effect_size']:.3f}")
print(f"Требуется пользователей на группу: {results['n_per_group']:,}")
print(f"Всего пользователей: {results['total_users']:,}")
print(f"\nПри 10000 DAU в день → {results['total_users'] / 10000:.1f} дней теста")
Шаг 3: Выбор дизайна эксперимента
Вариант A: Стандартный A/B тест (рекомендуется)
@dataclass
class ExperimentDesign:
name: str
control_group: dict
treatment_group: dict
split_ratio: tuple
duration_days: int
target_metrics: list
exclusion_criteria: list
design_ab = ExperimentDesign(
name="Standard A/B Test",
control_group={
'name': 'Control (без стикеров)',
'treatment': None,
'size_pct': 50
},
treatment_group={
'name': 'Treatment (со стикерами)',
'treatment': 'full_sticker_pack', # Все стикеры включены
'size_pct': 50
},
split_ratio=(0.5, 0.5),
duration_days=14, # 2 недели
target_metrics=[
'daily_messages_count',
'session_duration',
'daily_active_users',
'retention_day7',
'sticker_adoption_rate', # Сколько % юзеров используют стикеры
'sticker_usage_frequency' # Как часто используют
],
exclusion_criteria=[
'new_users_less_than_7_days', # Только активные юзеры
'bot_accounts',
'banned_users'
]
)
print(f"\n=== ДИЗАЙН ЭКСПЕРИМЕНТА: {design_ab.name} ===")
print(f"Контрольная группа: {design_ab.control_group['name']} ({design_ab.control_group['size_pct']}%)")
print(f"Экспериментальная группа: {design_ab.treatment_group['name']} ({design_ab.treatment_group['size_pct']}%)")
print(f"Длительность: {design_ab.duration_days} дней")
print(f"Исключение: {len(design_ab.exclusion_criteria)} критериев")
Вариант B: Градуальное раскатывание (Gradual Rollout)
Для снижения риска:
design_gradual = {
'name': 'Gradual Rollout',
'phase_1': {
'duration_days': 7,
'percentage': 0.05, # 5% юзеров
'goal': 'Обнаружить критические баги'
},
'phase_2': {
'duration_days': 7,
'percentage': 0.10, # 10% юзеров
'goal': 'Проверить стабильность на 10%'
},
'phase_3': {
'duration_days': 7,
'percentage': 0.50, # 50% юзеров
'goal': 'Полный эксперимент с достаточной выборкой'
},
'total_duration': 21, # 3 недели
}
print("\n=== ГРАДУАЛЬНОЕ РАСКАТЫВАНИЕ ===")
for phase, details in design_gradual.items():
if phase != 'total_duration':
print(f"\n{phase.upper()}:")
print(f" Дней: {details['duration_days']}")
print(f" Процент юзеров: {details['percentage']*100:.0f}%")
print(f" Цель: {details['goal']}")
Вариант C: Многовариантный тест (Multivariate / A/B/C)
Если есть несколько вариантов стикеров:
design_multivariate = {
'groups': [
{'name': 'Control (без стикеров)', 'size_pct': 25},
{'name': 'Variant A (стикеры, всегда видны)', 'size_pct': 25},
{'name': 'Variant B (стикеры, иконка)', 'size_pct': 25},
{'name': 'Variant C (стикеры, popup на первый чат)', 'size_pct': 25},
],
'duration_days': 21, # Нужно больше времени для 4 групп
'analysis_type': 'One-way ANOVA',
}
print("\n=== МНОГОВАРИАНТНЫЙ ТЕСТ ===")
for group in design_multivariate['groups']:
print(f" {group['name']:45s}: {group['size_pct']:3.0f}%")
Шаг 4: Техническая реализация
from enum import Enum
import hashlib
class ExperimentVariant(Enum):
CONTROL = 'control'
TREATMENT = 'treatment_stickers'
def assign_user_to_variant(user_id: str, experiment_id: str) -> ExperimentVariant:
"""
Детерминированное распределение юзера в группу
(один юзер всегда в одной группе)
"""
hash_input = f"{user_id}_{experiment_id}"
hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
# 50/50 split
if hash_value % 2 == 0:
return ExperimentVariant.CONTROL
else:
return ExperimentVariant.TREATMENT
# Пример
user_id = "user_12345"
experiment_id = "stickers_v1"
variant = assign_user_to_variant(user_id, experiment_id)
print(f"\nПользователь {user_id} → {variant.value}")
# Логирование события
def log_sticker_usage(user_id: str, event_type: str, variant: ExperimentVariant):
"""
Логируем использование стикеров
"""
event = {
'timestamp': datetime.now().isoformat(),
'user_id': user_id,
'experiment_variant': variant.value,
'event_type': event_type, # 'sticker_viewed', 'sticker_sent', etc.
'metadata': {}
}
# Отправляем в Analytics
print(f"Event logged: {event}")
# Примеры логирования
log_sticker_usage(user_id, 'sticker_viewed', variant)
log_sticker_usage(user_id, 'sticker_sent', variant)
Шаг 5: Метрики для отслеживания
from typing import Dict, List
class MetricsDefinition:
# PRIMARY METRICS (главные, для принятия решения)
primary_metrics = [
{
'name': 'Messages per DAU',
'definition': 'Среднее количество сообщений на активного пользователя в день',
'unit': 'count',
'expected_change': '+10%',
'guardrail': False
},
{
'name': 'Session Duration',
'definition': 'Среднее время проведенное в приложении',
'unit': 'seconds',
'expected_change': '+15%',
'guardrail': False
},
]
# SECONDARY METRICS (поддерживающие)
secondary_metrics = [
{
'name': 'DAU',
'definition': 'Количество дневных активных пользователей',
'unit': 'count',
'expected_change': 'No change (guardrail)',
'guardrail': True # Убедимся, что мы не теряем юзеров
},
{
'name': 'Retention D7',
'definition': '% пользователей, вернувшихся на день 7',
'unit': '%',
'expected_change': '+5%',
'guardrail': False
},
{
'name': 'Sticker Adoption',
'definition': '% юзеров, которые хотя бы раз используют стикеры',
'unit': '%',
'expected_change': '+30%',
'guardrail': False
},
]
# GUARDRAIL METRICS (чтобы не сломать ничего)
guardrail_metrics = [
{
'name': 'App Crashes',
'threshold_increase': '< 10%', # Не может увеличиться на 10%+
'critical': True
},
{
'name': 'Error Rate',
'threshold_increase': '< 5%',
'critical': True
},
{
'name': 'Churn Rate',
'threshold_increase': '< 2%',
'critical': False
},
]
# Вывод
metrics = MetricsDefinition()
print("\n=== PRIMARY METRICS ===")
for m in metrics.primary_metrics:
print(f"{m['name']:25s} | {m['definition']}")
print(f"{'':25s} Ожидаемо: {m['expected_change']}\n")
print("\n=== GUARDRAIL METRICS (не ломай!) ===")
for m in metrics.guardrail_metrics:
print(f"{m['name']:25s} | Лимит: {m['threshold_increase']}")
Шаг 6: Анализ результатов
import pandas as pd
from scipy import stats
class ExperimentAnalysis:
def __init__(self, control_data: pd.DataFrame, treatment_data: pd.DataFrame):
self.control = control_data
self.treatment = treatment_data
def analyze(self) -> dict:
results = {}
# Анализируем каждую метрику
for metric in ['messages', 'session_duration', 'retention']:
control_values = self.control[metric]
treatment_values = self.treatment[metric]
# T-тест
t_stat, p_value = stats.ttest_ind(control_values, treatment_values)
control_mean = control_values.mean()
treatment_mean = treatment_values.mean()
lift = ((treatment_mean - control_mean) / control_mean) * 100
# Cohen's d (effect size)
pooled_std = np.sqrt((control_values.std()**2 + treatment_values.std()**2) / 2)
cohens_d = (treatment_mean - control_mean) / pooled_std
results[metric] = {
'control_mean': control_mean,
'treatment_mean': treatment_mean,
'lift': lift,
'p_value': p_value,
'cohens_d': cohens_d,
'significant': p_value < 0.05
}
return results
def create_report(self, results: dict) -> str:
report = "\n=== РЕЗУЛЬТАТЫ ЭКСПЕРИМЕНТА ==="
for metric, stats_dict in results.items():
report += f"\n\n{metric.upper()}:\n"
report += f" Control: {stats_dict['control_mean']:.2f}\n"
report += f" Treatment: {stats_dict['treatment_mean']:.2f}\n"
report += f" Лифт: {stats_dict['lift']:+.1f}%\n"
report += f" P-value: {stats_dict['p_value']:.4f}\n"
report += f" Effect size (Cohen's d): {stats_dict['cohens_d']:.3f}\n"
if stats_dict['significant']:
report += f" ✓ СТАТИСТИЧЕСКИ ЗНАЧИМО (p < 0.05)\n"
else:
report += f" ✗ НЕ значимо (p >= 0.05)\n"
return report
# Пример вывода
print("\n=== ПРИМЕР ОТЧЁТА ===")
print("""
=== РЕЗУЛЬТАТЫ ЭКСПЕРИМЕНТА ===
MESSAGES PER DAU:
Control: 5.23
Treatment: 5.75
Лифт: +10.0%
P-value: 0.0234
Effect size (Cohen's d): 0.312
✓ СТАТИСТИЧЕСКИ ЗНАЧИМО (p < 0.05)
SESSION DURATION:
Control: 8.45 мин
Treatment: 9.23 мин
Лифт: +9.2%
P-value: 0.0892
Effect size (Cohen's d): 0.156
✗ НЕ значимо (p >= 0.05)
RETENTION D7:
Control: 42.1%
Treatment: 44.3%
Лифт: +5.2%
P-value: 0.1234
Effect size (Cohen's d): 0.089
✗ НЕ значимо (p >= 0.05)
""")
Шаг 7: Решение и Рекомендация
class ExperimentDecision:
def __init__(self, results: dict):
self.results = results
def make_decision(self) -> str:
"""
Принять решение на основе результатов
"""
significant_wins = 0
significant_losses = 0
for metric, stats_dict in self.results.items():
if stats_dict['significant']:
if stats_dict['lift'] > 0:
significant_wins += 1
else:
significant_losses += 1
if significant_losses > 0:
return "ROLLBACK - Есть значимое ухудшение"
elif significant_wins > 1:
return "ROLLOUT - Есть значимое улучшение"
elif significant_wins == 1:
return "INCONCLUSIVE - Нужны уточнения или расширение теста"
else:
return "NO EFFECT - Стикеры не влияют на метрики"
# Пример
decision_maker = ExperimentDecision({
'messages': {'significant': True, 'lift': 10.0},
'session_duration': {'significant': False, 'lift': 9.2},
'retention': {'significant': False, 'lift': 5.2}
})
print(f"\n=== РЕШЕНИЕ ===")
print(decision_maker.make_decision())
print("\nПримечание: Стикеры УВЕЛИЧИВАЮТ количество сообщений на 10% (значимо).")
print("Время в приложении и retention растут, но не значимо.")
print("РЕКОМЕНДАЦИЯ: Раскатать стикеры для всех пользователей.")
Практический План Эксперимента (Summary)
experiment_plan = {
'Гипотеза': 'Стикеры увеличивают engagement (+10%)',
'Дизайн': 'Стандартный A/B тест, 50/50 split',
'Размер выборки': 50000 пользователей (25000 на группу)',
'Длительность': 14 дней,
'Первичная метрика': 'Messages per DAU',
'Вторичные': ['Session duration', 'Retention D7'],
'Guardrails': ['App crashes', 'Error rate'],
'Уровень значимости': 0.05,
'Мощность': 0.80,
'Критерий успеха': 'Lift > 8% и p < 0.05',
'Запуск': 'Gradual rollout: 5% → 10% → 50%',
'Риски': ['User confusion with new feature', 'Technical issues'],
'Rollback condition': 'Crashes > +10% OR Errors > +5%',
}
for key, value in experiment_plan.items():
print(f"{key:25s}: {value}")