Собесов

МТС Финтех: A/B-тест баннера на странице заявки на карту

A/B-тестыАнализ A/B и решениеЛёгкаяJunior

Условие

В МТС Банке запустили A/B-тест на странице, где можно оформить заявку на банковскую карту:

  • В варианте A из 2574 клиентов, зашедших на страницу, на баннер нажали 350.
  • В варианте B из 2855 клиентов нажали 375.

Какой вариант баннера предложить продакту и почему?

Решение

Подход

Сравнить две пропорции (CTR на баннер) z-тестом, посчитать CI на разность, и не делать вывод только по точечной оценке.

Реализация

import numpy as np
from statsmodels.stats.proportion import proportions_ztest, confint_proportions_2indep
 
n_a, x_a = 2574, 350
n_b, x_b = 2855, 375
 
p_a = x_a / n_a
p_b = x_b / n_b
print(f"CTR A = {p_a:.4f} ({p_a*100:.2f}%)")
print(f"CTR B = {p_b:.4f} ({p_b*100:.2f}%)")
print(f"abs diff = {p_a - p_b:.4f}, rel diff = {(p_a-p_b)/p_b*100:.2f}%")
 
# Двусторонний z-test
z, p_value = proportions_ztest([x_a, x_b], [n_a, n_b])
print(f"z = {z:.3f}, p-value = {p_value:.4f}")
 
# 95% CI на разность пропорций
lo, hi = confint_proportions_2indep(x_a, n_a, x_b, n_b)
print(f"95% CI(p_A - p_B) = [{lo:.4f}, {hi:.4f}]")

Расчёт вручную

  • p_A = 350/2574 = 0.1360 (13.60%).
  • p_B = 375/2855 = 0.1313 (13.13%).
  • Абсолютная разница: +0.0047 (~0.47 п.п. в пользу A).
  • Pooled p = (350+375)/(2574+2855) = 725/5429 = 0.1335.
  • SE = sqrt(p̂(1-p̂)·(1/n_A + 1/n_B)) = sqrt(0.1335·0.8665·(1/2574 + 1/2855)) ≈ 0.00925.
  • z = 0.0047 / 0.00925 ≈ 0.508.
  • p-value (двусторонний) ≈ 0.611.

Решение

p-value = 0.61 ≫ 0.05разница не значима. CI на разность пропорций пересекает 0 — мы не можем утверждать, что A лучше B.

Что говорить продакту:

  1. Не выкатывать ни один вариант на основании этих данных. Разница (0.47 п.п.) укладывается в шум.
  2. Если продакт хочет «всё равно выбрать» — выбирайте по нестатистическим причинам: дизайн, бренд-гайдлайны, стоимость поддержки. Главное — не подменять «нет статистической разницы» словом «они одинаковые».
  3. Чтобы поймать MDE 0.5 п.п. на baseline 13%, нужно сильно больше выборки:
from statsmodels.stats.power import NormalIndPower
from statsmodels.stats.proportion import proportion_effectsize
es = proportion_effectsize(0.135, 0.130)  # отн. сдвиг 0.5 п.п.
n = NormalIndPower().solve_power(es, alpha=0.05, power=0.8)
print(f"Нужно ~{int(n)} на группу")
# ~70 000 на группу

Сейчас в каждой группе ~2500 — в 30 раз меньше нужного для адекватного MDE.

Дополнительные вопросы

  • Какой целевой метрикой мы хотим оптимизировать — CTR на баннер, заполнение заявки или одобрение карты? CTR — ранняя метрика; решение по downstream важнее.
  • Длительность теста — насколько репрезентативно 5429 человек?
  • Сегменты: новые vs возвращающиеся, мобайл vs веб.

Подводные камни

  1. Точечное сравнение «13.6% > 13.1% — A лучше». Без CI/p-value это решение мелкой подписи. Junior'ы часто срезают этот шаг.
  2. Маленькая выборка. Power < 20% при таком объёме и MDE — тест ничего не выявит.
  3. CTR — proxy. Конверсия в заявку и одобрение важнее. Можно нарастить CTR агрессивным баннером и потерять в конверсии.
  4. Нет sanity-чеков. SRM (соотношение размеров групп: 2574 vs 2855 = 47.4% / 52.6%, отклонение от 50/50 — норма для маленькой выборки, но проверить chi-square стоит).
  5. «p > 0.05» = «нет эффекта». Нет — «недостаточно данных». Корректная фраза: «не отвергаем H0».

Эталонный ответ

Z-test двух пропорций даёт p ≈ 0.61, разница 0.5 п.п. укладывается в шум. Не выкатываем ни один вариант на основании этих данных. Если решение нужно — берём по нестатистическим причинам и закладываем больший размер выборки на следующий тест. Главный навык — не путать «p > 0.05» с «варианты одинаковы».

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти