Условие
Платёжный сервис тестирует новую платёжную систему. Воронка пользователя:
open-list— открыл список платёжных систем.open-payment— выбрал платёжку.create-invoice— создал заявку на оплату.
Каждое следующее событие невозможно без предыдущего, но иногда события теряются в логах. Поведение может различаться по платформе (ui_version) и стране (user_country).
A/B-тест: группе A новая платёжка не показывалась, группе B — показывалась. Колонки лога: internal_time, milliseconds, action, user_id, user_country, ui_version, experiment_group.
Задача:
- Выбрать метрику/метрики для оценки эксперимента.
- Выбрать статистический метод.
- Написать код на Python.
- Написать отчёт для продакт-менеджера, не погружённого в статистику.
Решение
Шаг 1. Метрики
Цель «новой платёжки» — повысить долю пользователей, доходящих до создания заявки. Поэтому ключевая метрика:
CR-end-to-end = users with create-invoice / users with open-list (per user, не per session).
Дополнительные:
- CR(open-list → open-payment) — доходимость до выбора платёжки.
- CR(open-payment → create-invoice) — конверсия выбора в заявку.
- Среднее время от open-list до create-invoice (если метрика бизнесово важна).
- Доля переходов на ту самую новую платёжку в группе B (диагностика, не основная).
Гарды:
- Доля пользователей, у которых вообще нет последующих событий (свидетельство сбоя).
- Доли пропущенных событий между шагами (не растёт ли в B).
Шаг 2. Дизайн и метод
- Единица анализа — пользователь (
user_id), а не сессия / событие. Иначе нарушение iid (один user генерирует много строк). - H0: CR end-to-end в A = в B.
- Тест: для долей — z-test для двух пропорций (или chi-square на таблице 2×2). Для среднего времени — t-test/Welch или bootstrap разности средних.
- Доверительный интервал на разницу пропорций обязателен — продакту это понятнее p-value.
- Поправка на множественные сравнения — если смотрим несколько метрик / сегментов: Holm/Bonferroni или BH-FDR.
- A/A-санити — равны ли доли пользователей в A и B по странам/платформам? Если есть смещение — стратифицировать.
Шаг 3. Код
import pandas as pd
import numpy as np
from statsmodels.stats.proportion import proportions_ztest, confint_proportions_2indep
events = pd.read_csv("ab_log.csv", parse_dates=["internal_time"])
# 1. Сворачиваем на пользователя: дошёл ли до каждого шага
user_df = (events
.assign(step=lambda d: d["action"].map({
"open-list":1, "open-payment":2, "create-invoice":3
}))
.groupby(["user_id","experiment_group","user_country","ui_version"])
.agg(max_step=("step","max"))
.reset_index())
# Базовая аудитория — те, кто хоть раз открыл список
base = user_df[user_df["max_step"] >= 1].copy()
base["converted"] = (base["max_step"] >= 3).astype(int)
# 2. Sanity: размеры групп и распределение по сегментам
print(base.groupby("experiment_group").size())
print(base.groupby(["experiment_group","ui_version"]).size().unstack(fill_value=0))
# 3. CR end-to-end по группам
agg = base.groupby("experiment_group").agg(
users=("user_id","nunique"),
converted=("converted","sum")
)
agg["cr"] = agg["converted"] / agg["users"]
print(agg)
# 4. z-test и CI на разницу
counts = agg.loc[["A","B"], "converted"].values
nobs = agg.loc[["A","B"], "users"].values
stat, pval = proportions_ztest(counts, nobs)
ci_low, ci_high = confint_proportions_2indep(
counts[1], nobs[1], counts[0], nobs[0], method='wald'
)
print(f"diff CR (B-A) = {agg.loc['B','cr']-agg.loc['A','cr']:.4f}")
print(f"95% CI: [{ci_low:.4f}, {ci_high:.4f}], p={pval:.4f}")
# 5. Срезы по платформе и стране (с поправкой на множественность)
for col in ["ui_version","user_country"]:
for seg, sub in base.groupby(col):
c = sub.groupby("experiment_group")["converted"].agg(["sum","count"])
if {"A","B"} <= set(c.index):
s, p = proportions_ztest(c["sum"].values, c["count"].values)
print(col, seg, c["sum"].values, c["count"].values, f"p={p:.3f}")Шаг 4. Отчёт для продакта (1 стр)
Структура:
- TL;DR — одно предложение с выводом и числом. Пример: «Новая платёжная система повысила end-to-end-конверсию с 12.4% до 13.7% (+1.3 п.п., 95% ДИ [+0.8; +1.8]). Эффект статзначим (p < 0.001). Рекомендуем раскатать на всех.»
- Что мы измеряли — метрика и почему она ключевая.
- Размер групп и санити — равные ли группы, нет ли перекоса по странам/платформам.
- Результат — таблица: CR в A, CR в B, разница в п.п., доверительный интервал.
- Сегменты — есть ли разнонаправленные эффекты (например, мобайл +2 п.п., десктоп +0.2 п.п.).
- Гарды — доля пользователей с провалом в воронке не выросла в B.
- Риски и ограничения — что данные могут «терять» события, поэтому смотрим на максимально достигнутый шаг, а не на каждый.
- Рекомендация — раскатать / не раскатать / провести follow-up.
Без жаргона про z-test/p-value в основном тексте — только в техническом приложении.
Подводные камни
- Единица анализа. Если считать на сессиях/событиях, нарушается iid и тест переоценивает значимость. Делайте per-user.
- Потерянные события. Условие явно говорит, что события могут пропадать. Поэтому считаем «достиг ли пользователь шага N», а не «прошёл шаг N → шаг N+1» — иначе пропуски испортят воронку.
- Знаменатель воронки. «End-to-end CR» — от тех, кто открыл лист (
open-list). Если брать всех пользователей системы — попадут люди, не дошедшие до платежей. - Дисбаланс групп. Если в A 60%, в B 40% — нужно понять, по какому правилу делили. Sanity-test обязателен.
- Симпсон. На уровне всей выборки эффект может быть положительным, а внутри страны — отрицательным. Смотреть сегменты.
- Multiple testing. Десятки сегментов → ложные срабатывания. Holm/BH.
- Длина теста. Слишком короткий — в недельной сезонности «новости/выходные» дают сдвиг. Минимум полная неделя.
- Новинка-эффект. В первые дни B показывает «вау-эффект», который потом утихнет. Смотрите динамику CR по дням.
- «p < 0.05 → раскатываем». Без размера эффекта и бизнес-смысла p-value ничего не значит. Всегда — CI, а не только p.
- Cross-platform юзеры. Один user_id с iOS и desktop — куда отнести? По первой платформе.
Эталонный ответ
- Метрика: end-to-end CR
(users with create-invoice) / (users with open-list)per user. Дополнительно — пошаговые CR + гарды (доля проваленных воронок). - Метод: z-тест для двух пропорций + 95%-ДИ на разницу; chi-square как альтернатива; для срезов — поправка Holm/BH.
- Код: pandas → группировка по user_id с
max_step,proportions_ztest,confint_proportions_2indep, перебор сегментов. - Отчёт: TL;DR с одним числом и интервалом → метрика → санити → результат + сегменты → гарды → риски → рекомендация. Без статжаргона в теле, технические детали — в приложении.