Коротко (BLUF). Тестовое на A/B почти всегда сводится к одному: даны два варианта с числом конверсий и числом показов — реши, катить ли изменение в прод. Правильный ответ — не «p-value < 0.05, релизим», а последовательность: посчитать конверсии, применить z-тест для двух пропорций, построить доверительный интервал для разницы, отдельно проверить практическую значимость (окупает ли эффект затраты) и явно написать вывод с оговорками. Ниже — полное решение реального по формату кейса с кодом, разбор того, как это подать на собеседовании, и типичные ошибки, на которых валятся кандидаты.
Формат задания я взял из открытых сборников тестовых для аналитиков (например, nodatanogrowth.com/test-tasks — там 189+ реальных заданий Авито, Сбера, DatsTeam и других). Числа в разборе — приближённый к реальным кейсам пример, где важна не сама история, а метод: он одинаков для баннера, кнопки, письма и лендинга.
Задание (как оно выглядит в реальности)
Формулировка из тестового обычно короткая:
Продуктовая команда протестировала новый баннер на главной. Группа A видела старый баннер, группа B — новый. Цель баннера — клик по кнопке «Оформить подписку». Данные за 2 недели теста:
| Группа | Показы (пользователи) | Клики по CTA | Конверсия |
|---|---|---|---|
| A (контроль) | 10 000 | 430 | ? |
| B (новый баннер) | 10 000 | 510 | ? |
Вопрос: какой баннер лучше и стоит ли раскатывать новый на всех пользователей? Приложите расчёты и обоснуйте вывод.
Ровно такой формат — «вариант А: X кликов из N, вариант Б: Y из M, выбери лучший» — встречается в тестовых Авито, Ozon, банков. Иногда вместо баннера письмо или экран онбординга, иногда дают csv на 200 тысяч строк, но математика внутри одна.
Шаг 1. Считаем конверсии и формулируем гипотезы
Конверсия — доля кликнувших от увидевших:
$$p_A = \frac{430}{10000} = 0.0430 = 4.30\%$$
$$p_B = \frac{510}{10000} = 0.0510 = 5.10\%$$
Абсолютная разница — $5.10\% - 4.30\% = 0.80$ процентных пункта. Относительный прирост (lift) — $\frac{0.0510 - 0.0430}{0.0430} = 18.6\%$. Это цифра, которую любит бизнес: «новый баннер поднимает конверсию в клик на 18–19%».
Но 18.6% сами по себе не значат ничего. Разница могла возникнуть случайно — просто в группу B попали пользователи чуть активнее. Задача статистики — отделить сигнал от шума. Формулируем гипотезы:
- $H_0$ (нулевая): конверсии равны, $p_A = p_B$. Разница — случайность.
- $H_1$ (альтернативная): конверсии различаются, $p_A \neq p_B$.
Тест двусторонний: мы допускаем, что новый баннер может оказаться и хуже. Уровень значимости берём стандартный $\alpha = 0.05$.
Шаг 2. Z-тест для двух пропорций — формула
Метрика бинарная (кликнул / не кликнул), выборки большие — значит z-тест для пропорций. Сначала считаем объединённую (pooled) конверсию — как если бы обе группы были одной:
$$\hat{p} = \frac{x_A + x_B}{n_A + n_B} = \frac{430 + 510}{10000 + 10000} = \frac{940}{20000} = 0.0470$$
Стандартная ошибка разницы под нулевой гипотезой:
$$SE = \sqrt{\hat{p}(1-\hat{p})\left(\frac{1}{n_A} + \frac{1}{n_B}\right)} = \sqrt{0.047 \cdot 0.953 \cdot \left(\frac{1}{10000} + \frac{1}{10000}\right)} = 0.002993$$
Z-статистика — насколько наблюдаемая разница далека от нуля в единицах стандартной ошибки:
$$z = \frac{p_B - p_A}{SE} = \frac{0.0510 - 0.0430}{0.002993} = 2.67$$
Для двустороннего теста при $\alpha = 0.05$ критическое значение — $z_{крит} = 1.96$. Наше $z = 2.67 > 1.96$, значит p-value < 0.05.
Точное p-value для $z = 2.67$ — примерно 0.0075. Это вероятность увидеть такую (или большую) разницу, если на самом деле баннеры одинаковы. 0.75% — маловероятно, отвергаем $H_0$.
Шаг 3. Тот же расчёт кодом
На собеседовании ценят, что вы не считаете руками, а пишете воспроизводимый код. Вот полное решение:
import numpy as np
from statsmodels.stats.proportion import proportions_ztest, proportion_confint
# Данные теста
clicks = np.array([430, 510]) # A, B
views = np.array([10000, 10000])
p_a, p_b = clicks / views
print(f"Конверсия A: {p_a:.4f} ({p_a*100:.2f}%)")
print(f"Конверсия B: {p_b:.4f} ({p_b*100:.2f}%)")
print(f"Относительный lift: {(p_b - p_a) / p_a * 100:.1f}%")
# Z-тест для двух пропорций (two-sided, pooled)
z_stat, p_value = proportions_ztest(count=clicks, nobs=views)
print(f"z = {z_stat:.4f}, p-value = {p_value:.4f}")
Вывод:
Конверсия A: 0.0430 (4.30%)
Конверсия B: 0.0510 (5.10%)
Относительный lift: 18.6%
z = 2.6729, p-value = 0.0075
statsmodels по умолчанию использует pooled-оценку — ровно ту формулу, что мы вывели руками. Если под рукой только scipy, можно собрать z-тест вручную или взять scipy.stats.chi2_contingency на таблице 2×2 — для двусторонней проверки двух пропорций хи-квадрат даёт эквивалентный результат ($\chi^2 = z^2$). Потренироваться на подобных задачах с автопроверкой можно в /python-sandbox — там 538 задач, включая A/B и scipy.
Шаг 4. Доверительный интервал — вот что действительно убеждает
P-value отвечает «различие есть?», но не отвечает «насколько большое?». Для этого — доверительный интервал разницы конверсий. Здесь считаем стандартную ошибку без объединения (по каждой группе своя):
$$SE_{ДИ} = \sqrt{\frac{p_A(1-p_A)}{n_A} + \frac{p_B(1-p_B)}{n_B}}$$
95%-й интервал для разницы $p_B - p_A$:
$$(p_B - p_A) \pm 1.96 \cdot SE_{ДИ} = 0.008 \pm 1.96 \cdot 0.003$$
Получаем [0.21%, 1.39%] в абсолютных процентных пунктах.
# ДИ для разницы пропорций
se_diff = np.sqrt(p_a*(1-p_a)/views[0] + p_b*(1-p_b)/views[1])
diff = p_b - p_a
lo, hi = diff - 1.96*se_diff, diff + 1.96*se_diff
print(f"95% ДИ разницы: [{lo*100:.2f}%, {hi*100:.2f}%]")
# 95% ДИ разницы: [0.21%, 1.39%]
Ключевая проверка: интервал не пересекает ноль. Оба конца положительные — значит с 95% уверенностью новый баннер лучше минимум на 0.21 п.п. Это тот же вывод, что и p-value < 0.05, но в форме, которую понимает продакт: «прирост от +0.2 до +1.4 процентных пункта конверсии».
Именно доверительный интервал стоит выносить в вывод. Если написать «p = 0.0075», продакт спросит «и что?». Если написать «конверсия вырастет на 0.2–1.4 п.п., базовый сценарий +0.8 п.п.» — разговор сразу переходит к деньгам.
Шаг 5. Практическая значимость — обязательный второй фильтр
Статистическая значимость говорит «эффект реален». Практическая — «эффект стоит того, чтобы его катить». Это разные вещи, и путать их — классический провал.
Прикинем деньги. Пусть на баннер в месяц приходит 200 000 показов, а клик по CTA в среднем даёт 900 ₽ выручки (через воронку до подписки). Прирост 0.8 п.п. — это дополнительно $200000 \cdot 0.008 = 1600$ кликов в месяц, или ~1.44 млн ₽. Даже по нижней границе интервала (+0.21 п.п.) — ~378 тысяч ₽/мес. Разработка и поддержка нового баннера явно дешевле. Практическая значимость есть.
Здесь же полезно вспомнить про MDE — минимальный эффект, который тест вообще способен поймать. Размер выборки для z-теста считают так:
$$n = \frac{2(z_{\alpha/2} + z_{\beta})^2 \cdot p(1-p)}{MDE^2}$$
При базовой конверсии 4.3%, $\alpha = 0.05$ ($z_{\alpha/2} = 1.96$) и мощности 80% ($z_\beta = 0.84$), чтобы надёжно ловить эффект в 0.8 п.п., нужно ~10 100 пользователей на группу. У нас 10 000 — на грани, но эффект оказался достаточно крупным, чтобы «пробить» его. Для эффекта 0.5 п.п. понадобилось бы уже ~25 800 на группу. Это хорошо упомянуть: показывает, что вы думаете о дизайне теста, а не только о постобработке.
Шаг 6. Вывод — «релизить или нет»
Собираем всё в короткий, честный вывод — именно такой ждут в тестовом:
Новый баннер (B) повышает конверсию в клик с 4.30% до 5.10% — относительный прирост 18.6%. Различие статистически значимо: z = 2.67, p = 0.0075 < 0.05, нулевая гипотеза о равенстве конверсий отвергается. 95% доверительный интервал разницы — [0.21%, 1.39%] п.п., ноль не включён. Практическая значимость положительная: при текущем трафике прирост даёт ориентировочно 1.4 млн ₽/мес дополнительной выручки даже с учётом нижней границы интервала.
>
Рекомендация: раскатывать новый баннер на 100% с последующим мониторингом. Оговорки: тест шёл 2 недели — стоит убедиться, что не попали на аномальную неделю (акция, праздник); проверить эффект в разрезе платформ (web/mobile) и новых/старых пользователей на случай гетерогенности; следить за метриками ниже по воронке — рост кликов не должен «сломать» итоговую конверсию в оплату.
Эти оговорки — не вода. Они показывают зрелость: вы знаете, что «стат-значимый рост клика» может маскировать проблему дальше по воронке.
Контрпример: когда «красивый lift» не значим
Чтобы почувствовать разницу, замените числа: A = 340/8000 (4.25%), B = 391/8000 (4.89%). Относительный прирост 15% — на вид тоже победа. Но:
z_stat, p_value = proportions_ztest([340, 391], [8000, 8000])
print(f"z = {z_stat:.3f}, p = {p_value:.4f}")
# z = 1.931, p = 0.0535
$z = 1.93$, $p = 0.0535 > 0.05$. Доверительный интервал разницы — [−0.01%, 1.29%], он захватывает ноль. Вывод противоположный: несмотря на симпатичный lift 15%, различие статистически не подтверждено. Здесь правильный ответ — «не релизить по текущим данным, продлить тест до нужного размера выборки». Кандидаты, которые смотрят только на процент разницы и не считают тест, на таком кейсе релизят пустышку.
Как подать это на собеседовании
Даже с идеальными числами можно провалиться на защите. Что помогает:
Проговаривайте гипотезы вслух. Начните не с кода, а с «нулевая — конверсии равны, альтернативная — различаются, беру двусторонний тест и alpha 0.05». Это сразу отделяет вас от тех, кто заучил proportions_ztest.
Объясните выбор критерия. «Метрика бинарная, выборки большие — z-тест для пропорций. Если бы считали среднюю выручку на пользователя, взял бы t-тест или бутстрап, потому что распределение денег скошенное». Такая фраза показывает, что вы понимаете, а не подставляете.
Ведите к деньгам, а не к p-value. Финальный слайд/абзац — про бизнес: сколько кликов и рублей приносит эффект, окупается ли. P-value — техническая деталь по пути.
Назовите риски сами. Peeking (подглядывание в результаты до конца теста), сезонность, эффект новизны, несбалансированные группы, множественные сравнения. Достаточно перечислить 2–3 релевантных — интервьюер поймёт, что вы работали с реальными тестами.
Не бойтесь сказать «данных мало». В контрпримере выше честный ответ «продлить тест» ценнее уверенного «релизим». Гейм — не в том, чтобы всегда быть за релиз.
Типичные ошибки кандидатов
- Смотрят на разницу процентов без теста. «5.1% больше 4.3%, значит B лучше» — самый частый провал. Разница может быть шумом.
- Путают статистическую и практическую значимость. Значимый эффект в 0.02 п.п. на объёме в миллионы пользователей математически реален, но катить ради него редизайн бессмысленно. И наоборот — крупный эффект на маленькой выборке может быть незначим.
- Считают конверсию по кликам, а не по пользователям. Если один юзер кликнул трижды, а вы делите клики на пользователей — получите конверсию больше 100% и сломанный тест. Нужна доля *уникальных* кликнувших.
- Берут односторонний тест «потому что мы верим, что B лучше». Это занижает p-value искусственно. По умолчанию — двусторонний, если в задании прямо не сказано иное.
- Игнорируют размер выборки и мощность. Не проверив MDE, легко «не увидеть» реальный эффект (ошибка II рода) и зарубить хорошую гипотезу.
- Peeking. Останавливают тест, как только p-value впервые упал ниже 0.05. Это раздувает ложноположительные до 20–30%. Размер выборки и срок фиксируются до старта.
- Не проверяют равномерность сплита. Если в A попало 10 000, а в B — 7 000 при заявленном 50/50, значит сплит-система сломана и тесту верить нельзя (A/A-тест ловит это заранее).
- Забывают про метрики ниже по воронке. Рост клика по CTA бесполезен, если итоговая оплата не выросла или упала.
Разобрать больше форматов тестовых заданий и потренировать SQL-часть (в тестовых A/B почти всегда идёт в паре с выгрузкой данных) можно в /tasks и /sql-sandbox — 545 задач с автопроверкой. Продуктовую логику экспериментов — в разделе /cases.
Частые вопросы
Какой критерий выбрать для A/B-теста на конверсию?
Для бинарной метрики (кликнул / не кликнул, купил / не купил) и больших выборок — z-тест для двух пропорций. Он эквивалентен хи-квадрату на таблице 2×2 ($\chi^2 = z^2$). Для непрерывных метрик со скошенным распределением (выручка на пользователя, время на сайте) z-тест не подходит — там t-тест, а лучше бутстрап или тест Манна–Уитни. Главное на собеседовании — не просто назвать критерий, а объяснить, почему он подходит к типу метрики.
Что важнее — p-value или доверительный интервал?
Доверительный интервал информативнее. P-value отвечает только «различие есть или нет» одним числом, а интервал показывает и факт значимости (не пересекает ли ноль), и величину эффекта, и его неопределённость. В выводе для бизнеса всегда приводите интервал: «прирост конверсии от +0.2 до +1.4 п.п.» звучит убедительнее, чем «p = 0.0075». Опытные аналитики докладывают результат именно интервалом, а p-value оставляют как техническую сноску.
Чем статистическая значимость отличается от практической?
Статистическая значимость (p < 0.05, интервал не задевает ноль) говорит, что эффект реален и не случаен. Практическая значимость — что эффект достаточно велик, чтобы окупить внедрение. На больших выборках можно поймать статистически значимый прирост в 0.01 п.п., который не стоит ни рубля разработки. И наоборот: крупный эффект на маленькой выборке может быть незначим. В тестовом нужно проверить обе значимости и перевести эффект в деньги или целевую метрику.
Тест дал p = 0.06 — это провал эксперимента?
Нет, это «недостаточно данных для вывода», а не «эффекта нет». Правильная реакция — посмотреть доверительный интервал (насколько он широк и где расположен) и оценить, добрали ли вы нужный размер выборки под MDE. Часто ответ — продлить тест до запланированного объёма. Категорически нельзя останавливаться ровно в момент, когда p впервые опустился ниже 0.05: это peeking, он раздувает долю ложноположительных результатов. Срок и размер выборки фиксируются до старта.
Метод из этого разбора — конверсии → z-тест → доверительный интервал → практическая значимость → вывод с оговорками — работает для любого A/B-тестового: баннер, письмо, кнопка, лендинг, экран онбординга. Отработайте его на 2–3 примерах с разными числами (включая заведомо незначимый), и на реальном собеседовании останется только подставить данные. Больше разборов вопросов с собеседований — в /interviews и на странице /sobesedovanie-analitika-dannyh.