Regression discontinuity простыми словами
Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Зачем это знать
Бонусная программа включается при score ≥ 70. Те, кто 69, не получают, у 71 — получают. Почти identical users, разное treatment. Это natural experiment — regression discontinuity (RDD).
В банковской, страховой, fintech аналитике RDD встречается постоянно: cutoffs для кредитов, тарифов, offers. Знание RDD — мощный инструмент для middle+.
Короткое объяснение
RDD измеряет эффект воздействия, которое применяется к unit на основе их score относительно cutoff.
Сравниваем units «чуть выше» и «чуть ниже» threshold → они почти identical, кроме treatment.
Пример
Кредитный бонус даётся клиентам со score ≥ 700.
- Score 699: бонуса нет → LTV = $200
- Score 701: бонус есть → LTV = $280
Разница $80 — эффект бонуса, если верить в RDD.
Sharp vs Fuzzy
Sharp RDD
Treatment применяется всегда и только при score ≥ cutoff.
Все 701+ получают бонус, никто ниже.
Fuzzy RDD
Вероятность treatment растёт на cutoff, но не на 100%.
Например, 80% клиентов со score ≥ 700 выбирают бонус, 20% — нет.
Требует instrumental variable approach.
Assumption
Continuity
Все остальные факторы (кроме treatment) — continuous функции running variable около cutoff.
Другими словами: без cutoff LTV(score) был бы smooth function.
No manipulation
Units не могут precision-контролировать свой score, чтобы попасть в/выше cutoff.
Если клиенты могут «накачать» score до 701 — RDD сломан.
В формуле
Y = α + β × Treatment + f(score) + εГде f(score) — smooth function score (polynomial или kernel), а β — эффект treatment.
Визуально
График: X-axis — running variable, Y-axis — outcome.
- До cutoff: smooth trend
- На cutoff: jump (discontinuity)
- После cutoff: smooth trend
Размер jump = effect.
В Python
import statsmodels.api as sm
import numpy as np
# Near cutoff (bandwidth 30)
data_near = data[abs(data['score'] - 700) < 30]
data_near['treatment'] = (data_near['score'] >= 700).astype(int)
data_near['score_centered'] = data_near['score'] - 700
# Linear с interaction
X = sm.add_constant(data_near[['treatment', 'score_centered']])
X['interact'] = X['treatment'] * X['score_centered']
model = sm.OLS(data_near['outcome'], X).fit()
# treatment coefficient = effectИли специальный пакет:
import rdrobust
rdrobust.rdplot(y=data['y'], x=data['score'], c=700)Bandwidth
Ключевое решение: как близко к cutoff смотреть.
- Слишком wide: bias (units слишком разные)
- Слишком narrow: variance (мало данных)
Оптимум — через cross-validation или optimal bandwidth formulas (Imbens-Kalyanaraman).
Когда использовать
- Есть sharp cutoff в процессе
- Running variable continuous
- Около cutoff достаточно данных
Классические кейсы
- Кредитные ограничения
- Минимальный балл (admissions)
- Возрастные cutoffs (pension, legal drinking)
- Политические partitions (close elections)
Pitfalls
Manipulation
Если units могут manipulate score — тест McCrary для density около cutoff. Если density jump есть → manipulation.
Bandwidth sensitivity
Результат может сильно меняться от bandwidth. Robustness checks нужны.
Multiple cutoffs
Если есть несколько cutoffs — усложняется.
На собесе
«Что такое RDD?» Метод измерения эффекта от treatment на основе cutoff running variable.
«Sharp vs fuzzy?» Sharp — 100% treatment выше cutoff, fuzzy — probability.
«Assumption?» Continuity и no manipulation.
«Когда использовать?» Когда есть sharp cutoff и нельзя A/B.
Частые ошибки
Игнорировать manipulation
McCrary test обязательно.
Слишком wide bandwidth
Сравниваются units, не похожие друг на друга.
Wrong functional form
Если f(score) на самом деле cubic, а вы fit linear — bias.
Связанные темы
FAQ
В A/B есть?
Нет. RDD — когда A/B нельзя сделать.
Сложно?
Концепция проста, но детали (bandwidth, polynomial order) требуют care.
External validity?
Эффект на границе cutoff может отличаться от среднего effect в популяции.
Тренируйте causal — откройте тренажёр с 1500+ вопросами для собесов.