Привет, на связи Аслан Байрамкулов, автор курса по A/B-тестированию 👋🏻
Как проверить новый алгоритм цен? Этот вопрос вызывает трудности у многих команд, занимающихся ценообразованием. У кого-то новая модель динамических цен, у кого-то другая формула комиссии, у кого-то ML подсказывает скидки.
Первый импульс — провести классический A/B на пользователях/магазинах. Часто я отговариваю от этой идеи и предлагаю подумать в сторону switchback-экспериментов. Для прайсинга классический A/B не всегда является правильным решением. Давайте разберёмся, почему.
Объясню на примере. Вы тестируете новую логику скидок в доставке еды. Делите юзеров 50/50: половине — новые цены, половине — старые. Ждёте неделю, считаете средний чек.
Теперь посмотрим, что в это время делает рынок.
Скидка в ЦГ (целевая группа) подняла спрос на определённые позиции. Курьеры в этих районах загружены, что привело к росту среднего времени доставки. Пользователи в КГ (контрольная группа) видят этот увеличившийся показатель времени и отказываются от заказа. Их retention упал из-за изменения, которое было осуществлено в ЦА. КГ уже не является нейтральным базовым уровнем — это группа, на которую повлияло воздействие в тесте.
Формально это нарушение SUTVA — предположения, что воздействие на одного юзера не меняет результат у другого. Иными словами, ваш A/B молча предполагает, что пользователи живут в изолированных средах и не влияют друг на друга. В нашем кейсе это явно не так, рынок общий. Задача «сравнить две параллельные вселенные» делением пользователей не решается, вселенная одна на всех.
Для привыкших делать классические A/B это не так очевидно с первого взгляда. Я видел многие команды, кто делал классический A/B на ценах, получал слабые эффекты и думал: «Прайсинг-модель так себе, попробуем другую». Было показано на пальцах, что это лишь измерение интерференции, а не эффекта, и любая новая модель будет давать ту же картину.
Switchback идёт другим путём. Вместо того чтобы делить пользователей, мы делим время/пространство.
Как это выглядит. Берём сетку «час × регион», скажем, «Москва-центр, вторник, 19:00» — одна ячейка. Каждой ячейке случайно присваиваем treatment (тест) или control (контроль). В час X весь регион видит новую цену, в час Y старую. Усредняем метрики внутри ячейки, сравниваем средние между группами.
В любой момент времени рынок целиком в одном режиме. Интерференции больше нет.
Но и здесь есть ловушки.
🔸 Первая — carryover. Переключились с treatment на control в 19:00, но заказы из предыдущего часа выполняются 40 минут и падают в учёт контрольного часа. Решение — первые 10–15 минут ячейки не учитываются в метрике (но платим понижением статистической мощности).
🔸 Вторая — эффективный размер выборки считается не в пользователях, а в ячейках. У вас миллион заказов, но если они уложились в 400 ячеек, то эффективный N = 400. Анализ мощности, посчитанный по юзерам, будет некорректен. А это влияет на ширину доверительного интервала и точность оценки. Интервалы будут кратно шире.
🔸 Третья — временная гетерогенность. Будни ≠ выходные, утро ≠ вечер, праздники и другие календарные аспекты. Если две недели switchback совпали с длинными выходными, половина дизайна улетела. Решение — стратифицированная рандомизация: treatment и control распределяются равномерно по дню недели и часу суток, а не случайно по всем ячейкам.
Отдельно про анализ результатов. T-test на уровне заказов тут не работает. Заказы внутри одной ячейки не являются независимыми: один рынок в один час коррелирован сам с собой. Если считать наивно, то стандартные ошибки будут занижены, и p-value выглядит красивее, чем есть на самом деле.
Часто используют регрессию с фиксированными эффектами по ячейке и кластерные SE, либо блочный бутстрап. Некоторые решения можно подсмотреть в блоге Lyft. Кто хочет теоретический подробный разбор, то рекомендую к прочтению статью Bojinov, Simchi-Levi, Zhao в Management Science (2023). В ней можно найти разбор оптимального дизайна и анализа под разные модели carryover.