Привет! Меня зовут Паша, я маркетинговый аналитик в Купере. В этой статье речь пойдет о проверке качества «каузальных» моделей. На примере такой модели, как 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).

Причина отражена в следующем графе:

Рис.1
Рис.1

Если мы не распределяли 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 в названии метода).

Алгоритм подхода

  1. Строим M1 — модель машинного обучения для предсказания Y на основе X.

  2. Строим M2 — модель машинного обучения для предсказания D на основе X.

  3. Считаем остатки у M1 и M2 (фактические значения Y и D минус предсказанные моделями значения).

  4. Оцениваем влияние остатков 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 

Рис.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:

Рис.2
Рис.2

Соответственно, симуляции покажут качество метода, но, при условии, что наши предпосылки верны и все данные, нужные для ML-модели, есть в нашем распоряжении. Другими словами, «симулированные» условия могут быть просто удобными для метода, и он будет показывать высокую мощность. На практике же — предпосылки, особенно об отсутствии скрытых конфаундеров, легко нарушаются.

Анализ чувствительности

Сами авторы фреймворка DML предлагают гибкую систему оценки эффекта с поправкой на «скрытые конфаундеры» — называя sensitivity analysis, или анализом чувствительности.

Если мы строим модель с учетом наблюдаемых X, но без учета U, то получаем «сокращенную» оценку (short parameter). Однако, если бы нам пришлось строить модель с учетом U, мы бы получили «полную» оценку (long parameter). Разница между полной и сокращенной оценками — это «смещение» (bias), которое мы бы хотели учитывать. Авторы предлагают следующую формулу для оценки этого смещения:

bias^2 = p^2 C_Y^2 C_D^2 S^2

В ней для нас пока важны только параметры посередине — C_Y и C_D. Остальные значения можно пока оставить вне зоны контроля.

Формально C_Yи C_Dопределяются следующим образом:

  • C_Y— это доля остаточной дисперсии для Y, которую потенциально объясняет неучтенный «скрытый» конфаундер U после учёта X.

  • C_D— доля остаточной вариации Riesz representer (D с inverse-probability весами), которую потенциально объясняет неучтенный «скрытый» конфаундер U после учёта X.

Проще говоря: оба параметра показывают, насколько серьезный (с точки зрения влияния на Y и D) конфаундер U был упущен.

Если мы способны задать эти параметры, значит, можем подсчитать смещение (bias) и вычесть его из оценки, которую дает оценщик — как из точечной оценки, так и из границ доверительного интервала, чтобы понимать, по-прежнему ли наш доверительный интервал не покрывает собой 0?

Посмотрите, как это делается на примере с DML. Если DML-объект уже есть и нужно, например, посмотреть на скалиброванную оценку эффекта при C_Y=C_D= 10% (то есть «силу» влияния скрытых конфаундеров на Y и D мы оцениваем на уровне 10%), можно сделать это в две строки:

dml_obj.sensitivity_analysis(cf_y = 0.1, cf_d = 0.1)
print(dml_obj.sensitivity_summary)

Как понять, какие значения выставлять для C_Y=C_D? 10% — это много или мало? Авторы системы предлагают использовать «бенчмарки» — этоC_YиC_D, оцененные на наблюдаемых переменных, при этом желательно наиболее важных для нас.

Например, в нашем кейсе с оценкой эффекта от промо (D) на GMV пользователя в ретейлере (Y) одна из самых важных переменных — это GMV пользователя в ретейлере на предпериоде (назовем ее prev_retailer_gmv). Если мы будем применять оценщик на данных с этой важной переменной и без нее, то можем получить C_Yи
C_D, которые присущи именно этой метрике (или набору метрик).

bench_prev_retailer_gmv = dml_obj.sensitivity_benchmark(benchmarking_set=['prev_retailer_gmv'])
print(bench_prev_retailer_gmv)

Если эта метрика действительно важна для нас, мы можем допустить, что C_Yи C_D потенциально ненаблюдаемого конфаундера могут быть на том же уровне, что и у нее, а затем либо использовать C_Y и C_D, полученные на «бенчмарках», либо завышать их на X%, если ожидаем, что сила ненаблюдаемого конфаундера превосходит силу самой важной для нас переменной.

При увеличении этих параметров скалиброванный эффект постепенно снижается (либо увеличивается, если смещение отрицательное) — в общем случае рано или поздно мы придем к точке, когда он исчезнет (то есть граница доверительного интервала пересечет 0).

Рис.3
Рис.3

На рис. 3 можно увидеть пример визуализации изменений нижней границы доверительного интервала оценки эффекта на произвольную метрику при разных значениях C_Y и C_D (оси Y и X соответственно). Точка Scenario — наш «бенчмарк» — комбинация C_Y и C_D, рассчитанных на подборке наиболее важных для нас наблюдаемых переменных. Мы видим, что при таком «бенчмарке», даже с указанным влиянием «скрытых» конфаундеров, доверительный интервал не пересекает 0 — эффект уменьшился, но сохранился (для примера используются симулированные данные).

Подробнее о возможностях применения анализа чувствительности из «коробки» и визуализации этого процесса — можно ознакомиться в документации к соответствующей библиотеке.

Заключение: стоит ли доверять «каузальной» модели?

Базовая проверка «каузальной» модели — симуляции, A/A-тесты, искусственные эффекты. На синтетических данных большинство современных «каузальных» моделей (DML, Doubly Robust, Causal BART) действительно умеют восстанавливать эффект: если мы корректно задали D, Y и X, а данные удовлетворяют ключевым предпосылкам, то оценка сходится к «истине».

Проблема в том, что в реальных кейсах эти предпосылки почти никогда не соблюдаются идеально. Даже если мы соберем сотни признаков, останутся факторы, которые невозможно измерить. То есть главный риск скрытые конфаундеры.

Другими словами, универсального «правильного» метода оценки эффекта нет. Можно выбрать красивый алгоритм, написать аккуратный пайплайн, получить узкий доверительный интервал — и все равно ошибиться, если на данных есть систематическое смещение.

Вывод: хорошая практика при оценке эффекта — это не просто выбор метода, а процесс, в котором оценка проходит проверку на устойчивость. Один из способов такой проверки — анализ чувствительности. Мы пробуем допускать различные «масштабы» влияния скрытых конфаундеров и наблюдаем, сохраняется ли эффект и как меняется его размер при допускаемом нами «масштабе».

На этом все! Если у вас есть вопросы, с удовольствием пообщаюсь в комментариях!

Полезные ссылки