Проводил ли A/B тестирование и как анализировал результаты

Ответ

Да, проводил A/B тестирование для оценки эффективности изменений в продукте. Вот мой практический опыт:

1. Проект: Оптимизация конверсии на лендинге

Задача: Сравнить две версии кнопки CTA (Call-to-Action)

  • Контрольная группа (A): Зеленая кнопка "Купить сейчас"
  • Тестовая группа (B): Оранжевая кнопка "Получить скидку"

Метрики:

  • Основная: Конверсия (клики/показы)
  • Вторичные: Время на странице, bounce rate

2. Статистический анализ в Python

import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.stats.api as sms
import matplotlib.pyplot as plt

# Генерация данных (пример)
np.random.seed(42)
control_size = 5000
treatment_size = 5000

# Контрольная группа (базовая конверсия 10%)
control_conversions = np.random.binomial(1, 0.10, control_size)
# Тестовая группа (улучшенная конверсия 12%)
treatment_conversions = np.random.binomial(1, 0.12, treatment_size)

# Базовые статистики
control_rate = control_conversions.mean()
treatment_rate = treatment_conversions.mean()
relative_improvement = (treatment_rate - control_rate) / control_rate

print(f"Control conversion rate: {control_rate:.3%}")
print(f"Treatment conversion rate: {treatment_rate:.3%}")
print(f"Relative improvement: {relative_improvement:.2%}")

3. Проверка статистической значимости

# Z-test для пропорций
from statsmodels.stats.proportion import proportions_ztest

count = np.array([control_conversions.sum(), treatment_conversions.sum()])
nobs = np.array([control_size, treatment_size])

z_stat, p_value = proportions_ztest(count, nobs, alternative='smaller')
print(f"Z-statistic: {z_stat:.3f}")
print(f"P-value: {p_value:.5f}")

# Доверительный интервал разницы
from statsmodels.stats.proportion import confint_proportions_2indep

ci_low, ci_high = confint_proportions_2indep(
    count1=treatment_conversions.sum(), nobs1=treatment_size,
    count2=control_conversions.sum(), nobs2=control_size,
    method='wald',
    compare='diff',
    alpha=0.05
)
print(f"95% CI for difference: [{ci_low:.4f}, {ci_high:.4f}]")

4. Расчет мощности теста и размера выборки

# Расчет необходимого размера выборки до запуска теста
effect_size = sms.proportion_effectsize(0.10, 0.12)  # Минимальный детектируемый эффект
power_analysis = sms.NormalIndPower()

required_n = power_analysis.solve_power(
    effect_size=effect_size,
    power=0.8,           # 80% вероятность обнаружить эффект
    alpha=0.05,         # Уровень значимости 5%
    ratio=1.0           # Равные группы
)

print(f"Required sample size per group: {int(required_n)}")

5. Анализ воронки и сегментация

# Анализ по сегментам пользователей
data = pd.DataFrame({
    'group': ['control'] * control_size + ['treatment'] * treatment_size,
    'converted': np.concatenate([control_conversions, treatment_conversions]),
    'segment': np.random.choice(['new', 'returning'], 
                               control_size + treatment_size,
                               p=[0.6, 0.4])
})

# Конверсия по сегментам
segment_analysis = data.groupby(['group', 'segment'])['converted'] 
                      .agg(['mean', 'count', 'sum'])
print(segment_analysis)

# Статистическая значимость по сегментам
for segment in ['new', 'returning']:
    segment_data = data[data['segment'] == segment]
    count = segment_data.groupby('group')['converted'].sum().values
    nobs = segment_data.groupby('group').size().values

    z_stat, p_value = proportions_ztest(count, nobs)
    print(f"Segment {segment}: p-value = {p_value:.4f}")

6. Визуализация результатов

import seaborn as sns

# Распределение конверсий по дням (для проверки временных эффектов)
daily_data = pd.DataFrame({
    'day': np.tile(range(14), control_size // 14 + treatment_size // 14)[:control_size + treatment_size],
    'group': data['group'],
    'converted': data['converted']
})

daily_conversion = daily_data.groupby(['day', 'group'])['converted'].mean().reset_index()

plt.figure(figsize=(12, 6))
sns.lineplot(data=daily_conversion, x='day', y='converted', hue='group')
plt.title('Daily Conversion Rates')
plt.xlabel('Day')
plt.ylabel('Conversion Rate')
plt.axhline(y=control_rate, color='blue', linestyle='--', alpha=0.5)
plt.axhline(y=treatment_rate, color='orange', linestyle='--', alpha=0.5)
plt.show()

7. Практические рекомендации из опыта:

  • Перед запуском:

    • Рассчитываю минимальный детектируемый эффект (MDE)
    • Определяю длительность теста с учетом сезонности
    • Настраиваю систему сбора данных и event tracking
  • Во время теста:

    • Мониторю равномерность распределения трафика
    • Проверяю отсутствие технических проблем
    • Отслеживаю вторичные метрики на предмет негативных эффектов
  • После теста:

    • Анализирую результаты по сегментам
    • Проверяю устойчивость результатов во времени
    • Документирую выводы и рекомендации для product-менеджеров

Пример вывода из реального проекта: "Тест показал статистически значимое увеличение конверсии на 18% (p-value = 0.012). Эффект особенно выражен среди новых пользователей (+25%). Рекомендую внедрить изменение и мониторить долгосрочные метрики удержания."

Ответ 18+ 🔞

А, ну это про A/B тестирование, ебать мои старые костыли! Ну, типа, проверял, какая хуйня лучше работает в продукте. Вот, реально, как я это делал, чувак.

1. Проект: Ковырялись с кнопкой на сайте

Задача: Выяснить, на какую кнопку народ больше ведётся.

  • Старая версия (A): Кнопка зелёная, «Купить сейчас». Скукота.
  • Новая версия (B): Кнопка оранжевая, «Получить скидку». А вот это уже интереснее, да?

Что смотрели:

  • Главное: Конверсия — сколько лохов кликнуло из тех, кто увидел.
  • Попутно: Сколько времени торчали на странице, не свалили ли сразу.

2. Статистика на Python (чтобы не пиздеть просто так)

import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.stats.api as sms
import matplotlib.pyplot as plt

# Нагенерили данных для примера
np.random.seed(42)
control_size = 5000
treatment_size = 5000

# Контрольная группа (обычная конверсия 10%)
control_conversions = np.random.binomial(1, 0.10, control_size)
# Тестовая группа (предполагаем, что лучше — 12%)
treatment_conversions = np.random.binomial(1, 0.12, treatment_size)

# Считаем базовые цифры
control_rate = control_conversions.mean()
treatment_rate = treatment_conversions.mean()
relative_improvement = (treatment_rate - control_rate) / control_rate

print(f"Конверсия в контроле: {control_rate:.3%}")
print(f"Конверсия в тесте: {treatment_rate:.3%}")
print(f"Относительный рост: {relative_improvement:.2%}")

3. А теперь, ёпта, проверяем, не случайно ли это всё

# Z-тест для пропорций, чтобы понять, не обманываем ли мы себя
from statsmodels.stats.proportion import proportions_ztest

count = np.array([control_conversions.sum(), treatment_conversions.sum()])
nobs = np.array([control_size, treatment_size])

z_stat, p_value = proportions_ztest(count, nobs, alternative='smaller')
print(f"Z-статистика: {z_stat:.3f}")
print(f"P-value: {p_value:.5f}")  # Вот эта цифра главная! Если меньше 0.05 — охуенно.

# Доверительный интервал для разницы
from statsmodels.stats.proportion import confint_proportions_2indep

ci_low, ci_high = confint_proportions_2indep(
    count1=treatment_conversions.sum(), nobs1=treatment_size,
    count2=control_conversions.sum(), nobs2=control_size,
    method='wald',
    compare='diff',
    alpha=0.05
)
print(f"95% доверительный интервал для разницы: [{ci_low:.4f}, {ci_high:.4f}]")

4. Сила теста и сколько нам нужно народу

# Считаем, сколько пользователей надо, чтобы не облажаться
effect_size = sms.proportion_effectsize(0.10, 0.12)  # Минимальный эффект, который хотим поймать
power_analysis = sms.NormalIndPower()

required_n = power_analysis.solve_power(
    effect_size=effect_size,
    power=0.8,           # 80% шанс, что заметим эффект, если он есть
    alpha=0.05,         # Уровень значимости 5%
    ratio=1.0           # Группы равные
)

print(f"Нужно человек в каждой группе: {int(required_n)}")  # А то запустишь тест на 10 человеках — нихуя не узнаешь.

5. Копаем глубже: а для кого это работает?

# Смотрим по сегментам — может, только для новичков прокатило?
data = pd.DataFrame({
    'group': ['control'] * control_size + ['treatment'] * treatment_size,
    'converted': np.concatenate([control_conversions, treatment_conversions]),
    'segment': np.random.choice(['new', 'returning'],
                               control_size + treatment_size,
                               p=[0.6, 0.4])  # 60% новых, 40% возвращающихся
})

# Конверсия по сегментам
segment_analysis = data.groupby(['group', 'segment'])['converted'] 
                      .agg(['mean', 'count', 'sum'])
print(segment_analysis)

# Проверяем значимость для каждого сегмента отдельно
for segment in ['new', 'returning']:
    segment_data = data[data['segment'] == segment]
    count = segment_data.groupby('group')['converted'].sum().values
    nobs = segment_data.groupby('group').size().values

    z_stat, p_value = proportions_ztest(count, nobs)
    print(f"Сегмент {segment}: p-value = {p_value:.4f}")  # Вот тут может оказаться, что для старичков всё похуй.

6. Картинки, потому что начальство любит картинки

import seaborn as sns

# Смотрим, как конверсия плавала по дням (вдруг в понедельник все злые?)
daily_data = pd.DataFrame({
    'day': np.tile(range(14), control_size // 14 + treatment_size // 14)[:control_size + treatment_size],
    'group': data['group'],
    'converted': data['converted']
})

daily_conversion = daily_data.groupby(['day', 'group'])['converted'].mean().reset_index()

plt.figure(figsize=(12, 6))
sns.lineplot(data=daily_conversion, x='day', y='converted', hue='group')
plt.title('Конверсия по дням')
plt.xlabel('День')
plt.ylabel('Конверсия')
plt.axhline(y=control_rate, color='blue', linestyle='--', alpha=0.5)
plt.axhline(y=treatment_rate, color='orange', linestyle='--', alpha=0.5)
plt.show()

7. Выводы, которые у меня в голове после таких тестов, блядь:

  • Перед тем как запускать:

    • Чётко решаю, какой минимальный прирост мне нужен, чтобы не тратить время на хуйню.
    • Считаю, сколько дней гнать тест, учитывая, что в выходные всё может ебнуться.
    • Убеждаюсь, что данные собираются, а то потом волнение ебать — а логи пустые.
  • Пока тест идёт:

    • Слежу, чтобы трафик делился ровно, а не как попало.
    • Ищу косяки — вдруг кнопка у половины не грузится?
    • Гляжу на другие метрики — а то конверсия вырастет, а отказы взлетят до небес. Ёперный театр!
  • Когда всё закончилось:

    • Ломлюсь в сегменты. Может, новый дизайн только для «ядерных вшей» сработал?
    • Смотрю, держался ли эффект со временем или это был краткосрочный пиздец.
    • Пишу отчет продуктологам так, чтобы они не ебали мозг. Чётко: «Работает/не работает, вот цифры, вот рекомендация, идите нахуй с вопросами».

Итог из реального случая, чувак: «Тест показал, что оранжевая кнопка дала статистически значимый рост конверсии на 18% (p-value = 0.012). Особенно охуенно она зашла новым пользователям — там аж +25%. Давайте вкатываем эту хуйню на всех, но смотрим, не начнут ли старички возмущаться. Сам от таких цифр иногда охуеваю, но наука, блядь, не врёт».