A/B в SQL: если вы собрали выборку так, статистика уже не спасёт тест.

Типичная картина: в дашборде “победа”, в продукте ничего не меняется, а потом выясняется, что выборка была собрана с красивой логикой… но с одним маленьким читом. В A/B самый дорогой баг часто не в методе, а в join’ах и фильтрах. Потому что вы подменяете вопрос “что изменил эксперимент” на “что осталось в выборке”.

Почему это ломается: assignment (кто в какой группе) живёт во времени, экспозиция живёт во времени, события живут во времени. SQL же легко превращает это в одну плоскую таблицу, где будущее подмешалось в прошлое, а отсутствие событий стало “нулём”.

5 сигналов, что выборка собрана неправильно:

  1. Утечка будущего Вы фильтруете по событиям, которые могли случиться только после экспозиции, а потом считаете метрики “как будто честно”. Любой WHERE по post-event в таблице, из которой строится cohort, должен настораживать.

  2. Фильтры “после факта” “Оставим только тех, кто открыл экран/дошёл до шага/сделал покупку” — и это применяется к обеим группам. Это уже не A/B, а сравнение выживших.

  3. Неправильное окно экспозиции Экспозиция считается “когда угодно в периоде”, а метрики — “за весь период”. Или наоборот: пользователь попал в группу сегодня, а вы тянете его события за вчера.

  4. “Выпавшие” события После join’ов внезапно пропадает часть пользователей или событий: inner join на редкую таблицу, фильтр по non-null, дедуп по неправильному ключу. На графиках тишина, в логах дырка.

  5. Несогласованность join’ов Группу джойните по user_id, события по device_id, а конверсию по order_id. Или один и тот же пользователь размножается из-за many-to-many, и метрика раздувается.

Что проверять первым, до любых p-value:

  • Единица анализа: одна строка на пользователя (или на сессию) там, где вы считаете метрику. Убедитесь, что не размножили сущность.
  • Временные границы: assignment_time, first_exposure_time, start/end окна метрик. События до экспозиции не должны попадать в post-окно.
  • Потери: анти-джойн от assignment к финальной таблице и обратно. Сколько “не доехало” и почему.
  • Баланс: количество пользователей по группам до и после всех фильтров. Если баланс “исправился” фильтрами — это красный флаг.
  • Дедуп: явное правило, какая запись assignment считается истинной (первая, последняя), и одинаковое правило везде.

Правильная практика выглядит скучно: сначала фиксируете когорту по assignment, потом фиксируете факт экспозиции, потом режете окна метрик, и только потом агрегируете. Спорят обычно про детали окна и дедупа, но если у вас есть утечка будущего или post-фильтры, спорить уже не о чем.