new-lvl.pro · Статьи · A/B и эксперименты
Статья // 14 мин чтения

DiD без боли:
когда A/B нельзя — берём разность разностей

A/B-тест — лучший инструмент для измерения эффекта. Но иногда его просто нельзя запустить: региональный роллаут, B2B-контракты, сетевые эффекты, юридические ограничения. Разбираю Difference-in-Differences — метод, который даёт причинную оценку там, где рандомизация невозможна. С Python-кодом, тестом трендов и честным разбором граблей.

Когда A/B нельзя запустить

A/B-тест требует одного: возможности случайно разделить пользователей на группы до вмешательства. В теории — просто. На практике это часто невозможно, и вот почему.

Региональный роллаут
Новую цену запустили в Москве
Бизнес решил раскатить изменение на регион целиком. Рандомизировать внутри города нельзя — пользователи видят разные условия и жалуются.
Сетевые эффекты
Маркетплейс, соцсеть
Один пользователь в контроле взаимодействует с другим в тесте — SUTVA нарушено. Стандартный A/B покажет заниженный или искажённый эффект.
B2B и юрограничения
Нельзя по контракту
Корпоративный клиент подписал контракт на определённых условиях. Случайно применить к нему другие — юридически невозможно.

В таких ситуациях у аналитика два варианта: сказать «нельзя измерить» и уйти, или использовать квазиэкспериментальные методы. Difference-in-Differences (DiD) — один из самых надёжных из них и при этом один из самых понятных.

// Что такое квазиэксперимент
Эксперимент — мы сами рандомизируем. Квазиэксперимент — природа или бизнес-решение создало «почти эксперимент»: есть группа, которая получила воздействие, и группа, которая не получила. Мы не контролировали это разделение, но можем использовать его для оценки эффекта — если соблюдаются определённые допущения.

Суть DiD: разность разностей

Представь простую ситуацию. Компания изменила алгоритм ранжирования товаров в Москве, а в Петербурге оставила старый. Через три месяца ты хочешь оценить эффект на конверсию. Что можно сделать?

Плохой вариант — просто до-после в Москве. Конверсия выросла на 4%. Но одновременно началась весна, закончились новогодние праздники, конкурент поднял цены. Ты не знаешь, сколько из этих 4% — эффект алгоритма.

DiD вычитает общий тренд через контрольную группу. Смотришь, как изменилась конверсия в Петербурге за тот же период. Если там тоже +2% (сезонность, рынок) — то эффект алгоритма составляет 4% − 2% = 2%.

СОБЫТИЕ DiD Контроль (СПб) Лечение (МСК) контрфактуал До После
// Формула DiD
DiD = (Ytreat,after − Ytreat,before) − (Ycontrol,after − Ycontrol,before)

// В нашем примере
DiD = (МСКпосле − МСКдо) − (СПбпосле − СПбдо) = 4% − 2% = 2%

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

Тест параллельных трендов: как и зачем

DiD работает только при одном условии: в отсутствие вмешательства обе группы развивались бы параллельно. Это допущение параллельных трендов (parallel trends assumption). Проверить его напрямую нельзя — мы не знаем, что было бы с группой лечения без вмешательства. Но косвенно проверить можно.

// допущение
Parallel Trends Assumption
Без вмешательства метрика в группе лечения изменялась бы так же, как в контрольной группе. Формально: E[Ytreat,t(0) − Ycontrol,t(0)] постоянно во времени для всех t. Это нельзя проверить точно, но можно подтвердить косвенно через предпериодные данные.

Как проверить: визуально

Возьми данные до события. Нарисуй обе группы на одном графике. Если тренды идут параллельно (не пересекаются, не расходятся — движутся примерно с одинаковым наклоном) — допущение правдоподобно. Уровни могут быть разными, важен наклон, а не совпадение значений.

Как проверить: статистически

Построй регрессию только на предпериодных данных с interaction-термом «время × группа»:

Python тест параллельных трендов на предпериоде
# Данные только до события
pre = df[df['period'] == 'before'].copy()

# Линейный тренд × группа лечения
pre['time_trend'] = pre['month']  # порядковый номер месяца
pre['interaction'] = pre['treated'] * pre['time_trend']

model_pre = smf.ols(
    'conversion ~ treated + time_trend + interaction',
    data=pre
).fit()

# Если coef(interaction) ≈ 0 и p-value > 0.05 → тренды параллельны
print(model_pre.summary())

Если коэффициент при interaction близок к нулю и статистически незначим — тренды параллельны и ты можешь доверять DiD. Если коэффициент значим — группы уже расходились до события, и оценка будет смещённой.

// Что делать если тренды не параллельны
Три опции: (1) выбрать другую контрольную группу, более похожую на лечение; (2) перейти к методу Propensity Score Matching для выравнивания групп перед DiD; (3) использовать синтетический контроль (см. ниже). Продолжать с нарушенным допущением — значит сообщать смещённые оценки, что хуже, чем «не знаю».

Пример на Python: два региона, до и после

Задача: компания изменила политику бесплатной доставки в Москве (от 1000 руб. вместо 2000 руб.). В Екатеринбурге ничего не менялось. Оцениваем эффект на конверсию в заказ за 6 месяцев после.

Python подготовка данных
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf

# Создаём синтетический датасет
# В реальной задаче — загрузить из базы
np.random.seed(42)

n = 12  # 12 месяцев: 6 до + 6 после
months = pd.date_range('2024-01', periods=n, freq='MS')
event_month = pd.Timestamp('2024-07')

moscow = pd.DataFrame({
    'month': months,
    'city': 'moscow',
    'treated': 1,
    'post': (months >= event_month).astype(int),
    # базовый тренд + эффект лечения 0.025 после события
    'conversion': 0.12 + np.arange(n) * 0.001
                  + (months >= event_month) * 0.025
                  + np.random.normal(0, 0.003, n)
})

ekb = pd.DataFrame({
    'month': months,
    'city': 'ekb',
    'treated': 0,
    'post': (months >= event_month).astype(int),
    # тот же базовый тренд, без эффекта лечения
    'conversion': 0.10 + np.arange(n) * 0.001
                  + np.random.normal(0, 0.003, n)
})

df = pd.concat([moscow, ekb], ignore_index=True)
# interaction-терм: treated × post — сердце DiD
df['did'] = df['treated'] * df['post']
Python OLS-регрессия с DiD interaction-термом
# Базовая DiD-регрессия
# Y = α + β₁·treated + β₂·post + β₃·(treated×post) + ε
# β₃ — это и есть наша DiD-оценка эффекта
model = smf.ols('conversion ~ treated + post + did', data=df).fit()
print(model.summary())

# Итоговая оценка
did_estimate = model.params['did']
conf_int = model.conf_int().loc['did']

print(f"\nDiD-оценка эффекта: {did_estimate:.4f}")
print(f"95% CI: [{conf_int[0]:.4f}, {conf_int[1]:.4f}]")
print(f"p-value: {model.pvalues['did']:.4f}")
Output что получим
# Ожидаемый вывод (приблизительно):
DiD-оценка эффекта: 0.0248
95% CI: [0.0168, 0.0328]
p-value: 0.0001

# Интерпретация:
# Снижение порога бесплатной доставки увеличило конверсию
# в Москве на ~2.5 п.п. (от базы ~12% → ~14.5%)
# относительно контрфактуального тренда из Екатеринбурга.
# Эффект статистически значим (p < 0.001).

Коэффициент при did — это и есть оценка причинного эффекта. β₁ (treated) захватывает исходную разницу между регионами. β₂ (post) — общий временной тренд, одинаковый для обоих. β₃ (did = treated × post) — то, что изменилось в группе лечения сверх общего тренда. Это и есть DiD.

// Когда добавлять FE (fixed effects)
В базовом примере выше — два региона. В реальных задачах регионов может быть много и на разных уровнях (город, сегмент, когорта). Тогда используют TWFE (Two-Way Fixed Effects): фиксируем и эффекты единиц (регионов), и временные эффекты. В statsmodels добавляй C(city) + C(month) в формулу. Это убирает неизменяемые различия между группами и общие временные шоки.

Где DiD ломается

DiD — мощный инструмент, но не серебряная пуля. Вот четыре ситуации, когда он даёт смещённую оценку.

// проблема 01
Нарушение параллельных трендов
Группы уже расходились до события. Москва росла быстрее Екатеринбурга, потому что туда пришёл новый маркетинговый канал. Тогда часть «эффекта доставки» — это просто продолжение предпериодного расхождения. DiD переоценит эффект.
строй тест параллельных трендов (см. выше). Если нарушено — ищи более похожую контрольную группу или переходи к синтетическому контролю.
// проблема 02
Spillover — перетекание эффекта
Пользователи из Москвы рассказали петербургским друзьям об изменившихся условиях доставки. Петербуржцы стали меньше заказывать (ждут, когда и у них снизят порог). Тогда контрольная группа «заражена» эффектом лечения, и DiD завысит реальный эффект.
выбирай контрольные единицы, которые физически или поведенчески изолированы от лечения. В геовариантах — контроль в регионах, которые не граничат с лечением.
// проблема 03
Anticipation Effect — предвосхищение
Группа лечения узнала об изменении до его запуска. Покупатели в Москве начали откладывать крупные заказы ещё в апреле, зная, что в июле доставка подешевеет. Метрика в «предпериоде» уже изменилась — и тест трендов ничего не покажет, потому что изменение пошло плавно.
добавляй в модель «окна предвосхищения» — дополнительные interaction-термы для нескольких периодов до события. Это так называемый event study design.
// проблема 04
Изменение состава выборки
После события в Москву стали активнее приходить новые пользователи, привлечённые рекламой бесплатной доставки. Состав аудитории изменился. Конверсия выросла, но часть роста — за счёт изменения профиля, а не самого изменения условий. DiD смешает эти два эффекта.
ограничь выборку только «постоянными» пользователями, которые были активны и до и после. Или считай отдельно для старых и новых когорт.

Синтетический контроль: когда один регион без пары

Что делать, если у тебя один регион лечения и нет подходящей контрольной группы? Москва — это Москва: экономика другая, потребительское поведение другое, сезонность другая. Ни один регион не подходит как контроль.

В этом случае работает Synthetic Control Method (Абади и Гарадасабал, 2003). Идея: вместо реального контроля строим синтетический — взвешенную комбинацию нескольких потенциальных контролей, которая наилучшим образом воспроизводит предпериодную динамику группы лечения.

DiD Synthetic Control
Контроль Один или несколько реальных регионов Взвешенная сумма регионов-доноров
Когда применять Есть похожая контрольная группа, тренды параллельны Нет подходящего контроля; один уникальный регион лечения
Ограничения Параллельные тренды обязательны Нужно ≥5–7 потенциальных доноров; только агрегированные данные
Значимость Стандартные p-value через регрессию Permutation inference (placebo tests)
Python синтетический контроль — концептуальный скетч
# Концептуальный скетч (не production-код)
# Для полной реализации — библиотека SyntheticControlMethods
from scipy.optimize import minimize

# donors — матрица предпериодных метрик по регионам-донорам
# treatment_pre — вектор метрики группы лечения в предпериоде

def objective(weights):
    """Минимизируем расстояние между синтетическим и реальным трендом лечения"""
    synthetic = donors.T @ weights
    return np.sum((treatment_pre - synthetic) ** 2)

# Ограничения: веса неотрицательны и в сумме = 1
constraints = {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
bounds = [(0, 1)] * donors.shape[1]
w0 = np.ones(donors.shape[1]) / donors.shape[1]

result = minimize(objective, w0, method='SLSQP',
                   bounds=bounds, constraints=constraints)
optimal_weights = result.x

# Синтетический контрфактуал в постпериоде
synthetic_post = donors_post.T @ optimal_weights
did_synth = treatment_post - synthetic_post
print(f"Синтетический DiD: {did_synth.mean():.4f}")

Если хочешь готовую реализацию — смотри библиотеки SyntheticControlMethods (Python) или Synth (R). Проверка значимости в синтетическом контроле делается через placebo tests: применяешь тот же алгоритм к каждому региону-донору и смотришь, насколько уникален эффект лечения по сравнению с «ненастоящими» лечениями.

A/B на маркетплейсе: когда DiD лучше, чем обычный тест
В статье про A/B на маркетплейсе — про network effects, каннибализацию и SUTVA. DiD и гео-эксперименты часто оказываются единственным выходом.
Читать

Частые вопросы

Что такое Difference-in-Differences?
DiD — квазиэкспериментальный метод оценки причинного эффекта без рандомизации. Сравниваем изменение метрики в группе лечения (до и после) с изменением в контрольной группе за тот же период. Разность двух изменений — DiD-оценка. Метод вычитает общие тренды через контрольную группу, которая не получила вмешательство.
Чем DiD отличается от простого «до-после»?
Простое до-после не контролирует внешние факторы: сезонность, рыночный тренд, действия конкурентов. Если метрика выросла — неизвестно, сколько это эффект вмешательства, а сколько — общий тренд рынка. DiD вычитает этот тренд через контрольную группу, получая более надёжную причинную оценку.
Как проверить параллельные тренды?
Визуально: нарисуй обе группы на предпериодных данных — тренды должны идти параллельно (одинаковый наклон, не обязательно одинаковый уровень). Статистически: регрессия на предпериоде с interaction-термом «время × группа» — коэффициент должен быть близок к нулю и незначим (p > 0.05).
Когда DiD лучше, чем PSM?
DiD лучше PSM, когда у тебя временная структура данных (до и после события) и ты можешь проверить параллельность трендов. PSM лучше DiD, когда нет временной структуры — только один момент времени, — и нужно выровнять наблюдаемые ковариаты между группами. Нередко методы комбинируют: PSM для выравнивания групп + DiD для оценки эффекта.
Сколько нужно периодов для DiD?
Минимум — один период до и один после. Но чем больше предпериодных данных, тем надёжнее тест параллельных трендов. Хорошая практика: 3–6 периодов до события. Постпериод — зависит от задачи: нужно дождаться, пока эффект стабилизируется (novelty effect затихнет).
Что такое TWFE и когда он нужен?
Two-Way Fixed Effects — расширение DiD с фиксированными эффектами и для единиц наблюдения (регионов, пользователей), и для временных периодов. TWFE используют, когда единиц много, у каждой свой неизменяемый «фон», и нужно очистить оценку от неизменяемых различий между единицами и общих временных шоков. В Python — C(city) + C(month) в OLS или библиотека linearmodels с PanelOLS.

Связанные материалы

Главное про DiD

DiD — не замена A/B, а инструмент на случай, когда A/B физически невозможен. Он работает при одном ключевом условии: параллельные тренды в предпериоде. Без этой проверки оценка ничего не стоит.

Базовый рецепт: данные по двум группам × два периода → регрессия с interaction-термом treated × post → коэффициент β₃ — твой DiD. Если групп много — TWFE. Если контрольной группы нет совсем — синтетический контроль.

Следующий уровень — event study design: когда вместо одной оценки строишь временной ряд эффектов по каждому периоду. Это позволяет увидеть предпериодные тренды и постпериодную динамику в одном графике.

АТ
Андрей Тарасенко
// Продуктовый аналитик · Авито · Ментор

DiD появился у меня в практике именно там, где A/B не влезал: региональные роллауты ценовых изменений и оценка эффектов, которые нельзя рандомизировать по юридическим или бизнес-причинам. Главная ловушка — пропустить тест параллельных трендов, потому что «и так понятно, что регионы похожи». Почти всегда это стоит смещённой оценки.

Написать в Telegram
// ПРОВЕРЬ СЕБЯ

Разобрался? Проверь на квизе

15 ловушек A/B-тестов в формате квиза — peeking, SRM, ratio-метрики, novelty effect. А потом закрепи в SQL-тренажёре.

▶ Квиз: ловушки A/B ▶ SQL-тренажёр
Все материалы: База знаний · Telegram