Собесов

Хабр SQL — конверсия и lift в A/B-тесте

SQLA/B-тестыСредняяMiddle

Условие

Таблица experiment(user_id, variant text, has_purchase boolean). Посчитайте:

  • конверсию в покупку в группах A и B,
  • абсолютный uplift в п.п.,
  • относительный lift в процентах,
  • общее число пользователей в каждой группе.

Решение

Прямой запрос

WITH agg AS (
  SELECT
    variant,
    COUNT(*)                                             AS users,
    SUM(CASE WHEN has_purchase THEN 1 ELSE 0 END)        AS buyers,
    AVG(CASE WHEN has_purchase THEN 1.0 ELSE 0.0 END)    AS cr
  FROM experiment
  GROUP BY variant
)
SELECT
  MAX(CASE WHEN variant = 'A' THEN users  END) AS users_a,
  MAX(CASE WHEN variant = 'B' THEN users  END) AS users_b,
  MAX(CASE WHEN variant = 'A' THEN buyers END) AS buyers_a,
  MAX(CASE WHEN variant = 'B' THEN buyers END) AS buyers_b,
  ROUND(100 * MAX(CASE WHEN variant = 'A' THEN cr END)::numeric, 3) AS cr_a_pct,
  ROUND(100 * MAX(CASE WHEN variant = 'B' THEN cr END)::numeric, 3) AS cr_b_pct,
  ROUND(100 * (MAX(CASE WHEN variant = 'B' THEN cr END)
             - MAX(CASE WHEN variant = 'A' THEN cr END))::numeric, 3) AS uplift_pp,
  ROUND(100 *
        (MAX(CASE WHEN variant = 'B' THEN cr END)
         - MAX(CASE WHEN variant = 'A' THEN cr END))
       / NULLIF(MAX(CASE WHEN variant = 'A' THEN cr END), 0)::numeric, 2) AS lift_pct
FROM agg;

Z-критерий для двух пропорций — прямо в SQL

WITH agg AS (...)
SELECT
  (cr_b - cr_a)
  / SQRT(p*(1-p)*(1.0/n_a + 1.0/n_b)) AS z_stat
FROM (
  SELECT
    MAX(cr) FILTER (WHERE variant='A') AS cr_a,
    MAX(cr) FILTER (WHERE variant='B') AS cr_b,
    SUM(users) FILTER (WHERE variant='A') AS n_a,
    SUM(users) FILTER (WHERE variant='B') AS n_b,
    (SUM(buyers))::numeric / SUM(users)   AS p
  FROM agg
) t;

p > 1.96 → отвергаем H₀ на уровне 5% (двусторонне).

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

  1. Lift vs uplift. Разные люди путают: uplift — абсолютная разность пропорций (п.п.), lift — относительная (раз/процентов). Уточняйте.
  2. Деление целых. 2/10 = 0 в Postgres. Везде кастуйте ::numeric или умножайте на 1.0.
  3. Один пользователь в двух группах. Если попал и в A, и в B — должен быть исключён или приписан к одной группе при назначении.
  4. Бакетирование (sticky-assignment): проверьте, что один user_id всегда видит один variant.
  5. COUNT(*) vs COUNT(DISTINCT user_id). Если в таблице по строке на сессию — берите COUNT(DISTINCT user_id).
  6. Стат. значимость. Lift без p-value и доверительного интервала — не результат, а наблюдение.

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

AVG(CASE WHEN has_purchase THEN 1.0 ELSE 0.0 END) — конверсия. Uplift = cr_b - cr_a (п.п.), lift = (cr_b - cr_a)/cr_a (%). Без z-теста или CI результаты не сообщают.

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

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

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