Собесов

A/B-тест новой платёжной системы: метрики, тест и отчёт

Статистика и теорверA/B-тестыСложнаяMiddle

Условие

Платёжный сервис тестирует новую платёжную систему. Воронка пользователя:

  1. open-list — открыл список платёжных систем.
  2. open-payment — выбрал платёжку.
  3. create-invoice — создал заявку на оплату.

Каждое следующее событие невозможно без предыдущего, но иногда события теряются в логах. Поведение может различаться по платформе (ui_version) и стране (user_country).

A/B-тест: группе A новая платёжка не показывалась, группе B — показывалась. Колонки лога: internal_time, milliseconds, action, user_id, user_country, ui_version, experiment_group.

Задача:

  1. Выбрать метрику/метрики для оценки эксперимента.
  2. Выбрать статистический метод.
  3. Написать код на Python.
  4. Написать отчёт для продакт-менеджера, не погружённого в статистику.

Решение

Шаг 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 стр)

Структура:

  1. TL;DR — одно предложение с выводом и числом. Пример: «Новая платёжная система повысила end-to-end-конверсию с 12.4% до 13.7% (+1.3 п.п., 95% ДИ [+0.8; +1.8]). Эффект статзначим (p < 0.001). Рекомендуем раскатать на всех.»
  2. Что мы измеряли — метрика и почему она ключевая.
  3. Размер групп и санити — равные ли группы, нет ли перекоса по странам/платформам.
  4. Результат — таблица: CR в A, CR в B, разница в п.п., доверительный интервал.
  5. Сегменты — есть ли разнонаправленные эффекты (например, мобайл +2 п.п., десктоп +0.2 п.п.).
  6. Гарды — доля пользователей с провалом в воронке не выросла в B.
  7. Риски и ограничения — что данные могут «терять» события, поэтому смотрим на максимально достигнутый шаг, а не на каждый.
  8. Рекомендация — раскатать / не раскатать / провести follow-up.

Без жаргона про z-test/p-value в основном тексте — только в техническом приложении.

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

  1. Единица анализа. Если считать на сессиях/событиях, нарушается iid и тест переоценивает значимость. Делайте per-user.
  2. Потерянные события. Условие явно говорит, что события могут пропадать. Поэтому считаем «достиг ли пользователь шага N», а не «прошёл шаг N → шаг N+1» — иначе пропуски испортят воронку.
  3. Знаменатель воронки. «End-to-end CR» — от тех, кто открыл лист (open-list). Если брать всех пользователей системы — попадут люди, не дошедшие до платежей.
  4. Дисбаланс групп. Если в A 60%, в B 40% — нужно понять, по какому правилу делили. Sanity-test обязателен.
  5. Симпсон. На уровне всей выборки эффект может быть положительным, а внутри страны — отрицательным. Смотреть сегменты.
  6. Multiple testing. Десятки сегментов → ложные срабатывания. Holm/BH.
  7. Длина теста. Слишком короткий — в недельной сезонности «новости/выходные» дают сдвиг. Минимум полная неделя.
  8. Новинка-эффект. В первые дни B показывает «вау-эффект», который потом утихнет. Смотрите динамику CR по дням.
  9. «p < 0.05 → раскатываем». Без размера эффекта и бизнес-смысла p-value ничего не значит. Всегда — CI, а не только p.
  10. Cross-platform юзеры. Один user_id с iOS и desktop — куда отнести? По первой платформе.

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

  1. Метрика: end-to-end CR (users with create-invoice) / (users with open-list) per user. Дополнительно — пошаговые CR + гарды (доля проваленных воронок).
  2. Метод: z-тест для двух пропорций + 95%-ДИ на разницу; chi-square как альтернатива; для срезов — поправка Holm/BH.
  3. Код: pandas → группировка по user_id с max_step, proportions_ztest, confint_proportions_2indep, перебор сегментов.
  4. Отчёт: TL;DR с одним числом и интервалом → метрика → санити → результат + сегменты → гарды → риски → рекомендация. Без статжаргона в теле, технические детали — в приложении.

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

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

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