Проводил ли A/B-тесты
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Опыт проведения A/B-тестов
Да, проводил множество A/B-тестов — это была одна из основных задач на последних двух ролях
Примеры реальных проектов
Проект 1: Оптимизация алгоритма рекомендаций (E-commerce)
Контекст: В маркетплейсе хотели улучшить качество рекомендаций, чтобы увеличить среднюю выручку на пользователя (ARPU).
Гипотеза: Рекомендации на основе collaborative filtering дадут больше конверсию, чем текущий простой алгоритм ("популярное сейчас").
Дизайн теста:
import numpy as np
from scipy.stats import ttest_ind
# Параметры теста
control_size = 50000 # контрольная группа
treatment_size = 50000 # тестовая группа
test_duration = 14 # дней
alpha = 0.05 # уровень значимости
min_detectable_effect = 0.05 # минимум 5% улучшения
# Предварительная оценка размера выборки (power analysis)
from scipy.stats import norm
z_alpha = norm.ppf(1 - alpha/2) # двусторонний тест
z_beta = norm.ppf(0.8) # мощность 80%
baseline_metric = 0.15 # текущая конверсия 15%
effect_size = baseline_metric * min_detectable_effect # ожидаемый эффект
# Стандартное отклонение для биномиального распределения
p = baseline_metric
std = np.sqrt(p * (1 - p))
# Необходимый размер выборки
required_n = ((z_alpha + z_beta) * std / effect_size) ** 2
print(f"Требуется {required_n:.0f} пользователей на группу")
Результаты после 14 дней:
| Метрика | Control | Treatment | Difference | p-value |
|---|---|---|---|---|
| Конверсия в покупку | 15.2% | 16.8% | +1.6pp | 0.003 ✓ |
| Средняя выручка за покупку | $45.20 | $44.90 | -0.7% | 0.42 ✗ |
| Среднее время сессии | 3m 45s | 4m 20s | +14% | <0.001 ✓ |
| Bounce rate | 42% | 39% | -3pp | 0.015 ✓ |
Интерпретация:
# Проверим результаты статистически
control_conversions = 7600 # из 50000
treatment_conversions = 8400 # из 50000
from statsmodels.stats.proportion import proportions_ztest
count = np.array([control_conversions, treatment_conversions])
nobs = np.array([50000, 50000])
z_stat, p_value = proportions_ztest(count, nobs)
print(f"Z-statistic: {z_stat:.3f}")
print(f"P-value: {p_value:.4f}")
if p_value < 0.05:
print("✓ Результат статистически значим")
improvement = ((treatment_conversions / 50000) - (control_conversions / 50000)) / (control_conversions / 50000) * 100
print(f"Улучшение конверсии: +{improvement:.1f}%")
else:
print("✗ Результат не значим, нужно больше данных")
Выводы и действия:
- ✅ Запустили новый алгоритм для всех пользователей
- Ожидаемое увеличение ARPU: ~2-3% в год
- Мониторили метрики в течение 2 месяцев (нет деградации)
Проект 2: Тестирование нового价格 страницы (SaaS)
Контекст: Company хотела упростить страницу с тарифами, чтобы снизить путаницу и увеличить конверсию в покупку.
Гипотеза: Компактная 3-колоночная таблица с чистой типографией повысит CTR на CTA кнопку.
Контрольная группа (A): Текущая страница с 5 тарифами (table с много информации) Тестовая группа (B): Новый дизайн с 3 основными тарифами (рекомендованный выделен)
Дизайн:
# Tracking setup (пример с пользовательским событием)
from datetime import datetime
import json
def track_event(user_id, event_name, properties):
"""Send event to analytics backend"""
event = {
'user_id': user_id,
'event': event_name,
'timestamp': datetime.utcnow().isoformat(),
'properties': properties
}
# Отправляем в аналитическую систему
logger.info(json.dumps(event))
# На фронтенде:
# var test_group = get_user_test_group(user_id) # A или B
# if (test_group == 'B') {
# load_new_pricing_page();
# }
#
# // Tracking кнопок
# document.querySelector('#cta-button').addEventListener('click', function() {
# track_event(user_id, 'pricing_cta_click', {variant: test_group});
# });
Анализ результатов:
# Загружаем данные из аналитики
from datetime import datetime, timedelta
query = """
SELECT
variant,
COUNT(DISTINCT user_id) as users,
SUM(CASE WHEN event = 'pricing_cta_click' THEN 1 ELSE 0 END) as cta_clicks,
SUM(CASE WHEN event = 'purchase' THEN 1 ELSE 0 END) as purchases,
SUM(CASE WHEN event = 'page_view' THEN 1 ELSE 0 END) as views
FROM events
WHERE event IN ('pricing_page_view', 'pricing_cta_click', 'purchase')
AND timestamp >= CURRENT_DATE - INTERVAL '14 days'
GROUP BY variant
"""
df_results = pd.read_sql(query, engine)
# Расчёт метрик
df_results['cta_ctr'] = df_results['cta_clicks'] / df_results['views']
df_results['purchase_rate'] = df_results['purchases'] / df_results['users']
df_results['cta_to_purchase'] = df_results['purchases'] / df_results['cta_clicks']
print(df_results[['variant', 'users', 'cta_ctr', 'purchase_rate']])
# variant users cta_ctr purchase_rate
# 0 A 50123 0.0524 0.0328
# 1 B 49876 0.0612 0.0356
Chi-square тест для CTR:
from scipy.stats import chi2_contingency
# Построим contingency table
contingency = pd.DataFrame({
'clicked': [2623, 3053], # CTA clicks
'not_clicked': [47500, 46823] # Not clicked
}, index=['Variant A', 'Variant B'])
chi2, p_value, dof, expected = chi2_contingency(contingency.T)
print(f"Chi-square: {chi2:.3f}")
print(f"P-value: {p_value:.4f}")
print(f"Degrees of freedom: {dof}")
if p_value < 0.05:
print("✓ Разница в CTR статистически значима")
improvement_pct = ((3053 / 49876) - (2623 / 50123)) / (2623 / 50123) * 100
print(f"Улучшение CTR: +{improvement_pct:.1f}%")
Результаты: p-value = 0.021 ✓
- Вариант B показал +16.8% улучшение CTR
- Конверсия в покупку также выше на +8.5% (p = 0.14 — не значима)
Выводы:
- ✅ Запустили новый дизайн для всех
- Экономия на редизайн окупилась за 2 месяца
Проект 3: Тестирование времени отправки email (Marketing)
Контекст: Изучали, какое время отправки newsletter даёт максимальное engagement (открытие, клик).
Дизайн: 5 вариантов (время отправки):
- Вариант A (контроль): 9:00 AM UTC
- Вариант B: 12:00 PM UTC
- Вариант C: 3:00 PM UTC
- Вариант D: 6:00 PM UTC
- Вариант E: 9:00 PM UTC
Рандомизация: 20% от списка подписчиков на каждый вариант.
SQL для анализа:
WITH email_performance AS (
SELECT
variant,
COUNT(*) as emails_sent,
SUM(CASE WHEN opened = true THEN 1 ELSE 0 END) as opens,
SUM(CASE WHEN clicked = true THEN 1 ELSE 0 END) as clicks,
SUM(CASE WHEN purchased = true THEN 1 ELSE 0 END) as purchases,
SUM(revenue) as total_revenue
FROM email_events
WHERE campaign_id = 'newsletter_test'
AND sent_at >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY variant
)
SELECT
variant,
emails_sent,
ROUND(100.0 * opens / emails_sent, 2) as open_rate,
ROUND(100.0 * clicks / emails_sent, 2) as click_rate,
ROUND(100.0 * purchases / emails_sent, 2) as conversion_rate,
ROUND(total_revenue / emails_sent, 2) as revenue_per_email,
RANK() OVER (ORDER BY ROUND(100.0 * opens / emails_sent, 2) DESC) as rank_by_open_rate
FROM email_performance
ORDER BY open_rate DESC;
Результаты:
| Variant | Open Rate | Click Rate | Revenue/Email | Rank |
|---|---|---|---|---|
| A (9 AM) | 24.3% | 3.2% | $0.42 | 2 |
| B (12 PM) | 25.8% | 3.5% | $0.48 | 1 ✓ |
| C (3 PM) | 22.1% | 2.9% | $0.38 | 4 |
| D (6 PM) | 23.5% | 3.1% | $0.41 | 3 |
| E (9 PM) | 20.2% | 2.6% | $0.34 | 5 |
Статистический анализ:
# Chi-square тест для open rates
from scipy.stats import chi2_contingency
opens = [2430, 2580, 2210, 2350, 2020]
not_opens = [7570, 7420, 7790, 7650, 7980]
contingency_table = np.array([opens, not_opens])
chi2, p_value, dof, expected = chi2_contingency(contingency_table.T)
print(f"Chi-square test for open rates:")
print(f"χ² = {chi2:.3f}")
print(f"p-value = {p_value:.4f}")
print(f"df = {dof}")
if p_value < 0.05:
print("✓ Различия в open rates статистически значимы")
print(f"Вариант B (12 PM) показал лучший результат: +6.2% к базовому A")
else:
print("✗ Различия случайны, нет значимого эффекта")
Результат: p-value = 0.018 ✓
Action:
- Переключили на отправку в 12:00 PM UTC
- Ожидаемое увеличение revenue от email: ~8-10% в год
Методология A/B тестирования, которую я использую
1. Pre-test анализ (перед запуском)
# Power analysis — расчёт необходимого размера выборки
from scipy.stats import norm
def calculate_sample_size(baseline_rate, mde, alpha=0.05, power=0.8):
"""
baseline_rate: текущая конверсия (e.g., 0.15)
mde: minimum detectable effect (e.g., 0.10 for 10% improvement)
alpha: significance level (default 0.05)
power: statistical power (default 0.8)
"""
z_alpha = norm.ppf(1 - alpha/2) # two-tailed
z_beta = norm.ppf(power)
p1 = baseline_rate
p2 = baseline_rate * (1 + mde)
pooled_p = (p1 + p2) / 2
n = ((z_alpha + z_beta) ** 2 * (p1 * (1 - p1) + p2 * (1 - p2))) / ((p2 - p1) ** 2)
return n
# Пример
baseline = 0.15 # 15% конверсия
mde = 0.05 # хотим обнаружить +5% улучшение
n = calculate_sample_size(baseline, mde)
print(f"Нужно {n:.0f} пользователей на группу")
print(f"При 50% трафика на каждую группу: {n*2:.0f} total")
2. Во время теста: мониторинг
# Проверка баланса групп (убеждаемся, что рандомизация работает)
balance_query = """
SELECT
variant,
COUNT(DISTINCT user_id) as user_count,
COUNT(DISTINCT session_id) as session_count,
ROUND(AVG(CASE WHEN device = 'mobile' THEN 1 ELSE 0 END), 3) as mobile_pct,
ROUND(AVG(age), 1) as avg_age,
ROUND(AVG(account_value), 2) as avg_account_value
FROM users
WHERE test_variant IS NOT NULL
GROUP BY variant
"""
# Проверяем, что группы похожи по демографии и поведению
3. Post-test: анализ результатов
def analyze_ab_test_final(control_data, treatment_data, metric_name):
"""Comprehensive A/B test analysis"""
# Descriptive stats
control_mean = control_data.mean()
treatment_mean = treatment_data.mean()
control_std = control_data.std()
treatment_std = treatment_data.std()
# Effect size (Cohen's d)
pooled_std = np.sqrt(((len(control_data)-1)*control_std**2 + (len(treatment_data)-1)*treatment_std**2) / (len(control_data) + len(treatment_data) - 2))
cohens_d = (treatment_mean - control_mean) / pooled_std
# Statistical test
t_stat, p_value = ttest_ind(control_data, treatment_data)
# Confidence interval
from scipy.stats import t
df = len(control_data) + len(treatment_data) - 2
se = pooled_std * np.sqrt(1/len(control_data) + 1/len(treatment_data))
t_crit = t.ppf(0.975, df)
ci_lower = (treatment_mean - control_mean) - t_crit * se
ci_upper = (treatment_mean - control_mean) + t_crit * se
# Report
print(f"\n=== A/B Test Results: {metric_name} ===")
print(f"Control: {control_mean:.4f} ± {control_std:.4f}")
print(f"Treatment: {treatment_mean:.4f} ± {treatment_std:.4f}")
print(f"Difference: {treatment_mean - control_mean:.4f}")
print(f"\nEffect size (Cohen's d): {cohens_d:.3f}")
print(f" {'negligible' if abs(cohens_d) < 0.2 else 'small' if abs(cohens_d) < 0.5 else 'medium' if abs(cohens_d) < 0.8 else 'large'}")
print(f"\nStatistical Test:")
print(f" t-statistic: {t_stat:.3f}")
print(f" p-value: {p_value:.4f}")
print(f" 95% CI: [{ci_lower:.4f}, {ci_upper:.4f}]")
print(f" Result: {'✓ SIGNIFICANT' if p_value < 0.05 else '✗ NOT SIGNIFICANT'}")
return {
'control_mean': control_mean,
'treatment_mean': treatment_mean,
'p_value': p_value,
'cohens_d': cohens_d,
'ci': (ci_lower, ci_upper)
}
Важные принципы, которые я соблюдаю
- Не смотрю на результаты до запланированного дня (избегаем p-hacking)
- Определяю размер выборки ДО теста (power analysis)
- Анализирую бизнес-метрики, не только статистику (может быть статистически значимо, но бизнесу не выгодно)
- Мониторю побочные эффекты (improvement в одной метрике может привести к degradation в другой)
- Документирую все гипотезы и результаты (для истории и learning)
Типичные ошибки, которых я избегаю
❌ Останавливаю тест на полпути, потому что "результаты хорошие" ❌ Смотрю p-value каждый день и решаю раньше запланированного дня ❌ Запускаю тест без предварительного расчёта размера выборки ❌ Сравниваю метрики, которые не были запланированы (multiple comparisons problem) ❌ Игнорирую отрицательные результаты (если нет эффекта — тоже полезная информация)
My approach to A/B testing is based on solid statistical foundations and best practices from industry leaders (Google, Spotify, Amazon, etc.)