← Назад к вопросам

Проводил ли A/B-тесты

1.3 Junior🔥 211 комментариев
#A/B-тестирование#Опыт работы и проекты#Статистика и теория вероятностей

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Опыт проведения 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 дней:

МетрикаControlTreatmentDifferencep-value
Конверсия в покупку15.2%16.8%+1.6pp0.003 ✓
Средняя выручка за покупку$45.20$44.90-0.7%0.42 ✗
Среднее время сессии3m 45s4m 20s+14%<0.001 ✓
Bounce rate42%39%-3pp0.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;

Результаты:

VariantOpen RateClick RateRevenue/EmailRank
A (9 AM)24.3%3.2%$0.422
B (12 PM)25.8%3.5%$0.481 ✓
C (3 PM)22.1%2.9%$0.384
D (6 PM)23.5%3.1%$0.413
E (9 PM)20.2%2.6%$0.345

Статистический анализ:

# 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)
    }

Важные принципы, которые я соблюдаю

  1. Не смотрю на результаты до запланированного дня (избегаем p-hacking)
  2. Определяю размер выборки ДО теста (power analysis)
  3. Анализирую бизнес-метрики, не только статистику (может быть статистически значимо, но бизнесу не выгодно)
  4. Мониторю побочные эффекты (improvement в одной метрике может привести к degradation в другой)
  5. Документирую все гипотезы и результаты (для истории и 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.)

Проводил ли A/B-тесты | PrepBro