Как посчитать DiD в SQL
Содержание:
Зачем DiD
Difference-in-differences (DiD) — самый популярный метод causal inference, когда A/B-эксперимент невозможен. Идея: сравнить изменение метрики у «обработанной» группы (получили feature, изменение цены, маркетинг) с изменением у контрольной — за один и тот же период. Эффект = разница в дельтах.
Таблица 2×2
| Группа | До (pre) | После (post) | Δ |
|---|---|---|---|
| Treated | A | B | B − A |
| Control | C | D | D − C |
DiD = (B − A) − (D − C).
Это и эффект, если treated и control имеют параллельный тренд до intervention.
DiD в SQL
WITH base AS (
SELECT
user_id,
is_treated,
SUM(amount) FILTER (WHERE order_date BETWEEN '2026-03-01' AND '2026-03-31') AS revenue_pre,
SUM(amount) FILTER (WHERE order_date BETWEEN '2026-04-15' AND '2026-05-14') AS revenue_post
FROM users u
LEFT JOIN orders o USING (user_id)
GROUP BY user_id, is_treated
)
SELECT
is_treated,
AVG(revenue_pre) AS mean_pre,
AVG(revenue_post) AS mean_post,
AVG(revenue_post) - AVG(revenue_pre) AS delta
FROM base
GROUP BY is_treated;Получили 2×2. Эффект:
WITH did_cells AS (
-- из предыдущего CTE
SELECT is_treated, AVG(revenue_pre) AS mean_pre, AVG(revenue_post) AS mean_post FROM base
GROUP BY is_treated
)
SELECT
(MAX(CASE WHEN is_treated THEN mean_post END) - MAX(CASE WHEN is_treated THEN mean_pre END))
- (MAX(CASE WHEN NOT is_treated THEN mean_post END) - MAX(CASE WHEN NOT is_treated THEN mean_pre END))
AS did_estimate
FROM did_cells;Параллельные тренды
DiD работает только если treated и control двигались параллельно до intervention. Проверка — построить serial trends:
SELECT
DATE_TRUNC('week', order_date)::DATE AS week,
is_treated,
AVG(amount) AS avg_revenue
FROM users u
JOIN orders o USING (user_id)
WHERE order_date BETWEEN '2025-12-01' AND '2026-03-31' -- pre-period
GROUP BY DATE_TRUNC('week', order_date), is_treated
ORDER BY week, is_treated;Графически в pre-period линии должны быть параллельны. Расхождение — нарушение DiD assumption.
Через регрессию
DiD как OLS:
y = α + β1 × treated + β2 × post + β3 × treated × post + εβ3 — DiD estimate. В SQL делаем «руками»:
SELECT
AVG(CASE WHEN NOT is_treated AND NOT is_post THEN amount END) AS intercept,
AVG(CASE WHEN is_treated AND NOT is_post THEN amount END)
- AVG(CASE WHEN NOT is_treated AND NOT is_post THEN amount END) AS beta_treated,
AVG(CASE WHEN NOT is_treated AND is_post THEN amount END)
- AVG(CASE WHEN NOT is_treated AND NOT is_post THEN amount END) AS beta_post,
(AVG(CASE WHEN is_treated AND is_post THEN amount END)
- AVG(CASE WHEN is_treated AND NOT is_post THEN amount END))
- (AVG(CASE WHEN NOT is_treated AND is_post THEN amount END)
- AVG(CASE WHEN NOT is_treated AND NOT is_post THEN amount END)) AS beta_did
FROM observations;Для CI и стандартных ошибок — лучше OLS в Python (statsmodels.formula.api.ols).
Частые ошибки
Ошибка 1. Нарушенные параллельные тренды. Если до intervention тренды расходились, DiD приписывает разницу intervention. Графический pre-period check — обязательно.
Ошибка 2. Selection bias в treated. Если treated — это «top customers, кому решили дать feature» — они и так выросли бы. DiD не помогает без random assignment.
Ошибка 3. Малая control group. Слишком маленький control дисперсия раздувает CI. Минимум 100+ юзеров.
Ошибка 4. Внешний шок в post-period. Если в post интервал произошёл рынок-wide event (COVID, праздник), DiD улавливает его в treated × post.
Ошибка 5. Считать на больших covariates без adjustments. Если treated и control различаются по age/plan/region — нужны fixed effects или controls в регрессии.
Связанные темы
- Как посчитать t-test в SQL
- Как посчитать propensity score matching в SQL
- Difference-in-differences метод
- Как посчитать conversion uplift в SQL
FAQ
Когда DiD, когда A/B?
A/B — если можете рандомизировать. DiD — когда нельзя (политика, маркетинг по всем).
Параллельные тренды — что если нарушены?
Считаете synthetic control или event study. DiD будет biased.
DiD на 2 точках?
Минимум. Лучше много pre/post точек для устойчивости.
Можно ли DiD с непрерывным treatment?
Да, через continuous interaction term. Усложняется.
CI для DiD?
Через bootstrap или OLS standard errors. В чистом SQL — bootstrap.