MVP, гипотезы и A/B-эксперименты
Есть ровно два способа узнать, работает ли продуктовая идея: построить её целиком и посмотреть — или построить минимальный кусок, который даёт тот же ответ, и посмотреть раньше. Вся эта статья — о втором способе: как сделать проверку дешёвой, не сделав её при этом бессмысленной.
Это важно, потому что дешёвая проверка легко вырождается в самообман. Лендинг, на который пришли друзья из чата, «показал спрос». Тест, который смотрели каждый день и остановили на зелёном результате, «показал рост». Пилот на десяти клиентах, отобранных отделом продаж, «подтвердил гипотезу». Каждый из этих экспериментов формально проведён — и ни один не даёт информации.
Разберём с первых принципов: что такое MVP на самом деле (и чем он не является), как устроена фальсифицируемая гипотеза, почему рандомизация — не ритуал, а единственный известный способ измерить причинный эффект, какая статистика нужна минимально, и что из этой статистики разваливается на реальных продуктовых данных.
Предполагается, что вы уже знакомы с метриками из статьи Продуктовые метрики — деревом метрик, guardrails и когортным retention. Эксперимент без заранее выбранной метрики измерять нечем.
1. Зачем: цена непроверенного решения
1.1. Базовый факт индустрии
Доля идей, которые улучшают целевую метрику, мала и стабильна между компаниями:
| Компания | Доля экспериментов с положительным эффектом |
|---|---|
| Microsoft (Bing и др.) | около 1/3 улучшают, 1/3 нейтральны, 1/3 вредят |
| Netflix | «большинство идей не работает» — постоянный тезис их инженерного блога |
| Booking.com | порядка 10% экспериментов дают значимый положительный результат |
| Google (поиск) | около 10–20% запускаемых тестов доходят до продакшна |
Источник по Microsoft — Kohavi, Tang, Xu «Trustworthy Online Controlled Experiments», по Booking — их публичный доклад о культуре экспериментов.
Из этого следует неприятное, но полезное: ваша интуиция о том, что сработает, примерно не лучше монетки — и это верно для сильных продактов тоже. Разница между сильным и слабым продактом не в проценте угадываний, а в цене одной проверки. Если проверка стоит квартал разработки, три четверти квартала уходит в мусор. Если проверка стоит два дня, мусор стоит два дня.
1.2. Формула, из которой всё выводится
Скорость обучения команды:
скорость обучения = (число проверенных гипотез) / (время)
≈ 1 / (средняя стоимость одной проверки)
Отсюда две стратегии улучшения: проверять больше гипотез параллельно (нужна инфраструктура экспериментов) и делать каждую проверку дешевле (нужен навык проектирования MVP). Обе — предмет этой статьи. Заметьте: «угадывать лучше» в этой формуле не фигурирует. Это принципиально другая модель работы, чем «продакт придумывает правильное решение».
1.3. Контур Build–Measure–Learn и его правильное направление
Эрик Рис в «The Lean Startup» описал цикл Build → Measure → Learn. Формулировка регулярно понимается неправильно — как «сначала строим, потом смотрим». Планировать цикл нужно в обратном порядке:
- Learn — что именно я хочу узнать? Какое решение я приму по результату?
- Measure — какое число ответит на этот вопрос? Какой порог отделяет «да» от «нет»?
- Build — какой минимальный артефакт даст мне это число?
Если пункт 1 не сформулирован, пункт 3 всегда вырождается в «давайте сделаем нормально» — и вы построили продукт, а не эксперимент. Практическое правило: эксперимент, результат которого не меняет ни одного вашего решения, проводить не нужно — вне зависимости от того, насколько он интересен. Это прямое следствие идеи ценности информации из обзора трека.
2. MVP: что это на самом деле
2.1. Определение и главное недоразумение
Каноническое определение Риса: MVP — это версия продукта, которая позволяет собрать максимум проверенного знания о клиентах с минимальными затратами усилий.
Ключевые слова здесь — «проверенное знание», а не «продукт». MVP — это эксперимент, у которого случайно есть пользовательский интерфейс. Отсюда три частых искажения:
| Искажение | Почему неверно |
|---|---|
| «MVP = урезанная первая версия продукта» | Урезанная версия проверяет всё сразу и плохо; MVP проверяет одно и точно |
| «MVP = сырой продукт, качество не важно» | Если пользователь ушёл из-за бага, вы измерили баг, а не гипотезу |
| «MVP делается один раз, в начале» | MVP — режим проверки любой крупной ставки, в том числе в зрелом продукте |
Полезная поправка от Джеффа Готелфа и Джоша Сейдена и практиков Basecamp: слово «продукт» в аббревиатуре лишнее. Точнее было бы MVE — minimum viable experiment. Иногда лучший MVP не содержит ни строки кода.
2.2. «Минимальный» — относительно чего
Минимальность не абсолютна. MVP минимален относительно конкретной гипотезы.
- Гипотеза «люди захотят покупать обувь онлайн» → достаточно фотографий обуви из соседнего магазина и ручной покупки после заказа (так делал Zappos).
- Гипотеза «синхронизация файлов между устройствами понятна и нужна» → достаточно трёхминутного видео с демонстрацией работы (так делал Dropbox: видео подняло waitlist с 5 до 75 тысяч человек за ночь).
- Гипотеза «наша шифрованная синхронизация выдержит 10 ТБ и не потеряет данные» → видео бесполезно, нужен работающий код и нагрузочный тест.
Третий пример важен: если ваш риск технический, дешёвый MVP из папье-маше не помогает. Про четыре риска (ценность, юзабилити, реализуемость, жизнеспособность) — в Discovery; MVP по умолчанию бьёт по первым двум.
2.3. Вертикальный срез вместо слоёв
Главная инженерная ошибка при построении MVP — резать продукт горизонтально (сначала база, потом логика, потом UI). Такой порядок даёт нулевую обратную связь до самого конца.
Правило: любой инкремент должен быть сквозным сценарием, пусть узким до неприличия. «Один тип пользователя, один тариф, один город, оплата руками через переводы» — это MVP. «Готова схема БД для всех тарифов» — это не инкремент, это незавершённое производство.
Метафору популяризовал Хенрик Книберг в заметке «Making sense of MVP»: не делайте колесо → шасси → кузов → машина; делайте скейтборд → самокат → велосипед → машина. Важная оговорка, которую обычно опускают: скейтборд имеет смысл, только если он едет. Полу-собранный скейтборд — это всё то же колесо.
2.4. Каталог типов MVP
Разберём типы по возрастанию силы сигнала.
Fake door (ложная дверь). В интерфейс добавляется кнопка несуществующей функции; клик логируется, пользователю показывается «скоро будет». Меряет спрос на обещание, а не на продукт. Дёшево, быстро, есть этическая цена (см. 2.6).
Лендинг с предзаказом. Страница с ценностным предложением и ценой, кнопка «купить» ведёт на форму ожидания или на реальную оплату. Здесь важен нюанс: сигнал становится в разы достовернее, если запрашивается что-то дорогое для пользователя — деньги, корпоративная почта, телефон, время на звонок. Клик по красивой кнопке стоит ноль и значит примерно ноль.
Кликабельный прототип. Figma-прототип, показанный в интервью. Меряет понятность и юзабилити, но не меряет спрос: в присутствии интервьюера люди систематически доброжелательны.
Concierge. Услуга оказывается вручную, но пользователь платит и получает реальную ценность. Пример: Food on the Table — основатель лично составлял списки покупок и ходил в магазин для первых клиентов. Сигнал сильный, масштабирование нулевое — и это нормально, цель не в масштабе.
Wizard of Oz. Пользователь видит работающий продукт, внутри которого сидят люди. Классика — первые «ИИ-ассистенты», где ответы писали операторы. Отличие от concierge: пользователь не знает о ручном труде, поэтому его поведение ближе к реальному.
Пилот / бета на срезе трафика. Настоящий код на 1–5% пользователей за фича-флагом. Единственный тип, который даёт корректный причинный эффект на продуктовые метрики — и именно с него начинается вторая половина статьи.
2.5. Что выбрать: правило дешевейшей достаточной проверки
Правило звучит так: берите самую дешёвую проверку, которой хватит, чтобы изменить решение. Если результат concierge-теста не заставит вас отказаться от идеи — не делайте concierge-тест, делайте сразу то, что заставит.
2.6. Этика и репутационная цена
Fake door и Wizard of Oz — обман, пусть и мелкий. Профессиональная норма:
- Обманывать можно о механике, нельзя — о результате. Показать «скоро» вместо функции допустимо; взять деньги за то, чего не будет, — нет.
- Обязательно предусмотрите выход: «мы пока делаем это вручную, вот срок», компенсация, ранний доступ для тех, кто кликнул.
- Ограничивайте охват. Fake door на 100% аудитории — это не эксперимент, это порча продукта.
- Никогда не делайте fake door на критичных путях (оплата, безопасность, медицина, деньги).
Отдельный технический риск: fake door портит ваши же метрики — клики по несуществующей функции загрязняют воронку, а разочарование бьёт по retention. Логируйте эти показы отдельным флагом, чтобы потом уметь исключить когорту.
3. Гипотеза как единица работы
3.1. Фальсифицируемость
Утверждение является гипотезой, только если существует наблюдение, которое его опровергает. Критерий заимствован у Поппера и в продуктовой работе имеет очень практический смысл: нефальсифицируемая формулировка всегда «подтверждается», поэтому не несёт информации.
| Формулировка | Диагноз |
|---|---|
| «Улучшим онбординг — станет лучше» | Не гипотеза: «лучше» не измеримо, опровергнуть нельзя |
| «Пользователям нужен тёмный режим» | Не гипотеза: нет метрики, нет порога, нет срока |
| «Добавление шага с импортом контактов повысит 7-дневный retention новых пользователей на 3 п.п. или больше» | Гипотеза: измеримо, есть порог, есть срок |
Рабочий шаблон, который стоит завести в шаблоне задачи:
Мы верим, что [изменение] для [сегмента]
приведёт к [эффект на конкретной метрике, с числом]
потому что [механизм: почему это должно работать]
Мы поймём, что ошиблись, если [наблюдение-опровержение]
Проверим за [срок], решение примем [дата], владелец [имя]
Блок «потому что» — не украшение. Он фиксирует вашу модель мира. Когда результат окажется отрицательным, вы будете знать, что именно оказалось неверным: механизм или его реализация. Эксперимент без механизма учит вас только тому, что «этот конкретный вариант не сработал», — это самая дорогая форма знания.
3.2. Leap-of-faith assumptions: с чего начинать
У любой крупной идеи допущений десятки, но не все равны. Рис называет leap-of-faith assumptions те допущения, при ложности которых вся идея бессмысленна. Ранжируйте их по двум осям — «насколько критично» и «насколько мы уверены», и проверяйте сверху вниз: критично + не уверены.
Пример для маркетплейса услуг:
| Допущение | Критичность | Уверенность | Проверять? |
|---|---|---|---|
| Мастера готовы платить комиссию 15% | Высокая | Низкая | Первым делом |
| Клиенты доверят предоплату незнакомцу | Высокая | Низкая | Вторым |
| Мы сможем сверстать каталог | Высокая | Высокая | Не проверять |
| Нужны push-уведомления | Низкая | Средняя | Потом |
Типичная ошибка — начать с самой интересной или самой понятной проверки, а не с самой опасной.
3.3. Критерий успеха фиксируется ДО запуска
Это дисциплинарное требование, а не бюрократия. Если порог не записан заранее, человеческая психика подгоняет его под результат: рост 0.4% при заявленном ожидании 3% превращается в «ну, направление верное». Практика из клинических исследований — предрегистрация — переносится в продукт напрямую: до запуска фиксируем метрику, порог, размер выборки, срок, план сегментации и — обязательно — какое решение мы примем при каждом исходе.
Минимальный документ на одну страницу:
эксперимент: onboarding_import_contacts_v1
гипотеза: импорт контактов на 2-м шаге повышает 7d-retention новых пользователей
метрика_решения: retention_d7 новых регистраций # OEC
guardrail_метрики: # не должны просесть
- время_загрузки_p95
- доля_отказов_на_шаге_2
- жалобы_в_поддержку_на_1000
сегмент: новые регистрации, мобильное приложение, RU
юнит_рандомизации: user_id
доля_трафика: 50/50
mde: 3 п.п. (с 32% до 35%)
alpha: 0.05
power: 0.8
размер_выборки: 3800 на группу
длительность: 14 дней, не менее 2 полных недель
решение_если_да: раскатываем на 100%, ставим в онбординг постоянно
решение_если_нет: откатываем, гипотеза «трение импорта» закрыта
решение_если_неопределённо: не продлеваем, фиксируем как неудачу теста
владелец: ...
Строка решение_если_неопределённо — самая полезная. Именно её отсутствие порождает вечные
эксперименты, которые «ещё немного покрутим».
3.4. Жизненный цикл гипотезы
Обратите внимание на состояние Неопределённо — в реальности это самый частый исход, и его
отсутствие в процессе команды приводит к бинарному мышлению «сработало / не сработало»
там, где данных просто не хватило.
4. Почему нужен A/B, а не «выкатили и сравнили»
4.1. Проблема
Выкатили новый онбординг в понедельник, за следующую неделю конверсия выросла с 9% до 9.8%. Сработало?
Неизвестно. Одновременно с вашим релизом:
- маркетинг запустил кампанию, и пришёл другой по качеству трафик;
- в конкурента прилетел сбой, часть его аудитории пришла к вам;
- прошлая неделя включала праздник, а эта — нет;
- команда роста поменяла письмо активации;
- сезон.
Это конфаундеры — факторы, влияющие и на «попадание в новую версию» (через время), и на метрику. Сравнение «до/после» смешивает эффект вашей фичи со всем, что изменилось в мире. Это не поправимо статистикой: у вас просто нет наблюдения того, что было бы без изменения.
4.2. Формальная постановка: потенциальные исходы
Модель Рубина. Для пользователя i определим два числа: Yᵢ(1) — его метрика, если он получил
изменение, и Yᵢ(0) — если не получил. Индивидуальный эффект τᵢ = Yᵢ(1) − Yᵢ(0).
Фундаментальная проблема причинного вывода: для каждого i мы наблюдаем ровно одно из двух
чисел. Индивидуальный эффект принципиально ненаблюдаем.
Но средний эффект по популяции — наблюдаем, если мы умеем построить сравнимую группу:
ATE = E[Y(1)] − E[Y(0)]
Рандомизация делает ровно это: назначение варианта независимо от всех характеристик пользователя — и известных, и неизвестных. Тогда
E[Y | группа B] − E[Y | группа A] = E[Y(1)] − E[Y(0)] = ATE
Это единственный практичный метод, который защищает от конфаундеров, о которых вы не знаете. Никакая пост-фактум корректировка «по сегментам» так не умеет: скорректировать можно только те переменные, которые вы догадались измерить.
Формально требуются две предпосылки: назначение независимо от потенциальных исходов (даёт рандомизация) и SUTVA — исход пользователя не зависит от того, какой вариант получили другие. Вторая ломается в соцсетях и маркетплейсах; про это раздел 8.
Хорошее введение — Cunningham «Causal Inference: The Mixtape», бесплатно онлайн.
5. Статистика: минимально достаточный набор
5.1. Что означает p-value
Средние по группам всегда различаются — даже если варианты идентичны. Вопрос не «есть ли разница», а «правдоподобна ли такая разница при отсутствии эффекта».
p-value — вероятность увидеть отличие не меньше наблюдаемого, если истинного эффекта нет.
Чего p-value не означает (перечень ошибок, которые встречаются в каждом втором отчёте):
- ❌ вероятность того, что гипотеза верна;
- ❌ вероятность того, что вы ошиблись;
- ❌ размер эффекта («p = 0.001, значит эффект большой» — нет, значит выборка большая);
- ❌
p = 0.06означает «эффекта нет» (означает «данных не хватило»).
Официальная позиция Американской статистической ассоциации по этим ошибкам — ASA Statement on p-Values (2016).
5.2. Две ошибки, мощность и MDE
- α (ошибка I рода) — увидеть эффект, которого нет. Обычно 0.05.
- β (ошибка II рода) — не заметить существующий эффект. Обычно 0.2.
- Мощность = 1 − β — вероятность заметить эффект заданного размера. Обычно 0.8.
- MDE (minimum detectable effect) — наименьший эффект, который тест данного размера способен обнаружить с заявленной мощностью.
Цена ошибок в продукте асимметрична и зависит от контекста. Для рискованного изменения
в оплате α важнее (выкатить вредное дороже, чем упустить полезное). Для дешёвой правки текста
можно поднять α до 0.1: цена ложноположительного вывода — один зря сделанный копирайт.
Слепое α = 0.05 везде — карго-культ из биомедицины.
5.3. Размер выборки
Для метрики-доли (конверсия) при равных группах:
n на группу ≈ 2 · (z_{1−α/2} + z_{1−β})² · p(1−p) / Δ²
где p — базовая конверсия, Δ — абсолютный MDE. При α = 0.05 и мощности 0.8
(1.96 + 0.84)² ≈ 7.85.
Главное свойство формулы: n растёт как 1/Δ². Хотите ловить вдвое меньший эффект — нужно вчетверо больше пользователей. Это и есть причина, по которой малые продукты не могут «просто A/B-тестировать всё».
"""Расчёт размера выборки и MDE для A/B-теста на метрике-доле."""
from math import sqrt
from statistics import NormalDist
ND = NormalDist()
def sample_size_proportion(p: float, mde_rel: float,
alpha: float = 0.05, power: float = 0.8) -> int:
"""Сколько пользователей нужно на КАЖДУЮ группу.
p — базовая конверсия контроля (например 0.10)
mde_rel — относительный эффект, который хотим уметь ловить (0.05 = +5%)
"""
delta = p * mde_rel # абсолютный эффект
p2 = p + delta
p_bar = (p + p2) / 2
z_a = ND.inv_cdf(1 - alpha / 2) # двусторонний критерий
z_b = ND.inv_cdf(power)
# точная формула с раздельными дисперсиями под H0 и H1
num = (z_a * sqrt(2 * p_bar * (1 - p_bar))
+ z_b * sqrt(p * (1 - p) + p2 * (1 - p2))) ** 2
return int(num / delta ** 2 + 0.999)
def mde_from_n(p: float, n: int, alpha: float = 0.05, power: float = 0.8) -> float:
"""Обратная задача: какой относительный эффект поймаем при заданном n."""
z = ND.inv_cdf(1 - alpha / 2) + ND.inv_cdf(power)
delta = z * sqrt(2 * p * (1 - p) / n)
return delta / p
if __name__ == "__main__":
base = 0.10
for rel in (0.01, 0.02, 0.05, 0.10, 0.20):
n = sample_size_proportion(base, rel)
print(f"MDE {rel:>5.0%}: {n:>10,} на группу, "
f"{2 * n:>10,} всего")
# сколько мы реально можем при 20 000 пользователей в неделю на группу
print(f"\nПри n=20 000 на группу ловим эффект от "
f"{mde_from_n(base, 20_000):.1%}")
Вывод (базовая конверсия 10%):
MDE 1%: 1,419,073 на группу, 2,838,146 всего
MDE 2%: 356,335 на группу, 712,670 всего
MDE 5%: 57,763 на группу, 115,526 всего
MDE 10%: 14,751 на группу, 29,502 всего
MDE 20%: 3,841 на группу, 7,682 всего
При n=20 000 на группу ловим эффект от 8.4%
Практический вывод, который меняет работу продакта. Считайте MDE до того, как обещаете
эксперимент. Если у вас 20 000 пользователей в неделю на группу, а вы собираетесь тестировать
изменение цвета кнопки с ожидаемым эффектом 1% — тест обречён: он не отличит +1% от нуля,
и вы потратите две недели, чтобы получить p = 0.4. Правильное действие — либо взять
изменение покрупнее, либо мерить метрику ближе к изменению (клик по кнопке, а не покупка),
либо не тестировать вовсе и принять решение экспертно.
5.4. Анализ результата: интервал важнее p-value
"""Анализ завершённого A/B-теста на конверсии: z-критерий и доверительные интервалы."""
import math
from dataclasses import dataclass
from statistics import NormalDist
ND = NormalDist()
@dataclass
class ABResult:
p_control: float
p_treatment: float
lift_abs: float
lift_rel: float
p_value: float
ci_abs: tuple[float, float]
ci_rel: tuple[float, float]
def analyze(conv_a: int, n_a: int, conv_b: int, n_b: int,
alpha: float = 0.05) -> ABResult:
"""conv_* — число конверсий, n_* — число пользователей в группе."""
p_a, p_b = conv_a / n_a, conv_b / n_b
diff = p_b - p_a
# стандартная ошибка разности двух независимых долей
se = math.sqrt(p_a * (1 - p_a) / n_a + p_b * (1 - p_b) / n_b)
z = diff / se
p_value = 2 * (1 - ND.cdf(abs(z)))
crit = ND.inv_cdf(1 - alpha / 2)
lo, hi = diff - crit * se, diff + crit * se
return ABResult(
p_control=p_a, p_treatment=p_b,
lift_abs=diff, lift_rel=diff / p_a,
p_value=p_value,
ci_abs=(lo, hi),
ci_rel=(lo / p_a, hi / p_a),
)
r = analyze(conv_a=1080, n_a=12_000, conv_b=1176, n_b=12_000)
print(f"контроль: {r.p_control:.2%}")
print(f"вариант: {r.p_treatment:.2%}")
print(f"прирост: {r.lift_rel:+.2%} (абс. {r.lift_abs:+.2%})")
print(f"p-value: {r.p_value:.4f}")
print(f"95% CI отн: [{r.ci_rel[0]:+.2%}, {r.ci_rel[1]:+.2%}]")
контроль: 9.00%
вариант: 9.80%
прирост: +8.89% (абс. +0.80%)
p-value: 0.0337
95% CI отн: [+0.69%, +17.09%]
Как это читать правильно:
«Мы наблюдаем прирост конверсии 8.9%. Данные несовместимы с гипотезой об отсутствии эффекта (p = 0.034). При этом истинный эффект с 95% доверием лежит между +0.7% и +17.1% — то есть он может быть как почти нулевым, так и очень крупным. Для решения “раскатывать” этого достаточно (нижняя граница положительна, изменение дешёвое). Для решения “строить на этом стратегию и планировать выручку” — нет, интервал слишком широк.»
Именно поэтому всегда сообщайте интервал, а не только «стат-значимо». Точечная оценка 8.9% почти наверняка завышена — см. проклятие победителя в 7.6.
6. Дизайн эксперимента
6.1. Юнит рандомизации
| Юнит | Когда использовать | Риск |
|---|---|---|
user_id |
Основной выбор для залогиненных | Нужна стабильность между сессиями |
device_id / cookie |
Анонимный трафик, лендинги | Один человек попадёт в обе группы с разных устройств |
| Сессия | Изменения без памяти между визитами | Непоследовательный опыт, испорченный retention |
| Аккаунт / компания | B2B, командные продукты | Мало юнитов → низкая мощность |
| Гео / город | Маркетплейсы, доставка | Очень мало юнитов, нужны кластерные поправки |
Два железных правила:
- Юнит рандомизации не может быть мельче юнита анализа. Если рандомизируете по сессиям, а меряете 7-дневный retention пользователя — вывод некорректен: один пользователь оказался в обеих группах, эффект «размывается» к нулю.
- Назначение должно быть стабильным и детерминированным. Стандарт —
hash(user_id + experiment_salt) % 100. Соль на эксперимент нужна, чтобы пользователь, попавший в группу B в одном тесте, не попадал систематически в B и в следующем (иначе эксперименты коррелируют).
"""Детерминированное назначение варианта — так это делают в проде."""
import hashlib
def assign(user_id: str, experiment: str, weights: dict[str, int]) -> str:
"""Возвращает вариант. Одинаковый ответ на любом сервере и в любом ЯП,
потому что зависит только от хеша строки."""
key = f"{experiment}:{user_id}".encode()
# берём первые 8 байт SHA-256 как целое и нормируем в [0, 10000)
bucket = int.from_bytes(hashlib.sha256(key).digest()[:8], "big") % 10_000
edge = 0
for variant, weight in weights.items(): # weight в промилле от 10000
edge += weight
if bucket < edge:
return variant
return "control" # остаток трафика — контроль
# 5% на новый вариант, 5% на его контроль, 90% вне эксперимента
print(assign("u-42", "onboarding_import_v1", {"treatment": 500, "control": 500}))
Свойства такой схемы: без состояния (не нужна БД назначений), консистентна между бэкендом и клиентом, воспроизводима задним числом при разборе инцидентов.
6.2. OEC и guardrails
OEC (Overall Evaluation Criterion) — одна метрика, по которой принимается решение.
Одна, а не пять: при пяти метриках вы всегда найдёте выросшую. Если метрик решения объективно
несколько, комбинируйте их в явную формулу заранее (например,
OEC = конверсия × 0.7 + удержание_d7 × 0.3), а не выбирайте постфактум.
Guardrails — метрики, которые не должны просесть: скорость (p95 времени загрузки), доля ошибок, обращения в поддержку, отписки, ключевые метрики соседних команд. Их проверяют на непревышение порога вреда, а не на «рост». Классический пример из Bing: изменение, поднявшее выручку на 30%, было отклонено, потому что уронило метрики качества поиска — то есть конвертировало долгосрочное доверие в краткосрочные деньги.
Про построение дерева метрик и выбор North Star — Продуктовые метрики.
6.3. Санитарные проверки: A/A и SRM
Прежде чем верить результату, убедитесь, что механика работает.
A/A-тест — оба варианта идентичны. Ожидаемо: доля значимых результатов ≈ α, распределение p-value равномерное. Если A/A-тесты дают значимые различия чаще, чем в 5% случаев, — у вас сломана рандомизация, логирование или аналитика, и все остальные результаты недействительны.
SRM (Sample Ratio Mismatch) — проверка того, что фактическое разбиение соответствует заявленному. Это самая полезная одиночная проверка во всей практике экспериментов.
"""Проверка Sample Ratio Mismatch: соответствует ли фактическое деление плану."""
from math import erfc, sqrt
def srm_check(counts: dict[str, int], expected: dict[str, float]) -> tuple[float, float]:
"""counts — фактические размеры групп, expected — ожидаемые доли (в сумме 1).
Возвращает (chi2, p_value). p < 0.001 => эксперимент невалиден.
"""
total = sum(counts.values())
chi2 = 0.0
for group, observed in counts.items():
exp = total * expected[group]
chi2 += (observed - exp) ** 2 / exp
df = len(counts) - 1
if df == 1: # хвост хи-квадрат с 1 с.с. выражается через erfc
p = erfc(sqrt(chi2 / 2))
else: # для df>1 в проде берите scipy.stats.chi2.sf
raise NotImplementedError("используйте scipy.stats.chi2.sf(chi2, df)")
return chi2, p
for control, treatment in [(11_913, 12_087), (11_500, 12_500)]:
chi2, p = srm_check({"A": control, "B": treatment}, {"A": 0.5, "B": 0.5})
verdict = "ОК" if p > 0.001 else "SRM! результат читать нельзя"
print(f"A={control} B={treatment}: chi2={chi2:8.2f} p={p:.2e} {verdict}")
A=11913 B=12087: chi2= 1.26 p=2.61e-01 ОК
A=11500 B=12500: chi2= 41.67 p=1.08e-10 SRM! результат читать нельзя
Расхождение в 4% выглядит безобидно, но при таких размерах это событие вероятностью 1e-10 — значит, работает какой-то механизм отбора. Типичные причины: редирект, который теряет часть пользователей одной группы; падение варианта B у части устройств; бот-фильтр, срабатывающий асимметрично; кэш CDN, отдающий контроль части пользователей. При SRM результат почти всегда смещён в пользу варианта — потерялись именно те, кому было хуже. Микрософт делает SRM-проверку блокирующей: тест с SRM не показывается в отчёте вовсе.
6.4. Длительность
Три ограничения, максимум из которых определяет срок:
- Набрать размер выборки (раздел 5.3).
- Минимум одна полная неделя, лучше две. Поведение в выходные и будни различается, и если тест захватил три выходных для варианта B и два для A — вы измерили календарь.
- Пережить эффекты новизны и первичности. Novelty effect: активные пользователи кликают на новое просто потому, что оно новое; эффект затухает за 1–3 недели. Primacy effect: наоборот, привычные пользователи хуже работают с изменённым интерфейсом первое время. Оба искажают краткосрочный замер. Диагностика — посмотреть эффект по дням от начала экспозиции: если он монотонно затухает к нулю, это новизна, а не ценность.
Не останавливайте тест раньше срока из-за хорошего промежуточного результата — почему, см. 7.5.
6.5. Как это работает в рантайме
проверка сегмента и «мьютексов» F-->>B: "treatment" B->>L: exposure(user_id, exp, variant, ts) Note over L: экспозиция логируется ТОЛЬКО
когда пользователь реально увидел вариант B-->>C: экран с шагом импорта контактов C->>L: событие step_viewed U->>C: импортирует контакты C->>L: событие contacts_imported U-->>C: возвращается на 7-й день C->>L: событие session_start L->>DW: поток событий DW->>DW: join экспозиций и метрик, дедупликация, SRM DW->>DW: расчёт OEC, guardrails, CUPED, CI
Критичная деталь — шаг 6. Экспозиция логируется в момент фактического показа варианта, а не в момент назначения. Если логировать назначение, в анализ попадут пользователи, которые до экрана вообще не дошли; они одинаковы в обеих группах и разбавляют эффект, снижая мощность (иногда в разы). Это называется trigger-анализ: считаем только «затронутых».
7. Что ломается на реальных данных
Учебная статистика предполагает независимые одинаково распределённые наблюдения из нормального распределения. Продуктовые данные не такие ни в одном пункте.
7.1. Ratio-метрики и дельта-метод
Метрика вида «средний чек», «CTR = клики / показы», «просмотров на сессию» — это отношение двух случайных величин, и юнит анализа не совпадает с юнитом рандомизации: рандомизируем пользователей, а показов у пользователя много и они коррелируют.
Наивная формула стандартной ошибки для среднего здесь занижает её — и вы получаете
ложноположительные результаты. Правильный инструмент — дельта-метод: линеаризация отношения
R = X̄ / Ȳ вокруг средних даёт
Var(R) ≈ (1/Ȳ²)·Var(X) − 2·(X̄/Ȳ³)·Cov(X, Y) + (X̄²/Ȳ⁴)·Var(Y)
где X, Y — агрегаты на пользователя (например, клики пользователя и показы пользователя).
"""Дельта-метод: корректная дисперсия ratio-метрики (CTR = клики/показы)."""
import math
from statistics import mean
def ratio_metric_var(numer: list[float], denom: list[float]) -> tuple[float, float]:
"""numer/denom — поюзерные суммы (клики и показы одного пользователя).
Возвращает (значение метрики, дисперсию оценки).
"""
n = len(numer)
x_bar, y_bar = mean(numer), mean(denom)
var_x = sum((x - x_bar) ** 2 for x in numer) / (n - 1)
var_y = sum((y - y_bar) ** 2 for y in denom) / (n - 1)
cov = sum((x - x_bar) * (y - y_bar) for x, y in zip(numer, denom)) / (n - 1)
ratio = x_bar / y_bar
var_ratio = (var_x / y_bar ** 2
- 2 * x_bar * cov / y_bar ** 3
+ x_bar ** 2 * var_y / y_bar ** 4) / n
return ratio, var_ratio
def compare_ratio(num_a, den_a, num_b, den_b) -> tuple[float, tuple[float, float]]:
r_a, v_a = ratio_metric_var(num_a, den_a)
r_b, v_b = ratio_metric_var(num_b, den_b)
diff = r_b - r_a
se = math.sqrt(v_a + v_b)
return diff, (diff - 1.96 * se, diff + 1.96 * se)
Практическое следствие: если ваша аналитика считает CTR как «сумма кликов / сумма показов» и берёт биномиальную ошибку по показам — все её p-value неверны в опасную сторону. Проверьте это первым делом; это одна из самых распространённых ошибок в самописных экспериментальных отчётах.
7.2. Тяжёлые хвосты
Выручка на пользователя, время в приложении, число заказов — распределения с тяжёлым правым хвостом. Один кит, купивший на миллион, может в одиночку сделать «победу» варианта.
Что делают на практике:
- Винзоризация / клиппинг: обрезаем значения выше 99-го или 99.9-го перцентиля (порог считаем по объединённым данным обеих групп, иначе внесём смещение). Резко снижает дисперсию ценой небольшого смещения — почти всегда выгодный обмен.
- Бутстрап доверительных интервалов вместо нормальной аппроксимации.
- Дублирующая метрика: наряду со средним чеком смотрите долю платящих — она устойчива.
- Проверка чувствительности: пересчитайте результат без топ-10 пользователей. Если вывод перевернулся — у вас нет результата, у вас есть кит.
7.3. CUPED: как получить тот же вывод на вдвое меньшей выборке
Идея CUPED (Deng, Xu, Kohavi, Walker, Microsoft, 2013): большая часть дисперсии метрики объясняется тем, какими пользователи были до эксперимента. Активный пользователь и в контроле, и в тесте будет активнее среднего. Эту предсказуемую часть можно вычесть.
Берём ковариату X — ту же метрику за период до старта эксперимента (важно: до, иначе
ковариата сама зависит от воздействия и мы внесём смещение). Строим скорректированную метрику:
Y_cuped = Y − θ · (X − E[X]), где θ = Cov(Y, X) / Var(X)
Матожидание не меняется (оценка остаётся несмещённой), а дисперсия падает в (1 − ρ²) раз,
где ρ — корреляция Y и X. При ρ = 0.7 дисперсия падает на 51% — это эквивалентно
удвоению выборки бесплатно.
"""CUPED: снижение дисперсии за счёт предэкспериментальной ковариаты."""
import math
from statistics import mean
def cuped_adjust(y: list[float], x: list[float]) -> list[float]:
"""y — метрика в эксперименте, x — та же метрика ДО эксперимента."""
x_bar, y_bar = mean(x), mean(y)
n = len(y)
cov = sum((a - x_bar) * (b - y_bar) for a, b in zip(x, y)) / (n - 1)
var_x = sum((a - x_bar) ** 2 for a in x) / (n - 1)
theta = cov / var_x if var_x > 0 else 0.0
return [b - theta * (a - x_bar) for a, b in zip(x, y)]
def variance(v: list[float]) -> float:
m = mean(v)
return sum((a - m) ** 2 for a in v) / (len(v) - 1)
if __name__ == "__main__":
import random
random.seed(7)
# синтетика: метрика на 70% определяется прошлым поведением
pre = [max(0.0, random.gauss(10, 4)) for _ in range(20_000)]
post = [0.7 * p + 0.3 * random.gauss(10, 4) + random.gauss(0, 1) for p in pre]
adj = cuped_adjust(post, pre)
v0, v1 = variance(post), variance(adj)
print(f"дисперсия до CUPED: {v0:.3f}")
print(f"дисперсия после CUPED: {v1:.3f}")
print(f"снижение: {(1 - v1 / v0):.1%} "
f"→ эквивалент выборки в {v0 / v1:.2f}x больше")
print(f"среднее не поехало: {mean(post):.4f} vs {mean(adj):.4f}")
Ограничения, о которых стоит знать: CUPED требует истории пользователя, поэтому не работает для новых пользователей (у них нет «до») — а именно на новых часто и тестируют онбординг. Для них применяют другие ковариаты (страна, устройство, канал привлечения) через регрессию.
7.4. Множественные сравнения
Каждая дополнительная проверка при α = 0.05 добавляет шанс ложного открытия:
| Проверок | Вероятность хотя бы одного ложного «успеха» |
|---|---|
| 1 | 5% |
| 5 | 23% |
| 10 | 40% |
| 20 | 64% |
«Проверки» набегают незаметно: 4 варианта в тесте × 6 метрик × 5 сегментов = 120 сравнений, из которых ~6 будут «значимыми» на чистом шуме. Отсюда безумные отчёты вида «эффекта в целом нет, но в сегменте пользователей Android 12 из Новосибирска рост 14%, p = 0.03».
Что делать:
- Одна метрика решения (OEC), объявленная заранее. Это не поправка, это профилактика — и она эффективнее любой поправки.
- Bonferroni (
α/m) для небольшого числа guardrail-проверок: просто, консервативно. - Benjamini–Hochberg (контроль FDR) — когда сравнений много и вы согласны на долю ложных открытий среди отвергнутых, а не на ноль ложных.
- Сегменты — только для генерации гипотез, никогда для принятия решения. Найденное в сегменте проверяется отдельным экспериментом, специально нацеленным на этот сегмент.
"""Benjamini–Hochberg: контроль доли ложных открытий при множественных сравнениях."""
def benjamini_hochberg(p_values: list[float], q: float = 0.05) -> list[bool]:
"""Возвращает маску: какие гипотезы отвергаем при FDR <= q."""
m = len(p_values)
order = sorted(range(m), key=lambda i: p_values[i])
threshold_rank = -1
for rank, idx in enumerate(order, start=1):
if p_values[idx] <= q * rank / m:
threshold_rank = rank # берём НАИБОЛЬШИЙ подходящий ранг
result = [False] * m
for rank, idx in enumerate(order, start=1):
if rank <= threshold_rank:
result[idx] = True
return result
ps = [0.001, 0.008, 0.021, 0.031, 0.043, 0.28, 0.6, 0.71, 0.9, 0.95]
print(benjamini_hochberg(ps, q=0.05))
# [True, True, False, False, False, False, False, False, False, False]
# Bonferroni при m=10 отверг бы только p <= 0.005 — одну гипотезу;
# BH отвергает две, контролируя ожидаемую долю ошибок среди отвергнутых
7.5. Подглядывание (peeking) — самая частая порча результатов
Классический тест рассчитан на одну проверку в заранее назначенный момент. Если смотреть
на дашборд каждый день и останавливаться, как только появилось p < 0.05, фактическая
частота ложных срабатываний резко растёт. Симуляция A/A-теста (обе группы идентичны,
2000 прогонов, добавляем по 200 наблюдений на группу перед каждой проверкой):
| Число проверок | Доля ложных «побед» |
|---|---|
| 1 | 4.9% |
| 2 | 7.9% |
| 5 | 14.8% |
| 10 | 19.7% |
При ежедневном подглядывании в течение двух недель уровень ложных открытий превышает 25%. То есть каждый четвёртый ваш «успешный» эксперимент — шум, и вы честно раскатали его на всех пользователей.
Что с этим делать:
- Дисциплина. Фиксировать дату остановки заранее и не смотреть на OEC до неё. Смотреть на guardrails и на здоровье системы — можно и нужно.
- Групповые последовательные границы (O’Brien–Fleming, Pocock): заранее заданное число промежуточных проверок с ужесточёнными порогами.
- Always-valid inference / sequential testing: доверительные последовательности, корректные при непрерывном подглядывании. Подход популяризовали Optimizely (статья про Stats Engine) и Amplitude/Statsig. Плата — консервативность: чтобы получить право смотреть всегда, вы теряете мощность.
- Байесовский подход — «вероятность того, что B лучше A» интерпретируется проще и менее чувствителен к остановке, но не иммунен: при выборе момента остановки по апостериорной вероятности смещение всё равно возникает, просто иначе устроено. Хорошее введение — Bayesian A/B testing у VWO.
7.6. Парадокс Симпсона и проклятие победителя
Парадокс Симпсона. Вариант B может выигрывать в каждом сегменте, но проигрывать в целом (или наоборот), если доли сегментов в группах различаются. В A/B-тесте с корректной рандомизацией доли совпадают, поэтому парадокс — почти всегда симптом: SRM, разное время экспозиции (тест на B включили на день позже), разная скорость раскатки по платформам. Увидели Симпсона — идите проверять механику, а не «объяснять эффект».
Проклятие победителя (winner’s curse). Среди экспериментов, показавших значимый эффект, средний измеренный эффект систематически завышен — потому что через порог значимости проходят те, кому шум помог. Чем ниже мощность теста, тем сильнее завышение: при мощности 0.2 измеренный эффект может быть завышен вдвое-втрое.
Практические следствия:
- Не планируйте бизнес-показатели по точечной оценке эффекта из теста — берите нижнюю границу доверительного интервала.
- Сумма «приростов» из 30 успешных экспериментов за год никогда не сойдётся с фактическим ростом продукта. Это нормально, а не ошибка аналитики. Для сверки используют holdout — группу (1–5%), которая год не получает никаких новых фич; разница с ней даёт честный совокупный эффект.
8. Когда A/B невозможен
Контролируемый эксперимент требует много независимых юнитов. Часто их нет.
Мало трафика (B2B, нишевые продукты). Тысяча аккаунтов не даст мощности ни на что, кроме гигантских эффектов. Выход: мерить метрики ближе к изменению (не «выручка», а «дошёл до шага 3»), использовать качественные методы, пилоты с контрольной группой похожих клиентов, оценивать эффект на непрерывной метрике (она мощнее бинарной).
Интерференция (нарушение SUTVA). В соцсети друг из тестовой группы влияет на контрольного; в маркетплейсе водитель, отданный тестовому пассажиру, недоступен контрольному; в аукционе реклама конкурирует за одни и те же показы. Тогда обычный A/B систематически преувеличивает эффект (вы измеряете перераспределение, а не создание ценности).
Методы:
- Кластерная рандомизация — по городам, школам, компаниям, связным компонентам графа.
- Switchback — весь регион переключается между вариантами по временным интервалам; стандарт в доставке и такси (пример от Doordash).
- Бюджетное разделение в рекламных аукционах.
Изменение нельзя разделить. Ребрендинг, изменение цены на всём рынке, регуляторное требование, крупная миграция. Тогда — квазиэксперименты: difference-in-differences (сравнение динамики с похожим невоздействованным рынком), synthetic control (взвешенная комбинация контрольных регионов), прерванный временной ряд. Все они дают более слабый вывод и требуют явных допущений (главное — «параллельные тренды»).
Долгосрочные эффекты. Двухнедельный тест не измеряет, что будет через год. Инструменты: долгие holdout-группы, суррогатные метрики, повторные замеры.
разделить пользователей?"} B -->|"Нет: изменение общее"| C["Квазиэксперимент:
diff-in-diff, synthetic control"] B -->|"Да"| D{"Влияют ли пользователи
друг на друга?"} D -->|"Сильно: сеть, маркетплейс"| E{"Есть ли естественные кластеры?"} E -->|"Города, компании"| F["Кластерная рандомизация"] E -->|"Нет, всё связано"| G["Switchback по времени"] D -->|"Нет"| H{"Хватает ли юнитов
на нужный MDE?"} H -->|"Да"| I["Классический A/B
по user_id"] H -->|"Нет"| J{"Можно ли сдвинуть метрику
ближе к изменению?"} J -->|"Да"| K["A/B на прокси-метрике
+ качественная проверка"] J -->|"Нет"| L["Пилот с контрольной группой,
решение экспертно + мониторинг"]
9. Как это выглядит в проде
9.1. Платформа экспериментов
В зрелой компании эксперименты — инфраструктура, а не разовая работа аналитика. Минимальный состав:
- Сервис фича-флагов с детерминированным назначением, сегментацией, мгновенным выключением (kill switch) и постепенной раскаткой 1% → 5% → 25% → 50% → 100%.
- Единый лог экспозиций — таблица
(user_id, experiment, variant, ts)с дедупликацией. - Каталог метрик — метрики определены один раз в коде/SQL и переиспользуются всеми экспериментами. Без этого каждый аналитик считает «конверсию» по-своему.
- Автоматический отчёт: SRM, OEC, guardrails, доверительные интервалы, CUPED, разбивка по дням и стандартным сегментам — без ручного SQL.
- Реестр решений: что запустили, что решили, почему. Через год это единственный способ не проверять по третьему разу то, что уже проверяли.
Референсы: Microsoft ExP (десятки тысяч тестов в год), Netflix Experimentation Platform, Airbnb ERF, Statsig / GrowthBook как готовые решения.
9.2. Витрина для анализа
-- Базовый расчёт результата эксперимента из сырых событий.
-- Ключевые детали: дедупликация экспозиций, первая экспозиция как точка отсчёта,
-- метрика считается ТОЛЬКО после момента экспозиции (иначе утечка из прошлого).
WITH exposure AS (
SELECT
user_id,
variant,
MIN(event_ts) AS first_exposed_at
FROM events
WHERE event_name = 'experiment_exposure'
AND experiment = 'onboarding_import_v1'
AND event_ts >= TIMESTAMP '2026-06-01 00:00:00'
GROUP BY user_id, variant
),
-- пользователь, попавший в обе группы, — признак поломки; исключаем и считаем
clean AS (
SELECT user_id, MIN(variant) AS variant, MIN(first_exposed_at) AS exposed_at
FROM exposure
GROUP BY user_id
HAVING COUNT(DISTINCT variant) = 1
),
metric AS (
SELECT
c.user_id,
c.variant,
-- OEC: вернулся ли пользователь на 7-й день после экспозиции
MAX(CASE
WHEN e.event_name = 'session_start'
AND e.event_ts >= c.exposed_at + INTERVAL '6' DAY
AND e.event_ts < c.exposed_at + INTERVAL '8' DAY
THEN 1 ELSE 0
END) AS retained_d7,
-- ковариата для CUPED: активность за 14 дней ДО экспозиции
SUM(CASE
WHEN e.event_name = 'session_start'
AND e.event_ts < c.exposed_at
AND e.event_ts >= c.exposed_at - INTERVAL '14' DAY
THEN 1 ELSE 0
END) AS pre_sessions
FROM clean c
LEFT JOIN events e ON e.user_id = c.user_id
GROUP BY c.user_id, c.variant
)
SELECT
variant,
COUNT(*) AS users, -- сюда же SRM-проверка
AVG(retained_d7::float) AS retention_d7,
STDDEV_SAMP(retained_d7::float) AS sd,
AVG(pre_sessions::float) AS pre_activity, -- должна совпасть между группами
CORR(retained_d7::float, pre_sessions::float) AS rho -- потенциал CUPED
FROM metric
GROUP BY variant
ORDER BY variant;
Столбец pre_activity — бесплатная санитарная проверка: предэкспериментальные метрики
у групп обязаны совпадать. Если не совпадают — рандомизация или сбор данных сломаны, читать
результат нельзя. Столбец rho сразу показывает, даст ли CUPED выигрыш.
9.3. Ритуалы, которые реально работают
- Weekly experiment review: 30 минут, все завершённые тесты, решение по каждому в тот же день. Затягивание решений — главный источник «зомби-экспериментов», крутящихся месяцами.
- Каждый эксперимент имеет владельца и дату решения. Без даты тест живёт вечно.
- Празднуйте опровергнутые гипотезы. Отрицательный результат за две недели дешевле, чем квартал разработки. Если в команде «провалившийся тест» = «плохо поработали», вы получите подгонку данных, а не знание.
- Публичный реестр результатов с одной строкой на эксперимент: гипотеза, эффект, решение.
- Долгосрочный holdout 1–5% на квартал/год — единственный способ узнать реальный совокупный эффект работы команды.
10. Типичные ошибки
| Ошибка | Чем оборачивается | Что делать |
|---|---|---|
| MVP как «урезанный продукт» | Проверяем всё сразу, не узнаём ничего | Одна гипотеза — один минимальный артефакт |
| Горизонтальные срезы | 3 месяца без обратной связи | Только сквозные вертикальные инкременты |
| Гипотеза без числа и порога | Всегда «подтверждается» | Шаблон с метрикой, порогом и сроком |
| Критерий успеха придуман после результата | Систематический самообман | Предрегистрация плана до запуска |
| Не посчитан MDE | Тест физически не может дать ответ | Считать n до, а не после |
| Подглядывание и ранняя остановка | До 25% ложных «побед» | Фиксированная дата или sequential-методы |
| Игнорирование SRM | Смещённый результат читается как настоящий | Блокирующая SRM-проверка в отчёте |
| Логирование назначения вместо экспозиции | Разбавление эффекта, потеря мощности | Логировать факт показа, trigger-анализ |
| Ratio-метрика с биномиальной ошибкой | Заниженные p-value, ложные победы | Дельта-метод или бутстрап |
| Решение по сегменту, найденному постфактум | Ловля шума | Сегменты — только для гипотез, затем отдельный тест |
| Юнит рандомизации мельче юнита анализа | Эффект размывается, вывод некорректен | Рандомизация не мельче анализа |
| Тест длиной 3 дня | Ловим день недели и новизну | Минимум неделя, лучше две |
| Нет guardrails | Метрика выросла, продукт испортился | Список guardrails у каждого теста |
| Планирование выручки по точечной оценке | Прогноз не сходится с реальностью | Нижняя граница CI + holdout |
| A/B там, где юнитов нет | Вечные «неопределённые» результаты | Прокси-метрики, кластеры, switchback, квазиэксперименты |
11. Мини-итог
- MVP — это эксперимент, а не маленький продукт. Минимальность определяется гипотезой: минимально то, чего хватает, чтобы её опровергнуть.
- Резать нужно вертикально. Любой инкремент — работающий сквозной сценарий, пусть узкий.
- Гипотеза без метрики, порога, срока и механизма — не гипотеза. Критерий успеха фиксируется до запуска, вместе с решением на каждый исход.
- Рандомизация — единственная защита от неизвестных конфаундеров. Сравнение «до/после» и «выкатили и посмотрели» причинного вывода не дают.
- Считайте MDE до эксперимента.
n ∝ 1/Δ²: часто ответ — «этот тест невозможен», и узнать это лучше заранее. - Доверительный интервал информативнее p-value. «Значимо» без размера эффекта — бесполезное утверждение.
- Проверяйте механику раньше выводов: A/A, SRM, совпадение предэкспериментальных метрик.
- Не подглядывайте или используйте методы, которые это разрешают.
- Знайте, что ломается: ratio-метрики, тяжёлые хвосты, множественные сравнения, интерференция, проклятие победителя.
- Ценность процесса — в скорости обучения, а не в доле «побед». Команда, убивающая гипотезы за две недели, обыгрывает команду, угадывающую чуть чаще.
Источники
- Ron Kohavi, Diane Tang, Ya Xu. «Trustworthy Online Controlled Experiments: A Practical Guide to A/B Testing» — главная книга по теме, обязательна.
- Eric Ries. «The Lean Startup» — MVP и цикл Build–Measure–Learn.
- Alex Deng, Ya Xu, Ron Kohavi, Toby Walker. «Improving the Sensitivity of Online Controlled Experiments by Utilizing Pre-Experiment Data» — оригинальная статья про CUPED.
- Ron Kohavi et al. «Trustworthy Online Controlled Experiments: Five Puzzling Outcomes Explained».
- Ramesh Johari et al. «Peeking at A/B Tests: Why it matters, and what to do about it» — always-valid inference.
- Scott Cunningham. «Causal Inference: The Mixtape» — причинность, diff-in-diff, synthetic control; бесплатно.
- Henrik Kniberg. «Making sense of MVP».
- ASA Statement on p-Values and Statistical Significance.
- Microsoft Experimentation Platform — архив статей и практик.
- Netflix Tech Blog: Experimentation.
Что дальше
Эксперимент отвечает на вопрос «работает ли решение», но не подсказывает, каким это решение должно быть. Следующий шаг — научиться проектировать само решение: понимать путь пользователя, превращать инсайты в требования и описывать их так, чтобы команда построила именно то, что нужно.