Раздел 42 · Аналитика данных с нуля

A/B-тест на SQL: дизайн, проверка групп и z-тест

middle~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Фрея · риск-менеджер необанка «Монетка»

С 1 апреля половина клиентов получает 5% кешбэка в кафе. Продуктовая команда уже кричит «траты выросли на 11%!». Я не верю никому, пока не проверю сам тест. Он вообще честно устроен?

Мы продолжаем с базой bank из прошлого урока и Фреей, которая не принимает цифры на веру. В «Монетке» идёт эксперимент: с 1 апреля 2026 клиенты группы test получают 5% кешбэка в кафе и ресторанах (MCC 5812 и 5814) сверх обычного 1%, группа control только обычный 1%. Кто в какой группе, записано в cashback_groups. Окупился ли кешбэк, Фрея решит в финале курса. Сегодня проверяем, можно ли вообще доверять тесту, и считаем первую метрику.

Две половины поля

Агроном хочет проверить удобрение. Он не сравнивает этот год с прошлым: погода другая. Он делит одно поле на две половины, удобряет одну и сравнивает урожай в тот же год. А чтобы половины были похожи, делит не «северную и южную», а грядки вперемешку, по жребию.

A/B-тест устроен так же. Дизайн теста отвечает на пять вопросов до старта:

  1. Гипотеза: что изменится. «Кешбэк 5% в кафе приведёт клиентов в кафе чаще».
  2. Метрика: чем меряем. Одна главная, решённая заранее.
  3. Рандомизация: клиентов делят жребием, а не по городу или дате регистрации.
  4. Размер и срок: сколько клиентов и дней нужно, чтобы заметить ожидаемый эффект.
  5. Проверки: что посмотрим до выводов, чтобы поймать сломанный тест.

Проверка 1: каждый в одной группе

Клиент должен видеть ровно один вариант. Посмотрим, так ли это:

SELECT client_id, count(*)
FROM cashback_groups
GROUP BY client_id
HAVING count(*) > 1;

30 клиентов записаны в обе группы: сбой распределения выдал им второй вариант через несколько часов после первого. Какой кешбэк они видели, неизвестно, поэтому их исключают из анализа целиком, из обеих групп сразу.

WITH dual AS (
  SELECT client_id FROM cashback_groups GROUP BY client_id HAVING count(*) > 1
)
SELECT test_group, count(*) AS clients
FROM cashback_groups
WHERE client_id NOT IN (SELECT client_id FROM dual)
GROUP BY test_group;

После чистки 976 клиентов в test и 994 в control.

Проверка 2: сплит 50 на 50

Задумывали поровну. 976 из 1970 это 49,54%. Случайный перекос или сломанное распределение? Перекос размеров групп называют SRM, и его проверяют тем же z, что и доли, о нём ниже. Здесь z = −0,41: в пределах случайности, сплит в порядке.

Проверка 3: группы похожи до теста

Если жребий честный, до 1 апреля группы должны вести себя одинаково. Возьмём долю клиентов, которые хоть раз платили в кафе в январе-марте: 84,84% в test и 86,32% в control. Разница в полтора пункта, и она в пользу контроля. Запомним: тестовая группа стартовала не с форой.

Метрика: кто дошёл до кафе

Главная метрика нашего разбора: доля клиентов группы, которые за время теста хоть раз заплатили в кафе. Для каждого клиента ставим флаг через EXISTS: так клиент без единой операции останется в знаменателе с false, а не выпадет из JOIN.

WITH dual AS (
  SELECT client_id FROM cashback_groups GROUP BY client_id HAVING count(*) > 1
), groups AS (
  SELECT client_id, test_group FROM cashback_groups
  WHERE client_id NOT IN (SELECT client_id FROM dual)
), flags AS (
  SELECT g.test_group,
         EXISTS (
           SELECT 1 FROM transactions t JOIN cards c ON c.id = t.card_id
           WHERE c.client_id = g.client_id AND t.status = 'approved'
             AND t.mcc IN (5812, 5814) AND t.created_at >= '2026-04-01'
         ) AS converted
  FROM groups g
)
SELECT test_group, count(*) AS clients,
       count(*) FILTER (WHERE converted) AS converted,
       round(avg(converted::int), 4) AS conversion
FROM flags
GROUP BY test_group;
 test_group | clients | converted | conversion
------------+---------+-----------+------------
 control    |     994 |       870 |     0.8753
 test       |     976 |       881 |     0.9027

Разница 2,74 процентного пункта в пользу теста. Осталось понять, не шум ли это.

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.