Привет! Меня зовут Паша, я маркетинговый аналитик в Купере. В этой статье речь пойдет о проверке качества «каузальных» моделей. На примере такой модели, как Double Machine Learning разберемся, откуда вообще берутся «каузальные» предсказания, как понять, что им можно доверять, и что делать с фундаментальной проблемой «скрытых конфаундеров».

Немного о «каузальных» моделях
Аналитика регулярно сталкивается с задачами оценки влияния некоего воздействия D на метрику Y. Например, для нашей команды, эти вопросы выглядят так:
Сколько инкрементального GMV (Y) принесло промо (D) для ретейлера?
Повлияла ли акция с кешбеком (D) на GMV ретейлера (Y)?
Есть ли зависимость между количеством товаров, предложенных в товарной подборке конкретному пользователю (D), и вероятностью покупки им товаров из этой подборки (Y)?

Эксперимент как золотой стандарт
Эксперимент — эталонный подход к подобным задачам: мы случайно делим пользователей на группы, меняем только D и смотрим, как «сдвинулся» Y. Например, части пользователей даем промо, а части — нет, и сравниваем ARPU в обеих группах. Так, мы почти полностью защищаемся от большей части возможных смещений.
Тем не менее в реальной работе мы регулярно попадаем в ситуации, где A/B-тест невозможен или ограничен. Самый популярный сценарий — запрос на оценку инкрементальности маркетинговой активности в условиях, когда ретейлер не готов терять деньги на контрольной группе.
Обсервационные данные и скрытые смещения
Представим, что промо был запущен без A/B-теста и нам нужно оценить влияние его запуска на ARPU в ретейлере (пусть это будет целевая метрика Y). Основная сложность заключается в том, что мы не можем просто сравнить метрики пользователей, которые воспользовались промо (пусть это будет D = 1), с метриками тех, кто проигнорировали его (пусть это будет D = 0).
Причина отражена в следующем графе:

Если мы не распределяли D случайно, то на его проявление будут влиять внешние переменные X, влияющие одновременно и на Y. Формальное название таких переменных — конфаундеры. Если пользователь воспользовался промо (D = 1), он мог это сделать, например, потому, что у него была уже устоявшаяся история взаимодействия с ретейлером (X), значит, он по умолчанию заплатит больше денег ретейлеру, чем те, кто промо не воспользовался (D = 0).
Следовательно, наблюдаемая в Revenue разница между двумя такими группами — это не эффект от промо, а эффект от промо плюс историческая связь пользователя с ретейлером (а также плюс иные переменные X, которые мы можем придумать для кейса).
Что такое «каузальные» модели — общий взгляд
Под «каузальными» моделями мы понимаем набор подходов, позволяющих выявлять эффект некоего воздействия D на целевую метрику Y, игнорируя вмешательство потенциальных внешних переменных X.
Выбор конкретного подхода во многом зависит от поставленной задачи, условий, имеющихся данных и единиц наблюдения:
если под воздействие попали отдельные магазины или города — можем сформировать «синтетический контроль» на основе других подобных сущностей;
если это большое общесервисное воздействие, никак не связанное, например, с объемом трафика, — можем использовать Causal Impact с DAU в качестве ковариаты;
если у нас есть «инструментальная» переменная, то есть и переменная, влияющая на фактор воздействия, но никак не сказывающаяся на целевой метрике, — можем обратиться к одноименному методу.
Далее предлагаю рассмотреть сценарий с поюзерными данными с Y, D и набором метрик X, через призму возможностей «каузальной» модели Double Machine Learning (DML). Такой подход является для нас наиболее естественным, позволяя сводить различные виды маркетинговых воздействий: промокоды, акции, скидки, подборки, полки и прочее.
Что такое DML — специфика модели
Нам нравится думать о базовой реализации этого метода, как о модели машинного обучения для предсказания Y на основе X и D, с возможностью «вытащить» из этой модели коэффициент влияния, который она приписала D.
ML-модели хорошо справляются с моделированием сложных связей между переменными в данных с высокой размерностью, но они слабо интерпретируемы в том плане, что не позволяют напрямую получить нужный коэффициент так же легко, как в линейной регрессии. Здесь используется небольшой трюк, для которого нужно построить две ML-модели (отсюда и Double, D в названии метода).
Алгоритм подхода
Строим M1 — модель машинного обучения для предсказания Y на основе X.
Строим M2 — модель машинного обучения для предсказания D на основе X.
Считаем остатки у M1 и M2 (фактические значения Y и D минус предсказанные моделями значения).
Оцениваем влияние остатков M2 на остатки M1 через обычную линейную регрессию — это и есть «вытянутый» из модели эффект D на Y. Помимо точечной оценки, как и на любом коэффициенте линейной регрессии, мы получаем доверительный интервал эффекта и p-value, чтобы понимать статистическую значимость эффекта.
Почему это работает? Формально объяснение базируется на теореме Фриша–Во–Ловелла: остатки M1 и M2 — это то, что остается в Y и D после удаления влияния X. Таким образом, связь между этими остатками — это изолированная от X связь Y и D.
В коде мы можем это реализовать через инструмент, предложенный самими авторами в библиотеке doubleml:
import doubleml as dml #приводим данные к формату, с которым работает библиотека, указывая наши Y, D, и X dml_data = dml.DoubleMLData(df, 'Y', 'D', x_cols) #задаем ML-модели, которые хотим использовать для обучения M1 и M2 m_1 = LGBMRegressor(n_estimators=n_estimators, learning_rate = 0.05, verbose=-1) m_2 = LGBMClassifier(n_estimators=n_estimators, learning_rate = 0.05, verbose=-1) #запускаем алгоритм dml_obj = dml.DoubleMLPLR(dml_data, ml_l = m_1, ml_m = m_2, n_folds = 5, n_rep = 2) dml_obj.fit() #смотрим оценку и доверительный интервал dml_obj.summary.round(3)
Симуляции
Базовый метод проверки того, что метод вообще работает — это проверка его на симуляциях. При построении симуляций мы точно понимаем: закладываем ли эффект (например, увеличение среднего чека при покупке с промокодом) или нет, и в каком размере. Следовательно, при многократной генерации симуляций и «прогонке» метода на них, мы можем оценивать процент ложноположительных и истинных «прокрасов».
Отличие индустриальной валидации от научной — нам не нужно генерировать данные и строить симуляции с нуля. Вместо этого мы можем использовать наши актуальные данные.
Ошибка I рода: A/A-тесты
Мы выгружаем данные о наших пользователях за выбранный период с определенным набором метрик для этих пользо��ателей.
В качестве Y — например, GMV пользователя в ретейлере за выбранный период. В качестве X:
GMV в сервисе/ретейлере на предпериоде;
количество заказов в сервисе/ретейлере на предпериоде;
средний чек в сервисе/ретейлере на предпериоде;
операционная система;
город и т. д.
Следующий шаг — создаем колонку D, которую случайно заполняем 1 и 0 — «фейковыми» признаками воздействия на пользователя. Применение DML на таком наборе данных ожидаемо должно показать отсутствие эффекта.
Тем не менее, чтобы оценить процент случаев, когда DML все-таки показывает наличие эффекта, мы должны повторить этот алгоритм многократно. Фактически, это будет ошибкой первого рода, или вероятностью найти эффект там, где его нет.
На наших данных, например, при α = 5% (мы смотрели, попадает ли 0 в 95%-ный доверительный интервал оценки) DML показал вероятность ложноположительного прокраса 5%.
Мощность: закладываем искусственный эффект
Вторая задача — это оценить мощность метода, или как часто он находит эффект, если тот есть.
Для этого мы должны в наши данные заложить эффект, но есть нюанс. Случайно распределить значения D и увеличить значение метрики Y на некоторый процент в группе c D = 1, мы не можем. Вернемся к графу на рис. 1

Что мы видим, — D распределяется неслучайно, оно зависит от X. Следовательно, необходимо заложить в процесс наполнения колонки D зависимость от X.
Пример функции, с которой можно это сделать:
import numpy as np import pandas as pd def sigmoid(z): return 1 / (1 + np.exp(-z)) def assign_treatment(df: pd.DataFrame, treated_frac=0.05, seed=42): rng = np.random.default_rng(seed) # условные конфаундеры x1 = np.log1p(df["prev_retailer_gmv"].fillna(0)) x2 = np.log1p(df["prev_retailer_orders"].fillna(0)) x3 = np.log1p(df["prev_retailer_promo_costs"].fillna(0)) # влияние конфаундеров на вероятность попасть в D = 1 score = ( 1.2 * x1 + 1.0 * x2 - 0.6 * x3 + rng.normal(0, 0.7, size=len(df)) ) # преобразование полученного скора в вероятность через сигмоидную функцию p = sigmoid(score) df["ps_sim"] = p # оставляем только treated_frac % пользователей, якобы попавших под воздействие cutoff = np.quantile(p, 1 - treated_frac) t = (p >= cutoff).astype(int) df["D"] = t return df
Теперь мы можем закладывать какой-то эффект для нашей метрики. Если он неизвестен, его удобно рассчитывать как процент от стандартного отклонения метрики и руководствоваться «rule of thumb»:
20% от стандартного отклонения — маленький эффект, 50% — средний, 80% — большой. Так, мы сможем оценивать, насколько хорошо наш метод «видит» эффект при разных размерах.
На наших данных с небольшим заложенным эффектом метод показал мощность почти 90%! Казалось бы, это отличный результат: проблема в том, что он во многом обусловлен принципиальным ограничением симуляций.
Скрытые конфаундеры
В чем проблема симуляций? Это условия, искусственно созданные нами, и не факт, что они повторяют реальность — как минимум потому, что мы сами не до конца понимаем, как реальность устроена. Фундаментальная проблема в этом смысле — это «скрытые конфаундеры».
До сих пор мы исходили из предпосылки, что знаем все X, которые влияют на D и Y. Но ведь мы не можем учесть все! В лучшем случае мы просто можем забыть добавить какую-нибудь переменную, которая фактически влияет на D и Y, в модель, в худшем — это «скрытая» переменная, которую мы технически никак не можем измерить. Например, лояльность пользователя к ретейлеру или промочувствительность пользователя — мы можем придумать прокси-метрики для этих характеристик, но это будет не исчерпывающее их представление.
Снова посмотрим на показанный ранее граф. Мы можем считать, что у нас появилась дополнительная переменная U, которой мы в данных не видим, а следовательно, применяя «каузальную» модель для оценки влияния D на Y при условии X, все равно получаем смещение из-за неучтенного U:

Соответственно, симуляции покажут качество метода, но, при условии, что наши предпосылки верны и все данные, нужные для ML-модели, есть в нашем распоряжении. Другими словами, «симулированные» условия могут быть просто удобными для метода, и он будет показывать высокую мощность. На практике же — предпосылки, особенно об отсутствии скрытых конфаундеров, легко нарушаются.
Анализ чувствительности
Сами авторы фреймворка DML предлагают гибкую систему оценки эффекта с поправкой на «скрытые конфаундеры» — называя sensitivity analysis, или анализом чувствительности.
Если мы строим модель с учетом наблюдаемых X, но без учета U, то получаем «сокращенную» оценку (short parameter). Однако, если бы нам пришлось строить модель с учетом U, мы бы получили «полную» оценку (long parameter). Разница между полной и сокращенной оценками — это «смещение» (bias), которое мы бы хотели учитывать. Авторы предлагают следующую формулу для оценки этого смещения:
В ней для нас пока важны только параметры посередине — и
. Остальные значения можно пока оставить вне зоны контроля.
Формально и
определяются следующим образом:
— это доля остаточной дисперсии для Y, которую потенциально объясняет неучтенный «скрытый» конфаундер U после учёта X.
— доля остаточной вариации Riesz representer (D с inverse-probability весами), которую потенциально объясняет неучтенный «скрытый» конфаундер U после учёта X.
Проще говоря: оба параметра показывают, насколько серьезный (с точки зрения влияния на Y и D) конфаундер U был упущен.
Если мы способны задать эти параметры, значит, можем подсчитать смещение (bias) и вычесть его из оценки, которую дает оценщик — как из точечной оценки, так и из границ доверительного интервала, чтобы понимать, по-прежнему ли наш доверительный интервал не покрывает собой 0?
Посмотрите, как это делается на примере с DML. Если DML-объект уже есть и нужно, например, посмотреть на скалиброванную оценку эффекта при =
= 10% (то есть «силу» влияния скрытых конфаундеров на Y и D мы оцениваем на уровне 10%), можно сделать это в две строки:
dml_obj.sensitivity_analysis(cf_y = 0.1, cf_d = 0.1) print(dml_obj.sensitivity_summary)
Как понять, какие значения выставлять для =
? 10% — это много или мало? Авторы системы предлагают использовать «бенчмарки» — это
и
, оцененные на наблюдаемых переменных, при этом желательно наиболее важных для нас.
Например, в нашем кейсе с оценкой эффекта от промо (D) на GMV пользователя в ретейлере (Y) одна из самых важных переменных — это GMV пользователя в ретейлере на предпериоде (назовем ее prev_retailer_gmv). Если мы будем применять оценщик на данных с этой важной переменной и без нее, то можем получить и
, которые присущи именно этой метрике (или набору метрик).
bench_prev_retailer_gmv = dml_obj.sensitivity_benchmark(benchmarking_set=['prev_retailer_gmv']) print(bench_prev_retailer_gmv)
Если эта метрика действительно важна для нас, мы можем допустить, что и
потенциально ненаблюдаемого конфаундера могут быть на том же уровне, что и у нее, а затем либо использовать
и
, полученные на «бенчмарках», либо завышать их на X%, если ожидаем, что сила ненаблюдаемого конфаундера превосходит силу самой важной для нас переменной.
При увеличении этих параметров скалиброванный эффект постепенно снижается (либо увеличивается, если смещение отрицательное) — в общем случае рано или поздно мы придем к точке, когда он исчезнет (то есть граница доверительного интервала пересечет 0).

На рис. 3 можно увидеть пример визуализации изменений нижней границы доверительного интервала оценки эффекта на произвольную метрику при разных значениях и
(оси Y и X соответственно). Точка Scenario — наш «бенчмарк» — комбинация
и
, рассчитанных на подборке наиболее важных для нас наблюдаемых переменных. Мы видим, что при таком «бенчмарке», даже с указанным влиянием «скрытых» конфаундеров, доверительный интервал не пересекает 0 — эффект уменьшился, но сохранился (для примера используются симулированные данные).
Подробнее о возможностях применения анализа чувствительности из «коробки» и визуализации этого процесса — можно ознакомиться в документации к соответствующей библиотеке.
Заключение: стоит ли доверять «каузальной» модели?
Базовая проверка «каузальной» модели — симуляции, A/A-тесты, искусственные эффекты. На синтетических данных большинство современных «каузальных» моделей (DML, Doubly Robust, Causal BART) действительно умеют восстанавливать эффект: если мы корректно задали D, Y и X, а данные удовлетворяют ключевым предпосылкам, то оценка сходится к «истине».
Проблема в том, что в реальных кейсах эти предпосылки почти никогда не соблюдаются идеально. Даже если мы соберем сотни признаков, останутся факторы, которые невозможно измерить. То есть главный риск — скрытые конфаундеры.
Другими словами, универсального «правильного» метода оценки эффекта нет. Можно выбрать красивый алгоритм, написать аккуратный пайплайн, получить узкий доверительный интервал — и все равно ошибиться, если на данных есть систематическое смещение.
Вывод: хорошая практика при оценке эффекта — это не просто выбор метода, а процесс, в котором оценка проходит проверку на устойчивость. Один из способов такой проверки — анализ чувствительности. Мы пробуем допускать различные «масштабы» влияния скрытых конфаундеров и наблюдаем, сохраняется ли эффект и как меняется его размер при допускаемом нами «масштабе».
На этом все! Если у вас есть вопросы, с удовольствием пообщаюсь в комментариях!
Полезные ссылки
Causal Inference for Brave and True — классический учебник по теме Causal Inference
Double/Debiased Machine Learning for Treatment and Structural Parameters — оригинальная статья о DML
Estimating Causal Effects with Double Machine Learning - A Method Evaluation — оценка DML на симуляциях в различных сценариях
Long Story Short — авторы DML описывают анализ чувствительности для своего метода
Sensitivity Analysis for Causal ML: A Use Case at Booking.com — статья об анализе чувствительности на примере данных Booking’а
