Текст
                    
Матеус Факур, Артем Груздев Причинно-следственный анализ для смелых и честных Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Matheus Facure Alves Causal Inference for The Brave and True Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Матеус Факур, Артем Груздев Причинноследственный анализ для смелых и честных Москва, 2025
  УДК 005.642.4 ББК 65.291.2 Ф18 Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk Ф18 Факур М., Груздев А. В. Причинно-следственный анализ для смелых и честных / пер. с англ. А. В. Груздева. – М.: ДМК Пресс, 2025. – 594 с.: ил. ISBN 978-5-93700-349-2 Причинно-следственный анализ – это метод статистического анализа данных, помогающий выявить, как одно событие влияет на другое. Прочитав данную книгу, читатели научатся находить причинно-следственные связи в данных, применять правильные методы анализа и оценивать достоверность полученных выводов. Издание предназначено специалистам по анализу данных, а также будет полезно бизнес-аналитикам и инженерам. УДК 005.642.4 ББК 65.291.2 Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. ISBN 978-5-93700-349-2 (рус.) Copyright © 2022 by Matheus Facure Alves © Оформление, издание, перевод, ДМК Пресс, 2025
. .  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Содержание От издательства .............................................................................................................. 10 Часть I. ЯН ......................................................................................................................... 11 Глава 1. Введение в причинность (каузальность) ................................................. 12 Зачем беспокоиться? .......................................................................................................... 12 Наука о данных уже не та, что была раньше (или наконец-то стала таковой)............... 12 Отвечая на вопросы другого рода ..................................................................................... 14 Когда ассоциация ЯВЛЯЕТСЯ причинно-следственной связью ...................................... 15 Смещение ............................................................................................................................ 21 Ключевые идеи ................................................................................................................... 28 Глава 2. Рандомизированные эксперименты ........................................................ 29 Золотой стандарт ................................................................................................................ 29 В школе «на удаленке»........................................................................................................ 31 Идеальный эксперимент.................................................................................................... 34 Механизм распределения .................................................................................................. 35 Ключевые идеи ................................................................................................................... 36 Глава 3. Обзор статистик: самое опасное уравнение .......................................... 37 Стандартная ошибка наших оценок ................................................................................. 41 Доверительные интервалы ................................................................................................ 42 Тестирование гипотез ........................................................................................................ 48 P-значения .......................................................................................................................... 53 Ключевые идеи ................................................................................................................... 55 Глава 4. Графовые причинно-следственные (каузальные) модели ................ 57 Рассуждая о причинности .................................................................................................. 57 Ускоренный курс по графовым моделям .......................................................................... 59 Смещение, вызванное спутывающими факторами (ошибка спутывания, confounding bias) ................................................................................................................. 66 Смещение из-за отбора (ошибка отбора, selection bias) .................................................. 69 Ключевые идеи ................................................................................................................... 74 Глава 5. Поразительная эффективность линейной регрессии .......................... 75 Все, что вам нужно, – это регрессия .................................................................................. 75 Теоретические аспекты регрессии .................................................................................... 79 Регрессия для неслучайных данных .................................................................................. 81 Смещение, вызванное опущенной переменной или спутывающим фактором (omitted variable bias или confounding bias) ...................................................................... 85 Ключевые идеи ................................................................................................................... 89
6  Содержание Глава 6. Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными.......................................................................... 91 Регрессия, обученная на сгруппированных данных......................................................... 91 Регрессия c дамми-переменными...................................................................................... 97 Ключевые идеи...................................................................................................................105 Глава 7. Помимо спутывающих переменных.........................................................106 «Хорошие» контрольные переменные..............................................................................106 Преимущественно вредные контрольные переменные..................................................113 Плохие контрольные переменные – смещение из-за отбора.........................................119 Плохой COP-эффект...........................................................................................................124 Ключевые идеи...................................................................................................................130 Глава 8. Инструментальные переменные...............................................................131 Обход смещения, возникшего из-за опущенной переменной.......................................131 Квартал рождения человека и влияние образования на заработную плату..................135 Коэффициент регрессии 1-го этапа..................................................................................137 Коэффициент короткой регрессии....................................................................................140 Инструментальные переменные, созданные вручную....................................................141 Несколько инструментальных переменных.....................................................................143 Ключевые идеи...................................................................................................................148 Глава 9. Несоблюдение требований и LATE...........................................................149 Погружаемся в разнородный мир.....................................................................................149 Локальный средний эффект воздействия (local average treatment effect – LATE)..........155 Влияние на вовлеченность.................................................................................................158 Ключевые идеи...................................................................................................................161 Глава 10. Матчинг (сопоставление объектов тестовой и контрольной групп)....................................................................................................162 Что же в конце концов делает регрессия?.........................................................................162 Субклассификационная оценка.........................................................................................166 Матчинг-оценка..................................................................................................................167 Смещенность матчинг-оценки..........................................................................................174 Проклятие размерности.....................................................................................................178 Ключевые идеи...................................................................................................................180 Глава 11. Оценка склонности (Propensity Score)...................................................181 Психология роста................................................................................................................181 Оценка склонности.............................................................................................................186 Взвешивание по склонности.............................................................................................188 Прогнозирование оценки склонности..............................................................................190 Стандартная ошибка..........................................................................................................194 Распространенные проблемы с оценкой склонности......................................................196 Сопоставление по оценке склонности (propensity score matching)................................200 Ключевые идеи...................................................................................................................201 Глава 12. Получение оценок с двойной робастностью.......................................203 Не кладите все яйца в одну корзину.................................................................................203
Содержание  7 Получение оценок с двойной робастностью....................................................................206 Ключевые идеи...................................................................................................................212 Глава 13. Метод разности разностей........................................................................213 Три рекламных щита на юге Бразилии.............................................................................213 Метод разности разностей (difference in differences – DiD).............................................215 Непараллельные тренды....................................................................................................221 Ключевые идеи...................................................................................................................223 Глава 14. Панельные данные и фиксированные эффекты................................224 Параллельные тренды........................................................................................................225 Контролируйте то, что вы не видите.................................................................................226 Фиксированные эффекты..................................................................................................230 Визуализация фиксированных эффектов.........................................................................236 Фиксированные эффекты для периодов времени...........................................................239 Когда панельные данные вам не помогут........................................................................240 Ключевые идеи...................................................................................................................241 Глава 15. Синтетический контроль............................................................................242 Один удивительный математический трюк, позволяющий узнать то, что невозможноузнать......................................................................................................242 У нас есть время..................................................................................................................245 Синтетический контроль в виде линейной регрессии....................................................248 Не экстраполируйте............................................................................................................251 Делаем вывод......................................................................................................................256 Ключевые идеи...................................................................................................................262 Глава 16. Разрывной регрессионный дизайн. .......................................................263 Алкоголь убивает вас?........................................................................................................264 Оценка RDD.........................................................................................................................267 Взвешивание с помощью ядерной функции....................................................................271 Эффект овчины и нечеткий RDD.......................................................................................274 Тест Маккрари.....................................................................................................................278 Ключевые идеи...................................................................................................................282 Дополнительное чтение.....................................................................................................283 Часть II. ИНЬ.....................................................................................................................285 Глава 17. Курс по прогнозным моделям..................................................................286 Машинное обучение в промышленности.........................................................................287 Ускоренный курс по машинному обучению.....................................................................292 Перекрестная проверка......................................................................................................295 Прогнозы и политики.........................................................................................................297 Политика на основе одного признака...............................................................................297 Политика на основе модели машинного обучения..........................................................302 Тонкая настройка политики..............................................................................................307 Ключевые идеи...................................................................................................................310 Дополнительное чтение.....................................................................................................311
8  Содержание Глава 18. Гетерогенные эффекты воздействия и персонализация.................312 От прогнозов к анализу причинно-следственных связей...............................................312 От ATE к CATE......................................................................................................................314 Прогнозирование чувствительности................................................................................318 Ключевые идеи...................................................................................................................327 Дополнительное чтение.....................................................................................................328 Глава 19. Оценка качества причинно-следственных (каузальных) моделей. ...........................................................................................................................329 Чувствительность по диапазонам прогнозов модели......................................................333 Кривая накопленной чувствительности (cumulative sensitivity curve)...........................337 Кривая накопленного выигрыша (cumulative gain curve)................................................341 Принимаем дисперсию во внимание...............................................................................344 Ключевые идеи...................................................................................................................347 Дополнительное чтение.....................................................................................................347 Глава 20. Модели «Подключи и пользуйся». .........................................................349 Формулировка проблемы...................................................................................................349 Преобразование зависимой переменной.........................................................................351 Случай непрерывного воздействия..................................................................................357 Нелинейные эффекты воздействия..................................................................................363 Ключевые идеи...................................................................................................................364 Дополнительное чтение.....................................................................................................365 Глава 21. Метамодели...................................................................................................367 S-модель..............................................................................................................................368 T-модель..............................................................................................................................372 X-модель..............................................................................................................................376 Ключевые идеи...................................................................................................................380 Дополнительное чтение.....................................................................................................380 Глава 22. Несмещенное/ортогональное машинное обучение.........................382 Машинное обучение для мешающих параметров...........................................................384 Теорема Фриша–Во–Ловелла............................................................................................385 Теорема Фриша–Во–Ловелла на стероидах......................................................................388 Оценивание CATE с помощью двойного машинного обучения.....................................393 Непараметрическое двойное/несмещенное машинное обучение.................................394 Что такое непараметрическая оценка?.............................................................................398 Ненаучное двойное/несмещенное машинное обучение.................................................402 Возможно, потребуется больше эконометрики!..............................................................409 Ключевые идеи...................................................................................................................411 Дополнительное чтение.....................................................................................................412 Глава 23. Проблемы, связанные с гетерогенностью эффекта и нелинейностью............................................................................................................413 Эффекты воздействия для бинарного результата............................................................413 Симулируем данные...........................................................................................................416 Непрерывное воздействие и нелинейность.....................................................................425
Содержание  9 Ключевые идеи...................................................................................................................429 Дополнительное чтение.....................................................................................................430 Глава 24. Сага о разности разностей. .......................................................................431 1. Рождение: многообещающие панельные данные........................................................433 2. Смерть: проблемы из-за гетерогенности эффекта......................................................441 Изменение эффекта воздействия с течением времени..............................................443 Дизайн анализа событий...............................................................................................452 3. Просветление: гибкая функциональная форма............................................................455 Ключевые идеи...................................................................................................................463 Дополнительное чтение.....................................................................................................464 Глава 25. Синтетическая разность разностей. .......................................................466 Ревизия метода разности разностей.................................................................................470 Ревизия метода синтетического контроля.......................................................................473 Синтетическая разность разностей..................................................................................477 Временная гетерогенность эффекта и постепенная адаптация.....................................489 Оценивание плацебо-дисперсии......................................................................................495 Ключевые идеи...................................................................................................................500 Дополнительное чтение.....................................................................................................501 Приложение 1. Устранение смещения с помощью ортогонализации............502 Приложение 2. Устранение смещения с помощью оценки склонности........518 Приложение 3. Когда прогнозирование не работает. ........................................537 Приложение 4. Когда прогнозные метрики опасны для причинно-следственных моделей....................................................................563 Приложение 5. Конформный вывод для синтетического контроля. .............568 Словарь..............................................................................................................................586 Предметный указатель.................................................................................................592
От издательства Отзывы и пожелания Мы всегда рады отзывам наших читателей. Расскажите нам, что вы ду­маете об этой книге – что понравилось или, может быть, не понравилось. Отзывы важны для нас, чтобы выпускать книги, которые будут для вас максимально полезны. Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на страницу книги и оставив комментарий в разделе «Отзывы и рецензии». Также можно послать письмо главному редактору по адресу dmkpress@gmail. com; при этом укажите название книги в теме письма. Если вы являетесь экспертом в какой-либо области и заинтересованы в написании новой книги, заполните форму на нашем сайте по адресу http:// dmkpress.com/authors/publish_book/ или напишите в издательство по адресу dmkpress@gmail.com. Список опечаток Хотя мы приняли все возможные меры для того, чтобы обеспечить высокое качество наших текстов, ошибки все равно случаются. Если вы найдете ошибку в одной из наших книг, мы будем очень благодарны, если вы сообщите о ней главному редактору по адресу dmkpress@gmail.com. Сделав это, вы избавите других читателей от недопонимания и поможете нам улучшить последующие издания этой книги. Нарушение авторских прав Пиратство в интернете по-прежнему остается насущной проблемой. Издательство «ДМК Пресс» очень серьезно относится к вопросам защиты авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконной публикацией какой-либо из наших книг, пожалуйста, пришлите нам ссылку на интернет-ресурс, чтобы мы могли применить санкции. Ссылку на подозрительные материалы можно прислать по адресу элект­ ронной почты dmkpress@gmail.com. Мы высоко ценим любую помощь по защите наших авторов, благодаря которой мы можем предоставлять вам качественные материалы. Благодарность Издательство «ДМК Пресс» выражает искреннюю благодарность Дмитрию Колодезеву и Аслану Байрамкулову за неоценимую помощь в подготовке русскоязычного издания книги. Ваши знания, опыт и поддержка сыграли важную роль в создании качественного и доступного перевода. Спасибо за ваш вклад в популяризацию знаний!
ЧАСТЬ I ЯН Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
­ ­ ­ Глава 1 Введение в причинность (каузальность) Зачем беспокоиться? Прежде всего вы можете задаться вопросом: зачем мне это? А вот зачем: Наука о данных уже не та, что была раньше (или наконец-то стала таковой) Harvard Business Review назвал профессию data scientist самой сексуальной профессией XXI века https://hbr.org/2012/10/data-scientist-the-sexiest-job-ofthe-21st-century. И это не было пустым заявлением. Вот уже десять лет профессия data scientist находится в центре внимания. Заработная плата специа листов по искусственному интеллекту не уступала зарплатам суперзвезд спорта https://www.economist.com/business/2016/04/02/million-dollar-babies. В поисках славы и богатства сотни молодых специалистов вступили в то, что казалось неистовой золотой лихорадкой, чтобы как можно быстрее получить должность в сфере data science. Вокруг всей этой шумихи возникли целые новые отрасли. Чудесные методы обучения могут сделать вас специалистом по data science, не требуя от вас ни одной математической формулы. Спе циалисты-консультанты пообещали миллионы, если ваша компания сможет раскрыть потенциал данных. ИИ или машинное обучение называют новым электричеством, а данные – новой нефтью. Все это время экономисты пытались ответить, каково истинное влияние образования на заработок, биостатистики пытались понять, приводят ли насыщенные жиры к более высокому риску сердечного приступа, а психологи
­ Наука о данных уже не та, что была раньше (или наконец-то стала таковой)  13 стремились понять, действительно ли слова поддержки приводят к более счастливому браку. Между тем мы забыли о тех, кто все это время занимался «старомодной» наукой с использованием данных. Если быть до конца честными, data science – не новая область. Мы просто узнали о ней только сейчас благодаря огромному количеству бесплатного маркетинга, предоставленного средствами массовой информации. Если использовать аналогию Джима Коллинза, представьте, что вы наливаете себе кружку любимого ледяного пива. Если вы сделаете это правильно, почти вся кружка будет занята пивом, однако наверху будет слой пены толщиной в один палец. Эта кружка похожа на data science. Что здесь пиво? Статистические основы, научная любознательность, страсть к сложным задачам. Все это доказало свою ценность в течение сотен лет. Что здесь пена? Нереалистичные ожидания, которые в конечном итоге исчезнут. Эта пена может исчезнуть быстрее, чем вы думаете. Как пишет The Economist, «те же самые консультанты, которые предсказывают, что ИИ изменит мир, также сообщают, что настоящим менеджерам в реальных компаниях трудно внедрить ИИ, и энтузиазм по поводу него остывает. Светлана Сикуляр из исследовательской фирмы Gartner утверждает, что 2020 год может стать годом, когда мода на ИИ начнет спускаться по склону “цикла хайпа”, подробно описанного ее компанией. Инвесторы начинают понимать, что нужно проснуться и спрыгнуть с подножки: исследование европейских стартапов в области ИИ, проведенное венчурным фондом MMC, показало, что 40 % из них, похоже, вообще не используют ИИ». Что мы как специалисты по data science – или, еще лучше, как «просто» ученые – должны делать во время всего этого помешательства? Если вы сообразительны, вы научитесь игнорировать пену с самого начала. Мы здесь собрались из-за пива. Математика и статистика всегда были полезными, и вряд ли они прекратят свое существование сейчас. Изучайте именно то, что делает вашу работу ценной и еще раз ценной, а не новый блестящий инструмент, которым никто не научился пользоваться. И последнее, но не менее важное: помните, что коротких путей не сущест вует. Знания в области математики и статистики ценны именно потому, что их трудно приобрести. Если бы все могли это сделать, избыточное предложение привело бы к снижению его цены. Так что наберитесь мужества! Изучите их «Соберись, неженка!», как можно лучше. И черт возьми, поче- «Поднапрягись, мягкотелый!» – фраза используется, чтобы в шутливой му бы нет? Получайте удовольствие, поили подбадривающей форме скольку мы приступаем к этому квесту сказать кому-то быть сильнее только для смелых и честных. в какой-то ситуации
14  Введение в причинность (каузальность) Отвечая на вопросы другого рода Машинное обучение в настоящее время очень хорошо отвечает на вопросы предсказательного характера. Как написали Аджай Агравал, Джошуа Ганс и Ави Голдфарб в книге «Prediction Machines», «новая волна искусственного интеллекта на самом деле приносит нам не интеллект, а важнейший компонент интеллекта – прогноз». Мы можем реализовать массу всяких красивых вещей с помощью машинного обучения. Единственное требование состоит в том, чтобы мы формулировали наши задачи как задачи прогнозирования. Хотите перевести с английского на португальский? Тогда создайте модель машинного обучения, которая предсказывает предложения на португальском языке при наличии предложений на английском языке. Хотите распознавать лица? Тогда создайте модель машинного обучения, которая предсказывает наличие лица на картинке. Хотите построить беспилотный автомобиль? Тогда создайте модель машинного обучения, которая спрогнозирует поворот руля, давление на тормоз и газ, основываясь на изображениях и данных датчиков автомобиля. Однако машинное обучение не является панацеей. Оно может творить чудеса в условиях жестких ограничений и все же может с треском провалиться, если данные немного отклоняются от того, к чему привыкла модель. Приведу еще один пример из книги «Prediction Machines»: «во многих отраслях низкие цены связаны с низкими продажами. Например, в гостиничном бизнесе цены низкие, когда нет туристического сезона, и высокие, когда спрос резко возрастает, а отели заполнены. Учитывая эти данные, наивная прогнозная модель может предположить, что повышение цены приведет к увеличению количества проданных номеров». Машинное обучение, как известно, плохо справляется с такой проблемой, как обратная причинно-следственная связь. Его просят отвечать на вопросы типа «что, если», которые экономисты называют контрфактическими. Что произойдет, если я укажу другую цену вместо той, которую сейчас прошу за свой товар? Что произойдет, если я сяду на диету с низким содержанием сахара вмес­ то диеты с низким содержанием жиров, на которой я сижу? Если вы работаете в банке, выдавая кредит, вам придется выяснить, как изменение кредитной линии изменит ваш доход. Или, если вы работаете в органах местного самоуправления, вам могут предложить выяснить, как улучшить систему школьного образования. Стоит ли давать планшеты каждому ребенку, потому что так велит эра цифровых знаний? Или вам следует построить старомодную библиотеку? В основе этих вопросов лежит поиск причин и следствий, которые мы хотим понять. Вопросы о причинно-следственных связях пронизывают повседневные задачи, например нам нужно выяснить, как увеличить продажи. А еще они играют существенную роль в очень личных и важных для нас дилеммах: должен ли я ходить в дорогую школу, чтобы добиться успеха в жизни (влияет ли образование на заработок)? Снижает ли иммиграция мои шансы на получение работы (приводит ли иммиграция к росту безработицы)? Денежные выплаты бедным снижают уровень преступности? Не важно, в какой
Когда ассоциация ЯВЛЯЕТСЯ причинно-следственной связью  15 области вы работаете. Очень вероятно, что вам приходилось или придется отвечать на тот или иной вопрос о причинно-следственных связях. К сожалению для машинного обучения, пытаясь ответить на эти вопросы, мы не можем полагаться на прогнозы, основанные на корреляции. Ответить на такой вопрос сложнее, чем думает большинство людей. Ваши родители, вероятно, неоднократно повторяли вам, что «ассоциация1 – это не причинно-следственная связь» («ассоциация – это не каузация»). Но на самом деле объяснить, почему это так, немного сложнее. Именно этому и посвящено введение в анализ причинно-следственных связей (также мы будем называть его причинно-следственным анализом, каузальным анализом). Что касается остальной части данной книги, то она будет посвящена выяснению того, как превратить ассоциацию в причинно-следственную связь (каузацию). Когда ассоциация ЯВЛЯЕТСЯ причинно-следственной связью Интуитивно мы как бы понимаем, почему ассоциация не является причинно-следственной связью. Если кто-то скажет вам, что школы, которые выдают своим ученикам планшеты, демонстрируют более высокие результаты 1 Обратите внимание, что здесь и далее Матеус использует термин «ассоциация», а не «корреляция». Термины «ассоциация» и «корреляция» используются для описания того, существует ли связь между двумя случайными величинамии. Оба термина можно использовать для анализа связи между случайными величинами с помощью диаграммы рассеяния. Корреляция количественно определяет связь между двумя случайными переменными, используя число от –1 до 1, но ассоциация не использует конкретное число для количественной оценки связи. Корреляция может только сказать нам, имеют ли две случайные величины линейную связь, в то время как ассоциация может сказать нам, имеют ли две случайные величины линейную или нелинейную связь. Поэтому нулевая корреляция между двумя переменными может нередко ввести в заблуждение, поскольку она скрывает тот факт, что вместо этого существует нелинейная связь. – Прим. перев. строгая положительная корреляция слабая положительная корреляция ассоциация нет ассоциации строгая отрицательная корреляция слабая отрицательная корреляция ассоциация нет ассоциации
16  Введение в причинность (каузальность) успеваемости, чем школы, которые не выдают планшеты, вы можете на это быстро заявить, что, вероятно, школы, выдающие планшеты, богаче. Таким образом, в этих школах успеваемость будет выше, чем в среднем, даже без выдачи планшетов. В силу этого мы не можем сделать вывод, что предоставление планшетов детям во время занятий приведет к повышению их успеваемости. Можно лишь сказать, что планшеты в школе ассоциированы с высокой успеваемостью, измеренной по результатам экзамена ENEM1. Приведем ящичковые диаграммы для иллюстрации связи между наличием планшетов и успеваемостью, которая измерена в количестве баллов, полученных на экзамене ENEM. import pandas as pd import numpy as np from scipy.special import expit import seaborn as sns from matplotlib import pyplot as plt from matplotlib import style style.use("fivethirtyeight") np.random.seed(123) n = 100 tuition = np.random.normal(1000, 300, n).round() tablet = np.random.binomial( 1, expit((tuition - tuition.mean()) / tuition.std())).astype(bool) enem_score = np.random.normal(200 - 50 * tablet + 0.7 * tuition, 200) enem_score = (enem_score - enem_score.min()) / enem_score.max() enem_score *= 1000 data = pd.DataFrame(dict(enem_score=enem_score, Tuition=tuition, Tablet=tablet)) plt.figure(figsize=(6,8)) sns.boxplot(y="enem_score", x="Tablet", data=data).set_title("Количество баллов по экзамену ENEM\n" "в зависимости от наличия планшетов в классе") plt.show() 1 ENEM (Exame Nacional do Ensino Medio) является необязательным стандартизированным бразильским национальным экзаменом, который оценивает старшеклассников в Бразилии. ENEM – самый важный экзамен такого рода в Бразилии: в 2016 го­ду было зарегистрировано более 8.6 млн кандидатов. Он является вторым по величине в мире после Национального вступительного экзамена в высшие учебные заведения Китая.
Когда ассоциация ЯВЛЯЕТСЯ причинно-следственной связью  17 Чтобы выйти за рамки простой интуиции, давайте сначала введем некоторые обозначения. Это будет наш повседневный язык, позволяющий рассуждать о причинности. Представьте, что речь идет об общепринятом языке, который мы будем использовать для идентификации других смелых и честных воинов причинно-следственного анализа, и он станет боевым кличем в предстоящих битвах. Пусть T i – получение лечения i-м объектом. Лечение здесь не обязательно должно быть приемом лекарства или чем-то, связанным с медициной. Наоборот, это просто термин, который мы будем использовать для обозначения некоторого вмешательства, воздействия, эффект от которого хотим выяснить. Здесь мы будем использовать термины «лечение», «воздействие», «вмешательство» в качестве синонимов. В нашем случае лечение или воздействие заключается в выдаче планшетов студентам. Здесь же отметим, что иногда для обозначения лечения или воздействия будет использоваться D вместо T. Пусть Yi – наблюдаемое (фактическое) значение результирующей переменной для элемента i. Результирующая переменная – это интересующая нас переменная. Ее еще можно назвать переменной результата, зависимой переменной. Мы хотим выяснить, влияет ли на нее лечение. В нашем примере с планшетами результирующей переменной будет академическая успеваемость. Вот тут-то все и становится интересным. Фундаментальная проблема анализа причинно-следственных связей состоит в том, что мы никогда не сможем наблюдать один и тот же объект, который подвергся воздействию
18  Введение в причинность (каузальность) и остался без воздействия. Это как будто у нас – две расходящиеся дороги, и мы можем выяснить, что нас ждет впереди только на той дороге, по которой пойдем. Как в стихотворении Роберта Фроста: В осеннем лесу, на развилке дорог, Стоял я, задумавшись, у поворота; Пути было два, и мир был широк, Однако я раздвоиться не мог, И надо было решаться на что-то. Чтобы разобраться в этой проблеме, мы будем много говорить о потенциальных результатах (potential outcomes). Они являются потенциальными, потому что на самом деле их не было. Они обозначают, что произошло бы в случае, если бы было оказано какое-либо воздействие. Мы иногда называем потенциальный результат, который произошел, фактическим, а тот, который не произошел, контрфактическим. Что касается обозначений, мы введем дополнительный индекс:   Y0i – потенциальный результат для объекта i, который не подвергся воздействию;   Y1i – потенциальный результат для того же самого элемента i, который подвергся воздействию. Потенциальные результаты можно записать в виде функций Yi (t), так что будьте внимательны. Y0i может стать Yi (0), а Y1i может стать Yi (1). Здесь в большинстве примеров мы будем использовать индексную нотацию. Вернемся к нашему примеру. Y1i – это академическая успеваемость ученика i, если он или она посещает класс, в котором выдают планшеты. Получит или не получит ученик планшет, это не имеет значения для Y1i. Нет никакой разницы. Если ученик получит планшет, мы сможем наблюдать Y1i. Если
Когда ассоциация ЯВЛЯЕТСЯ причинно-следственной связью  19 ученик не получит планшет, мы сможем наблюдать Y0i. Обратите внимание, что в последнем случае результат Y1i по-прежнему определен, мы просто не можем увидеть его. В данном случае речь идет о контрфактическом потенциальном результате. Исходя из потенциальных результатов, мы можем определить индивидуальный эффект воздействия: Y1i – Y0i. Конечно, из-за фундаментальной проблемы причинно-следственного вывода мы никогда не сможем определить индивидуальный эффект воздействия, поскольку наблюдаем только один из потенциальных исходов. На данный момент давайте сосредоточимся на чем-то более простом, чем оценка индивидуального эффекта воздействия. Давайте сосредоточимся на среднем эффекте воздействия (average treatment effect), который определяется следующим образом: ATE = E[Y1 – Y0], где E[…] – математическое ожидание. Другой более простой для оценки величиной является средний эффект воздействия для тех, кто подвергся воздействию (average treatment effect on the treated)1: ATT = E[Y1 – Y0 | T = 1]. Теперь я знаю, что мы не можем увидеть оба потенциальных результата, но просто ради аргументации давайте предположим, что можем. Представьте, что бог причинно-следственного вывода удовлетворился многочисленными статистическими битвами, в которых мы поучаствовали, и наградил нас божественными способностями видеть потенциальные альтернативные результаты. Обладая такими возможностями, мы собираем данные по 4 школам. Мы знаем, раздавали ли они планшеты своим ученикам, а также их баллы по некоторым ежегодным академическим тестам. Здесь раздача планшетов – это воздействие, таким образом, T = 1, если школа предоставляет своим детям планшеты. Y – количество баллов, полученное по итогам теста. pd.DataFrame(dict( i=[1, 2, 3, 4], Y0=[500, 600, 800, 700], Y1=[450, 600, 600, 750], T=[0, 0, 1, 1], Y=[500, 600, 600, 750], TE=[-50, 0, -200, 50], )) 1 Термины «индивидуальный эффект воздействия», «средний эффект воздействия», «средний эффект воздействия для тех, кто подвергся воздействию» мы могли бы заменить на синонимы «индивидуальный эффект лечения», «средний эффект лечения», «средний эффект лечения для тех, кто подвергся лечению» соответственно.
20  Введение в причинность (каузальность) В реальном мире нам неизвестны эти значения (это контрфактические результаты) Разберем первую строку таблицы. Вы – руководство школы i = 1, в которой не выдали планшеты (T = 0). Записываем успеваемость Y0(0) = 500. А что, если бы в школе выдали планшеты, какой была бы успеваемость? В реальной жизни вы не знаете. Записываем успеваемость Y1(0) = NaN. Впрочем, если вы наделены божественными способностями, то знаете. Записываем успеваемость Y1(0) = 450. Разберем третью строку таблицы. Вы – руководство школы i = 3, в которой выдали планшеты (T = 1). Записываем успеваемость Y1(1) = 600. А что, если бы в школе не выдали планшеты, какой была бы успеваемость? В реальной жизни вы не знаете. Записываем успеваемость Y0(1) = NaN. Впрочем, если вы наделены божественными способностями, то знаете. Записываем успеваемость Y0(1) = 800. Здесь ATE будет средним значением последнего столбца, т. е. средним значением эффекта воздействия: ATE = (–50 + 0 – 200 + 50)/4 = –50. Это означало бы, что планшеты снижают успеваемость студентов в среднем на 50 баллов. ATT здесь будет средним значением последнего столбца, когда T = 1: ATT = (–200 + 50)/2 = –75. Это говорит о том, что в школах, которые подверглись воздействию, планшеты снизили успеваемость учащихся в среднем на 75 баллов. Конечно, мы никогда не сможем этого узнать. В действительности приведенная выше таблица выглядела бы следующим образом: pd.DataFrame(dict( i=[1, 2, 3, 4], Y0=[500, 600, np.nan, np.nan], Y1=[np.nan, np.nan, 600, 750], T=[0, 0, 1, 1], Y=[500, 600, 600, 750], TE=[np.nan, np.nan, np.nan, np.nan], ))
Смещение  21 Вы могли бы заявить, что такая таблица, конечно, не идеальна, но разве я не могу взять среднее значение, вычисленное по объектам, испытавшим воздействие, и сравнить его со средним значением, вычисленным по объектам, не испытавшим воздействия? Другими словами, разве я не могу прос­ то посчитать ATE = (600 + 750)/2 – (500 + 600)/2 = 125? Ну уж нет! Обратите внимание, насколько теперь отличаются результаты от полученных ранее. Вы только что совершили тягчайший грех, приняв ассоциацию за причинно-следственную связь. Чтобы понять почему, давайте рассмотрим главного врага причинно-следственного вывода. Смещение Смещение – это то, что отличает ассоциацию от причинно-следственной связи. К счастью, ее легко понять, прибегнув к интуиции. Давайте вспомним про планшеты для учащихся. Столкнувшись с утверждением, что школы, которые выдают своим детям планшеты, достигают более высоких результатов в тес­ тах, мы можем опровергнуть его, сказав, что эти школы, вероятно, в любом случае получат более высокие результаты тестов даже без выдачи планшетов. Это обусловлено тем, что у них, вероятно, больше денег, чем у других школ. Следовательно, они могут платить лучшим учителям, позволить себе более оснащенные классы и т. д. Другими словами, это тот случай, когда школы, подвергшиеся воздействию (школы, выдавшие планшеты), несопоставимы со школами, не подвергшимися воздействию (школами, не выдавшими планшеты). Используя обозначение потенциального результата, мы тем самым показываем, что результат Y0 для школ, подвергнутых воздействию (в таблице ниже выделен красным овалом), отличается от результата Y0 для школ, не подвергнутых воздействию (в таблице ниже выделен синим овалом).
22  Введение в причинность (каузальность) Вспомним, что результат Y0 для школ, подвергнутых воздействию (успеваемость для школ, которые выдали планшеты, в предположении что они не выдали планшеты), является контрфактическим (counterfactual). Мы не можем наблюдать его, но можем порассуждать о нем. В данном конкретном случае мы даже можем использовать наше понимание того, как устроен мир, чтобы пойти еще дальше. Можно сказать, что, вероятно, результат Y0 для школ, подвергнутых воздействию, выше результата Y0 для школ, не подвергнутых воздействию. Это связано с тем, что школы, которые могут позволить себе выдавать планшеты детям, также могут позволить себе другие инструменты, способствующие лучшим результатам тестов. Давайте призадумаемся. Требуется некоторое время, чтобы привыкнуть к рассуждениям о потенциальных результатах. Перечитайте этот абзац и убедитесь, что вы его поняли. Помня вышесказанное, мы можем показать с помощью элементарной математики, почему ассоциация не является причинно-следственной связью. Ассоциация измеряется по формуле E[Y | T = 1] – E[Y | T = 0]. В нашем примере это средний балл по тесту для школ, выдавших планшеты, минус средний балл по тесту для школ, у которых планшетов нет. С другой стороны, причинно-следственная связь измеряется по формуле E[Y1 – Y0]. Давайте возьмем формулу ассоциации и заменим наблюдаемые результаты потенциальными результатами, чтобы увидеть, как они соотносятся. Для школ, подвергнутых воздействию, наблюдаемый результат – это Y1 (в таб­ лице ниже выделен фиолетовым овалом). Для школ, не подвергнутых воздействию, наблюдаемый результат – это Y0 (в таблице ниже выделен синим овалом). E[Y | T = 1] – E[Y | T = 0] = E[Y1 | T = 1] – E[Y0 | T = 0]. Теперь добавим и вычтем E[Y0 | T = 1]. E[Y0 | T = 1] – это контрфактический результат. Он показывает, каким был бы результат школ, подвергнутых воздействию, если бы они не подверглись воздействию: E[Y | T = 1] – E[Y | T = 0] = E[Y1 | T = 1] – E[Y0 | T = 0] + E[Y0 | T = 1] – E[Y0 | T = 1]. Наконец, мы переупорядочим члены, объединим некоторые ожидания, и о чудо:
Смещение  23 E[Y | T = 1] – E[Y | T = 0] = E[Y1 – Y0 | T = 1] + E[Y0 | T = 1] – E[Y0 | T = 0]. E[Y1 | T = 1] – E[Y0 | T = 0] + E[Y0 | T = 1] – E[Y0 | T = 1] По сути, полученную формулу можно разложить на средний эффект воздействия для тех, кто подвергся воздействию, и смещение: E[Y | T = 1] – E[Y | T = 0] = Ожидаемый уровень успеваемости для школ, выдавших планшеты (T = 1), если бы они не выдали планшеты (Y0) Ожидаемый уровень успеваемости для школ, не выдавших планшеты E[Y1 – Y0 | T = 1] + E[Y0 | T = 1] – E[Y0 | T = 0]. АТТ Смещение Средний эффект воздействия для школ, которые подверглись воздействию Забегая вперед, можно сказать следующее. Если в нашем примере про школы предположить, что руководство школ само решает, выдавать учащимся планшеты или нет, то естественно ожидать положительного смещения. Потому что планшеты в этом случае будут выдавать более богатые школы, которые могут позволить себе лучших преподавателей, лучшую техническую оснащенность, и, соответственно, у них будут более высокие показатели успеваемости. Соответственно, разность математических ожиданий не будет равна интересующему нас эффекту воздействия. Этот простой математический пример охватывает все проблемы, с которыми мы столкнемся при решении вопросов, связанных с причинами и следствиями. Я не могу не подчеркнуть, насколько важно, чтобы вы понимали каждый аспект этого уравнения. Если вас когда-нибудь заставят сделать татуировку на руке, это уравнение должно стать подходящим вариантом. Это то, за что нужно крепко держаться и понимать, о чем оно нам говорит, словно некий священный текст, который можно интерпретировать 100 различными способами. На самом деле давайте посмотрим глубже. Давайте разберем некоторые следствия, вытекающие из этой формулы. Во-первых, это уравнение объясняет, почему ассоциация не является причинно-следственной связью. Как мы видим, ассоциация равна эффекту воздействия для получивших воздействие плюс смещение. Смещение обусловлено различиями между тес­ товой группой (группой объектов, подвергнутых воздействию) и конт­ рольной группой (группой объектов, не подвергнутых воздействию) до начала воздействия, в ситуации, когда ни одна из них не испытала воздействия. Теперь мы можем точно сказать, почему у нас возникают подо-
24  Введение в причинность (каузальность) зрения, когда кто-то говорит нам, что наличие планшетов в классе повышает успеваемость. Мы полагаем, что в нашем примере E[Y0 | T = 0] < E[Y0 | T = 1], т. е. школы, которые выдают детям планшеты, лучше школ, которые не выдают планшеты, независимо от воздействия, оказываемого планшетами. Почему так происходит? Мы поговорим об этом подробнее, когда будем разбирать спутанность переменных, но сейчас просто подумайте о смещении, возникающем из-за того, что многие вещи, которые мы не можем конт­ ролировать, меняются вместе с воздействием. Школы, подвергшиеся воздействию, и школы, не подвергшиеся воздействию, отличаются не только по факту выдачи планшетов. Они также различаются по стоимости обучения, местонахождению, преподавателям… Чтобы утверждать, что наличие планшетов в классе повышает успеваемость, нам нужно, чтобы школы, выдавшие планшеты, и школы, не выдавшие планшеты, были в среднем похожи друг на друга. plt.figure(figsize=(10,6)) sns.scatterplot(x="Tuition", y="enem_score", hue="Tablet", data=data, s=70).set_title( "Количество баллов по экзамену ENEM\n" "в зависимости от стоимости обучения") plt.show() Теперь, когда мы поняли проблему, давайте рассмотрим ее решение. Кроме того, мы можем сказать, что необходимо для того, чтобы приравнять ассоциацию к причинно-следственной связи (каузации). Если E[Y0 | T = 0] = E[Y 0 | T = 1], то ассоциация является ПРИЧИННО-СЛЕДСТВЕННОЙ СВЯЗЬЮ­ (КАУЗАЦИЕЙ)! Понимание этого заключается не только в запоминании уравнения. Здесь есть сильный интуитивный аргумент. Сказать,
Смещение  25 что E[Y0 | T = 0] = E[Y0 | T = 1], означает, что тестовая и контрольная группы сравнимы до начала воздействия. Результат для группы объектов, подвергнутых воздействию, если бы они не подверглись воздействию (E[Y0 | T = 1]), при возможности наблюдать значение Y0 будет таким же, как для группы объектов, не подвергнутых воздействию (E[Y0 | T = 0]). Успеваемость для школ, выдавших планшеты, если бы они не выдали планшеты (E[Y0 | T = 1]), будет такой же, как для школ, не выдавших планшеты (E[Y0 | T = 0]). Тогда смещение исчезнет: E[Y | T = 1] – E[Y | T = 0] = E[Y1 – Y0 | T = 1] = ATT. Кроме того, если группа объектов, подвергнутых воздействию, и группа объектов, не подвергнутых воздействию, отличаются друг от друга только с точки зрения самого воздействия, то E[Y0 | T = 0] = E[Y0 | T = 1], и можно заключить, что причинно-следственное (каузальное) влияние на группу объектов, подвергнутых воздействию, является таким же, как и на группу объектов, не подвергнутых воздействию (потому что группы очень схожи): E[Y1 – Y0 | T = 1] = E[Y1 | T = 1] – E[Y0 | T = 1] = E[Y1 | T = 1] – E[Y0 | T = 0] = E[Y | T = 1] – E[Y | T = 0]. В этом случае разность средних СТАНОВИТСЯ причинно-следственным (каузальным) эффектом: E[Y | T = 1] – E[Y | T = 0] = ATT. Кроме того, если группа объектов, подвергнутых воздействию, и группа объектов, не подвергнутых воздействию, отличаются друг от друга только с точки зрения самого воздействия, то E[Y1 | T = 0] = E[Y1 | T = 1] , т. е. мы убеждаемся в том, что и тестовая, и контрольная группы реагируют на воздействие одинаково. Теперь, помимо того что группа объектов, подвергнутых воздействию, и группа объектов, не подвергнутых воздействию, являются взаимозаменяемыми до воздействия, они также являются взаимозаменяемыми после воздействия. В этом случае E[Y1 – Y0 | T = 1] = E[Y1 – Y0 | T = 0] и E[Y | T = 1] – E[Y | T = 0] = ATT = ATE. Повторюсь, это настолько важно, что я думаю, стоит повторить это еще раз, теперь уже с красивыми картинками. Если мы проведем сравнение между группой объектов, испытавших воздействие, и группой объектов, не испытавших воздействия, используя обычные средние, то вот что мы получим (синие точки – это школы, которые не испытывали воздействия, т. е. дети в них не получали планшет):
Результаты теста 26  Введение в причинность (каузальность) E[Y | T = 1] E[Y | T = 1] – E[Y | T = 0] E[Y | T = 0] Стоимость обучения Обратите внимание, что разница в результатах между двумя группами может быть вызвана двумя причинами: 1) эффект воздействия. Увеличение баллов, полученных в ходе тестов, вызвано тем, что детям дают планшеты; 2) некоторые различия в результатах тестов могут быть связаны со стоимостью обучения. В этом случае объекты, подвергнутые воздействию, и объекты, не подвергнутые воздействию, отличаются друг от друга, потому что подвергнутые воздействию имеют гораздо более высокую стоимость обучения. Прочие различия между группой объектов, подвергнутых воздействию, и группой объектов, не подвергнутых воздействию, лежат НЕ в самом воздействии. Стоимость обучения E[Y0 | T = 1] E[Y0 | T = 0] Смещение Y1 – Y0 Результаты теста Результаты теста Индивидуальный эффект воздействия – это разница между результатом, полученным для данного объекта в случае воздействия, и другим теоретическим результатом, который был бы у того же самого объекта, если бы он не получил воздействие. Индивидуальный эффект воздействия может быть получен только в том случае, если у нас есть божественные силы, позволяющие наблюдать за потенциальным результатом, как показано на левом рисунке ниже. Эти контрфактические результаты обозначены светлым цветом. Стоимость обучения На правом рисунке мы изобразили смещение, о котором говорили ранее. Мы получим смещение, если поставим условие, что никто не испытывает воздействия. В этом случае у нас остается только потенциальный результат Y0. И тогда мы увидим, как группа объектов, подвергнутых воздействию, в предположении что воздействия не было, отличается от группы объектов, не подвергнутых воздействию. Применительно к нашему примеру мы
Смещение  27 Результаты теста увидим, как успеваемость школ, выдавших планшеты, если бы они не выдали планшеты (E[Y0 | T = 1]), отличается от успеваемости школ, не выдавших планшеты (E[Y0 | T = 0]). Если они различаются, то что-то иное, помимо воздействия, становится причиной различия между группой объектов, подвергнутых воздействию, и группой объектов, не подвергнутых воздействию. Это нечто и есть смещение, и именно оно затеняет фактический эффект воздействия. Теперь сравните это с гипотетической ситуацией, когда нет смещения. Предположим, что планшеты распределены по школам случайным образом. В этой ситуации богатые и бедные школы имеют равные шансы подвергнуться воздействию. Воздействие будет хорошо распределено по всей стоимости обучения. E[Y | T = 1] E[Y | T = 0] E[Y | T = 1] – E[Y | T = 0] Стоимость обучения Y1 – Y0 Стоимость обучения Результаты теста Результаты теста В этом случае разница результатов для объектов, испытавших воздействие, и для объектов, не испытавших воздействия, является усредненным причинным эффектом. Это происходит потому, что нет другого источника различий между объектами, подвергнутыми воздействию, и объектами, не подвергнутыми воздействию, кроме самого воздействия. Все различия, которые мы видим, должны быть приписаны ему. Еще можно сказать, что здесь нет смещения. E[Y0 | T = 0] – E[Y0 | T = 1] Стоимость обучения Если мы представим, что никто не испытывает воздействия и поэтому наблюдаем только значения Y0, мы не увидим никакой разницы между тестовой группой и контрольной группой. Вот во всем этом и заключается титаническая задача причинно-следственного анализа. Речь идет о поиске умных способов устранения смещения
28  Введение в причинность (каузальность) и такого сопоставления объектов, получивших воздействие, и объектов, не получивших воздействия, при котором все наблюдаемые различия были бы лишь усредненным эффектом воздействия. В конечном счете причинноследственный вывод – это выяснение того, как устроен мир, лишенный всех иллюзий и неправильных интерпретаций. И теперь, когда мы это понимаем, мы можем двигаться вперед к освоению одного из самых мощных методов устранения предубеждений – оружия смелых и честных для выявления причинно-следственного эффекта. Ключевые идеи Мы убедились, что ассоциация – это не причинно-следственная связь. Самое главное, мы точно увидели, почему это не так и как мы можем превратить ассоциацию в причинно-следственную связь. Мы также ввели обозначение потенциального результата как способа разобраться в причинно-следственных связях. С его помощью мы рассматривали статистику как две возможные реальности: одну, в которой осуществляется воздействие, и другую, в которой его нет. Но, к сожалению, мы можем измерить только одну из них, в этом и заключается фундаментальная проблема причинно-следственного вывода. Мы рассмотрим некоторые основные методы оценки причинно-следственного эффекта, начиная с золотого стандарта рандомизированного исследования. По ходу дела я также рассмотрю некоторые статистические концепции. Я закончу цитатой, часто используемой на занятиях по причинно-следственному выводу и взятой из сериала «Кунг-фу»: «То, что происходит в жизни человека, уже предначертано. Человек должен идти по жизни так, как велит ему судьба». – Кейн. «Да, и все же каждый человек волен жить так, как он хочет. Хотя эти высказывания кажутся противоположными, оба верны». – Старик.
Глава 2 Рандомизированные эксперименты Золотой стандарт В предыдущей главе мы узнали, почему и чем ассоциация отличается от причинно-следственной связи. Кроме того, мы узнали, что требуется, чтобы ассоциация стала причинно-следственной связью. E[Y | T = 1] – E[Y | T = 0] = E[Y1 – Y0 | T = 1] + E[Y0 | T = 1] – E[Y0 | T = 0]. АТТ Смещение Напомним, что ассоциация становится причинно-следственной связью, если нет смещения. Смещение исчезает, если E[Y0 | T = 0] = E[Y0 | T = 1]. Другими словами, ассоциация будет причинно-следственной связью, если объекты, подвергнутые воздействию, и объекты, не подвергнутые воздействию, равны или сопоставимы, за исключением воздействия. Или, выражаясь более технически, когда результат для объектов, не подвергнутых воздействию (объектов контрольной группы), равен контрфактическому результату для объектов, подвергнутых воздействию (объектов тестовой группы). Помните, что этот контрфактический результат является результатом для группы объектов, подвергнутых воздействию, если бы они не подвергались воздействию. Применительно к примеру со школами смещение исчезает, когда средняя успеваемость школ, не выдавших планшеты, равна средней успеваемости школ, выдавших планшеты, если бы они не выдали планшеты. Я думаю, мы хорошо поработали, объяснив, как сделать ассоциацию эквивалентной причинно-следственной связи с точки зрения математических терминов. Но это только в теории. Теперь обратимся к первому инструменту,
30  Рандомизированные эксперименты который нам нужен, чтобы устранить смещение, – рандомизированным экспериментам (randomised experiments). Рандомизированные эксперименты случайным образом распределяют объекты из популяции либо в тестовую, либо в контрольную группу. Доля объектов, подвергнутых воздействию, не обязательно должна составлять 50 %. Вы можете провести эксперимент, в котором только 10 % ваших участников будут подвергнуты воздействию. Рандомизация устраняет смещение, делая потенциальные результаты независимыми от воздействия: (Y0, Y1) ⫫ T. Поначалу это может сбить с толку (так было у меня). Но не волнуйся, мой смелый и честный друг, дальше я поясню. Если результат не зависит от воздействия, не означает ли это также, что воздействие не возымело эффекта? Ну разумеется! Однако заметьте, я говорю не о результатах. Вместо этого я говорю о потенциальных (potential) результатах. Потенциальный результат – это то, каким был бы результат в тестовой группе (Y1) или конт­ рольной группе (Y0). В рандомизированных исследованиях мы не хотим, чтобы результат был независим от воздействия, поскольку считаем, что воздействие определяет результат. Но мы хотим, чтобы потенциальные результаты не зависели от воздействия. (Y0, Y1) ⫫ T Y⫫T Здесь используется мем с Мари Кондо (Marie Kondo), известной организацией пространства по методу «Spark Joy» («Искра радости») для объяснения принципов причинноследственного анализа. Подтекст мема в том, что исследователи радуются, когда потенциальные результаты не зависят от воздействия ((Y0, Y1) ⫫ T, и разочаровываются, если воздействие не влияет на результат (Y ⫫ T). Утверждение о том, что потенциальные результаты не зависят от воздействия, означает, что они будут одинаковыми в тестовой группе и контрольной группе. Проще говоря, это означает, что тестовая группа и контрольная группа сопоставимы. Или что информация о назначении воздействия не дает мне никакой информации о том, каким был результат до воздействия. Следовательно, (Y0, Y1) ⊥ T означает, что воздействие – единственное, что приводит к разнице между результатами в тестовой группе и контрольной
В школе «на удаленке»  31 группе. Чтобы убедиться в этом, обратите внимание, независимость под­ разумевает именно то, что E[Y0 | T = 0] = E[Y0 | T = 1] = E[Y0], в свою очередь, как мы уже видели, приводит к тому, что E[Y | T = 1] – E[Y | T = 0] = E[Y1 – Y0] = ATE. Итак, рандомизация позволяет нам использовать простую разницу средних для тестовой и контрольной групп и назвать это эффектом воздействия. В школе «на удаленке» В 2020 году пандемия коронавируса вынудила бизнес адаптироваться к социальному дистанцированию. Службы доставки получили широкое распространение, а крупные корпорации перешли на стратегию удаленной работы. Со школами все было по-другому. Многие открыли собственные онлайн-классы. Спустя четыре месяца после начала пандемии многие задались вопросом, можно ли сохранить введенные изменения. Нет никаких сомнений в том, что онлайн-обучение имеет свои преимущества. Оно дешевле, так как позволяет сэкономить на помещениях и транспорте. Кроме того, оно может быть более цифровым, используя контент со всего мира, а не только от фиксированного набора учителей. Несмотря на все это, нам все еще нужно ответить, оказывает онлайн-обучение отрицательное или положительное влияние на успеваемость учащегося. Один из способов ответить на этот вопрос – взять учащихся из школ, которые проводят в основном онлайн-занятия, и сравнить их с учащимися из школ, которые проводят лекции в традиционных аудиториях. Как мы уже знаем, это не лучший подход. Возможно, онлайн-школы нравятся только хорошо дисциплинированным ученикам, которые достигли бы более высокой успеваемости (в сравнении со средней успеваемостью) даже в случае присутствия на традиционных занятиях. В этом случае у нас будет положительное смещение, когда учащиеся, подвергнутые воздействию (перешедшие на онлайн-формат обучения), имеют более высокую успеваемость, чем учащиеся, не подвергнутые воздействию: E[Y0 | T = 1] > E[Y0 | T = 0]. Или, с другой стороны, онлайн-классы могут быть дешевле, и их посещают в основном менее состоятельные ученики, которым, возможно, приходится работать помимо учебы. В этом случае такие ученики будут успевать хуже, чем ученики, посещающие обычные занятия, даже если их перевести на традиционный формат обучения. Если это так, у нас будет смещение в другую сторону, когда учащиеся, подвергнутые воздействию (перешедшие на
32  Рандомизированные эксперименты онлайн-обучение), имеют более низкую успеваемость, чем учащиеся, не подвергнутые воздействию: E[Y0 | T = 1] < E[Y0 | T = 0]. Таким образом, мы хоть и можем провести простое сравнение, оно не будет убедительным. Так или иначе, мы никогда не можем быть уверены в том, что где-то рядом не скрывается какое-то смещение, маскирующее причинно-следственный эффект. Здесь используется мем с Джейсоном Момоа, подкрадывающимся к Генри Кавиллу. Смещение E[Y0 | T = 1] – E[Y0 | T = 0] незаметно подкрадывается к ассоциации E[Y | T = 1] – E[Y | T = 0] Чтобы решить эту проблему, нам нужно сделать группу объектов, подвергнутых воздействию, и группу объектов, не подвергнутых воздействию, сопоставимыми E[Y0 | T = 1] = E[Y0 | T = 0]. Один из способов добиться этого – случайным образом отправить учеников на онлайн-занятия и традиционные занятия. Если бы нам удалось это сделать, объекты, подвергнутые воздействию, и объекты, не подвергнутые воздействию, в среднем были бы одинаковыми, за исключением полученного воздействия. К счастью, некоторые экономисты сделали это за нас. Они случайным образом распределили учащихся так, что некоторым поручили посещать традиционные занятия, другим – только онлайн-уроки, а третьей группе предложили смешанный формат онлайн- и традиционных занятий. Затем они собрали данные о результатах стандартного экзамена в конце семестра. Давайте загрузим данные. import pandas as pd import numpy as np # загружаем данные data = pd.read_csv("data/online_classroom.csv")
В школе «на удаленке»  33 print(data.shape) data.head() (323, 10) Мы видим, что у нас есть 323 наблюдения. Это небольшие по объему данные, но мы можем с ними работать. Чтобы оценить причинно-следственный эффект, мы можем просто вычислить средний балл для каждой из групп. # вычислим средние значения переменных по группам (data .assign(class_format = np.select( [data["format_ol"].astype(bool), data["format_blended"].astype(bool)], ["online", "blended"], default="face_to_face" )) .groupby(["class_format"]) .mean()) Ага. Так просто. Мы видим, что традиционные занятия дают средний балл 78.54, а онлайн-занятия дают средний балл 73.63. Не очень хорошая новость для сторонников онлайн-обучения. Таким образом, ATE для онлайн-занятий составляет –4.91. Это означает, что онлайн-занятия снижают успеваемость учащихся примерно на 5 баллов в среднем. Вот и все. Вам не нужно беспокоиться о том, что на онлайн-занятиях могут быть более бедные ученики, которые не могут позволить себе традиционные занятия, или, если уж на то пошло, вам не нужно беспокоиться о том, что ученики из разных групп отличаются каким-либо образом, кроме воздействия, которое они испытали. По замыслу случайный эксперимент проводится для того, чтобы нивелировать эти различия. По этой причине хорошей проверкой на здравый смысл, позволяющей убедиться, что рандомизация была проведена правильно (или убедиться
34  Рандомизированные эксперименты в том, что вы анализируете корректные данные), является проверка эквивалентности объектов, подвергнутых воздействию, объектам, не подвергнутым воздействию, с точки зрения предварительно выбранных переменных. Наши данные содержат информацию о поле и этнической принадлежности, которая позволяет увидеть, схожи ли объекты по полу и этнической принадлежности в разных группах. Мы можем сказать, что группы выглядят довольно схожими по переменным gender, asian, hispanic и white. Однако переменная black ведет себя немного иначе. Данный факт привлекает внимание, потому что мы работаем с небольшим набором данных. Даже в условиях рандомизации может случиться так, что чисто случайно одна группа будет отличаться от другой. В больших выборках эта разница обычно исчезает. Идеальный эксперимент Рандомизированные эксперименты, или рандомизированные контролируемые испытания, кратко – РКИ (Randomised Controlled Trials – RCT) – самый надежный способ определить причинно-следственные эффекты. Это прос­ тая и до абсурда убедительная техника. Она настолько мощная, что в большинстве стран рандомизированный эксперимент является обязательным требованием для подтверждения эффективности нового лекарства. Чтобы провести ужасную аналогию, вы можете представить РКИ в виде Аанга из сериала «Аватар: последний маг воздуха», в то время как другие техники больше похожи на Сокку. Сокка крут и может выполнить несколько ловких трюков и тут, и там, но Аанг может подчинить себе четыре стихии и взаимодействовать с духовным миром. Подумайте об этой аналогии: будь наша воля, РКИ стали бы универсальным инструментом для раскрытия причинности. Хорошо спланированные РКИ – мечта любого ученого. Кэтрин Боуман (Katherine Bouman) – ученая в трансдисциплинарной области исследований, объединяющей компьютерное зрение, обработку изображений, атмосферную оптику, астрофотографию и физическое моделирование. На фото – Кэтрин после получения первого в мире снимка черной дыры
Механизм распределения  35 К сожалению, РКИ, как правило, либо очень дорогие, либо просто нарушают этические принципы. Иногда мы просто не можем контролировать механизм назначения объектов в группы. Представьте себя врачом, пытающимся оценить влияние курения во время беременности на вес ребенка при рождении. Вы не можете просто заставить случайно выбранных будущих мам курить во время беременности. Или, скажем, вы работаете в крупном банке и вам нужно оценить влияние условий кредитной линии на отток клиентов. Было бы слишком дорого предоставлять случайным образом кредиты вашим клиентам. Или, допустим, вы хотите понять влияние повышения минимальной заработной платы на безработицу. Вы не можете просто назначить странам ту или иную минимальную заработную плату. В общем, вы поняли. Позже мы увидим, как снизить стоимость проведения рандомизации с помощью условной рандомизации, но мы ничего не можем поделать с неэтичными или невыполнимыми экспериментами. Тем не менее всякий раз, когда мы задаем вопросы о причинно-следственных связях, стоит подумать об идеальном эксперименте. Всегда спрашивайте себя по возможности, какой идеальный эксперимент вы бы провели, чтобы выявить данный причинно-следственный эффект. Этот вопрос, как правило, проливает свет на то, как мы можем обнаружить причинно-следственный эффект, даже не проводя идеального эксперимента. Механизм распределения В рандомизированном эксперименте механизм распределения объектов по группам (или, еще можно сказать, механизм назначения объектов в группы) является случайным. Как мы увидим позже, все методы причинно-следственного вывода так или иначе пытаются определить механизм назначения воздействия. Когда мы точно знаем, как ведет себя этот механизм, причинно-следственный вывод будет гораздо более надежным, даже если механизм присваивания не является случайным. К сожалению, механизм распределения нельзя обнаружить, просто взглянув на данные. Например, если у вас есть набор данных, в котором высшее образование коррелирует с богатством, вы не сможете точно сказать, какой из этих факторов является причиной другого, просто взглянув на данные. Вам придется использовать свои знания об устройстве мира, чтобы привести доводы в пользу правдоподобного механизма распределения: действительно ли школы обучают людей, делая их более продуктивными и приводя их к более высокооплачиваемой работе. Или, если вы пессимистично относитесь к образованию, вы можете сказать, что школы ничего не делают для повышения производительности труда, и это всего лишь ложная корреляция, потому что только богатые семьи могут позволить себе дать ребенку высшее образование. В вопросах о причинно-следственных связях мы обычно можем рассуждать двояко: либо X является причиной Y, либо третья переменная Z является
36  Рандомизированные эксперименты причиной и X, и Y, и, следовательно, корреляция X и Y просто ложна. В силу этого знание механизма распределения приводит к гораздо более убедительному объяснению причинно-следственных связей. И это именно то, что делает причинно-следственный вывод таким захватывающим. Если прикладное машинное обучение обычно сводится к простому нажатию нескольких кнопок в правильном порядке, то прикладной причинно-следственный вывод требует, чтобы вы серьезно подумали о механизме, генерирующем эти данные. Ключевые идеи Мы выяснили, как рандомизированные эксперименты стали самым простым и эффективным способом выявления причинно-следственных связей. Это достигается за счет сопоставимости тестовой и контрольной групп. К сожалению, мы не можем постоянно проводить рандомизированные эксперименты, но все же полезно подумать о том, какой идеальный эксперимент мы бы провели, если бы у нас была такая возможность. Кто-то, знакомый со статистикой, может прямо сейчас возмутиться тем, что я не рассмотрел дисперсию моей оценки причинно-следственного эффекта. Откуда мне знать, что снижение на 4.91 балла не случайно? Другими словами, как я могу узнать, является ли разница статистически значимой? И они будут правы. Не волнуйтесь. Далее я намерен рассмотреть некоторые статистические понятия. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Глава 3 Обзор статистик: самое опасное уравнение В своей знаменитой статье 2007 года Говард Вейнер пишет об очень опасных уравнениях: «Некоторые уравнения опасны, если вы их знаете, а другие опасны, если вы их не знаете. Первые могут представлять опасность, потому что находящиеся в них секреты открывают двери, за которыми таится страшная опасность. Очевидным победителем в этом соревновании является культовое уравнение Эйнштейна E = mc2, поскольку оно дает меру огромной энергии, скрытой в обычной материи. […] Однако меня интересуют уравнения, которые раскрывают свою опасность не тогда, когда мы знаем о них, а скорее тогда, когда мы их не знаем. Находясь под рукой, эти уравнения позволяют нам ясно понимать вещи, но их отсутствие оставляет нас в опасном неведении». Уравнение, о котором он говорит, – это уравнение Муавра: где SE – стандартная ошибка среднего, σ – стандартное отклонение, а n является размером выборки. Выглядит как кусочек математики, который должны освоить смелые и честные, так что давайте приступим к делу. Чтобы понять, почему незнание этого уравнения очень опасно, давайте посмотрим на некоторые данные об образовании. Я собрал данные о баллах ENEM по разным школам за 3 года. Кроме того, я почистил данные, чтобы оставить только актуальную для нас информацию. Исходные данные можно скачать на сайте Inep http://portal.inep.gov.br/ web/guest/microdados. Если мы посмотрим на лучшую школу (лучшую с точки зрения количества баллов ENEM), кое-что бросается в глаза: в этой школе достаточно мало учеников.
38  Обзор статистик: самое опасное уравнение # импортируем библиотеки import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from scipy import stats import seaborn as sns from matplotlib import pyplot as plt from matplotlib import style style.use("fivethirtyeight") # загружаем данные и сортируем по убыванию балла ENEM df = pd.read_csv("data/enem_scores.csv") df.sort_values(by="avg_score", ascending=False).head(10) Посмотрим на данные под другим углом: мы можем выделить только 1 % лучших школ и изучить их. Чем они схожи между собой? Возможно, мы сможем чему-то научиться у лучших школ и повторить это в других школах. И конечно же, если мы посмотрим на 1 % лучших школ, то обнаружим, что в них в среднем меньше учеников. # строим ящичковые диаграммы, удалив выбросы plot_data = ( df .assign(top_school = df["avg_score"] >= np.quantile(df["avg_score"], .99)) [["top_school", "number_of_students"]] .query(f"number_of_students<{np.quantile(df['number_of_students'], .98)}")) plt.figure(figsize=(6,6)) sns.boxplot(x="top_school", y="number_of_students", data=plot_data) plt.title("Количество учеников в 1 % топовых школ (справа)");
Ключевые идеи  39 Один из естественных выводов состоит в том, что небольшие школы приводят к более высокой успеваемости. Это имеет интуитивно понятный смысл, поскольку мы считаем, что меньшее количество учеников на одного учителя позволяет учителю уделять пристальное внимание каждому ученику. Но какое отношение это имеет к уравнению Муавра? И почему оно опасно? Что ж, оно становится опасным, когда люди начинают принимать важные и дорогостоящие решения на основе вышеприведенной информации. В своей статье Ховард продолжает: «В 1990-е годы стало популярным выступать за сокращение размеров школ. Многочисленные благотворительные организации и правительственные учреждения финансировали дробление больших школ, потому что учащиеся маленьких школ преобладают в группах с высокими результатами тестов». Люди забыли обратить внимание на 1 % худших школ. Если мы это сделаем, о чудо! У них тоже очень мало учеников! # получим распределение баллов ENEM # по количеству учеников в школе q_99 = np.quantile(df["avg_score"], .99) q_01 = np.quantile(df["avg_score"], .01) plot_data = ( df .sample(10000) .assign(Group = lambda d: np.select( [d["avg_score"] > q_99, d["avg_score"] < q_01], ["Top", "Bottom"], "Middle")) )
­ 40  Обзор статистик: самое опасное уравнение plt.figure(figsize=(10,5)) sns.scatterplot(y="avg_score", x="number_of_students", hue="Group", data=plot_data) plt.title("Распределение баллов ENEM по\n" "количеству учеников в школе"); Мы видим именно то, что ожидается согласно уравнению Муавра. По мере увеличения количества учеников средний балл становится все более точным. Школы с очень небольшим количеством наблюдений могут иметь очень высокий или очень низкий средний балл просто в силу случайности. В больших школах такое встречается реже. Уравнение Муавра иллюстрирует фундаментальный факт, касающийся мира информации и записей в виде данных: информация всегда является неточной. Тогда возникает вопрос, насколько неточной. Статистика – это наука, которая занимается этими неточностями, поэтому они не застают нас врасплох. Как пишет Талеб в своей книге «Одураченные случайностью»: «Вероятность – это не столько вычисление шансов при бросании костей, сколько признание недостатка определенности в наших знаниях и разработка методов для работы с нашим неведением». Одним из способов количественной оценки нашей неопределенности является дисперсия наших оценок. Дисперсия говорит нам, насколько наблюдение отклоняется от своего центрального и наиболее вероятного значения. Как показывает уравнение Муавра, эта неопределенность уменьшается по мере увеличения объема наблюдаемых нами данных. Это имеет смысл, верно? Если мы видим, что большое количество учеников отлично учится в школе, у нас больше уверенности в том, что это действительно хорошая школа. Однако если мы видим школу, в которой всего 10 учеников и восемь из них хорошо учатся, нам нужно быть более подозрительными. Возможно, по чис той случайности, в школе было несколько учеников выше среднего уровня.
­ Стандартная ошибка наших оценок  41 Красивый график в виде перевернутого вправо треугольника, который мы видим выше, иллюстрирует именно это. Он показывает нам, насколько сильно различаются наши оценки школьной успеваемости при небольших размерах выборки. Он также указывает на то, что дисперсия уменьшается по мере увеличения размера выборки. Это верно для среднего балла в школе, но это также верно для любой сводной статистики, имеющейся у нас, включая ATE, который мы часто хотим оценить. Стандартная ошибка наших оценок Поскольку это всего лишь обзор статистик, я позволю себе двигаться чуть быстрее. Если вы незнакомы с распределениями, дисперсией и стандартными ошибками, читайте дальше, но имейте в виду, что вам могут понадобиться дополнительные ресурсы. Я предлагаю вам погуглить любой вводный курс MIT по статистике. Обычно они весьма хороши. В предыдущей главе мы оценили средний эффект воздействия E[Y1 – Y0] как разницу между средним значением, полученным для группы объектов, испытавших воздействия, и средним значением, полученным для группы объектов, не испытавших воздействия E[Y | T = 1] – E[Y | T = 0]. Мы вычислили ATE для онлайн-занятий в качестве мотивирующего примера. Кроме того, мы увидели негативное влияние: онлайн-занятия привели к тому, что учащиеся показали результаты примерно на 5 баллов хуже, чем учащиеся, посещающие традиционные занятия. Теперь мы выясним, является ли это влияние статистически значимым. Чтобы сделать это, нам нужно оценить SE. У нас уже есть n, размер нашей выборки. Для вычисления оценки стандартного отклонения мы берем формулу где – x – это среднее значение x. К счастью для нас, в большинстве программных продуктов эта формула уже реализована. В библиотеке pandas мы можем воспользоваться методом .std(). # загружаем данные data = pd.read_csv("data/online_classroom.csv") online = data.query("format_ol==1")["falsexam"] face_to_face = data.query("format_ol==0 & format_blended==0")["falsexam"] # пишем функцию вычисления SE def se(y: pd.Series): return y.std() / np.sqrt(len(y)) print("SE для онлайн-занятий:", se(online)) print("SE для очных занятий:", se(face_to_face)) SE для онлайн-занятий: 1.5371593973041635 SE для очных занятий: 0.8723511456319104
42  Обзор статистик: самое опасное уравнение Доверительные интервалы Стандартная ошибка нашей оценки является мерой достоверности. Нам нужно погрузиться в бурные и полемичные воды статистики, чтобы точно понять, что это значит. В рамках одного из статистических подходов, частотного подхода, наши данные – не что иное, как проявление точного процесса генерации данных. Этот процесс абстрактен и идеален. Он управляется истинными параметрами, которые неизменны, но при этом неизвестны нам. В контексте тестирования учеников можно сказать, что если бы мы могли провести множество экспериментов и собрать множество наборов данных, то каждый из них напоминал бы истинный процесс генерации данных, но не был бы в точности эквивалентен ему. Это очень похоже на то, что писал Платон о формах: «Каждая [из сущностных форм] проявляется в самых разнообразных сочетаниях с действиями, с материальными вещами и друг с другом, и каждая, по-видимому, является многогранной». Чтобы лучше понять вышесказанное, давайте предположим, что у нас есть истинное абстрактное распределение результатов тестирования учеников. Оно представляет собой нормальное распределение с истинным средним значением 74 и истинным стандартным отклонением 2. На основе этого распределения мы можем провести 10 000 экспериментов. В каждом из них мы отбираем выборку из 500 наблюдений. Если нанести средние значения, вычисленные в ходе экспериментов, на гистограмму, мы увидим, что они распределены вокруг истинного среднего. Некоторые экспериментальные данные будут иметь среднее значение ниже истинного, а некоторые – выше. # построим распределение экспериментальных средних true_std = 2 true_mean = 74 n = 500 def run_experiment(): return np.random.normal(true_mean,true_std, 500) np.random.seed(42) plt.figure(figsize=(8,5)) freq, bins, img = plt.hist([run_experiment().mean() for _ in range(10000)], bins=40, label="Экспериментальные средние") plt.vlines(true_mean, ymin=0, ymax=freq.max(), linestyles="dashed", label="Истинное среднее", color="orange") plt.legend();
Доверительные интервалы  43 Обратите внимание, что здесь мы говорим о среднем значении средних. Таким образом, случайно мы могли бы провести эксперимент, в котором среднее значение было бы несколько ниже или выше истинного среднего. Это означает, что мы никогда не можем быть уверены, что среднее значение, вычисленное в ходе нашего эксперимента, соответствует истинному платоновскому и идеальному среднему значению. Однако с помощью стандартной ошибки мы можем вычислить интервал, который будет накрывать истинное среднее значение в 95 % случаев. В реальной жизни мы не можем позволить себе роскошь симулировать один и тот же эксперимент на множестве наборов данных. У нас, как правило, есть только один набор. Но мы можем опираться на вышеупомянутое интуитивное описание, чтобы построить доверительные интервалы (confidence intervals). С доверительными интервалами связана доверительная вероятность. Чаще всего используется 95%-ная доверительная вероятность. Эта вероятность говорит нам, сколько гипотетических доверительных интервалов, которые мы могли бы построить в ходе многочисленных экспериментов, накрывают истинное среднее значение. Например, 95%-ные доверительные интервалы, рассчитанные на основе аналогичных экспериментов, будут накрывать (покрывать) истинное среднее значение в 95 % случаев. Для расчета доверительного интервала воспользуемся центральной предельной теоремой (central limit theorem). Эта теорема утверждает, что средние значения, вычисленные в ходе экспериментов, распределяются нормально. Из статистической теории мы знаем, что 95 % значений нормального распределения находятся между 2 (более точно, между 1.96) стандартными отклонениями выше и ниже среднего. 2 – это Z-значение, которое показывает, насколько наше экспериментальное среднее отличается от истинного среднего в единицах стандартного отклонения.
44  Обзор статистик: самое опасное уравнение .40 34.1 % .35 34.1 % Probability .30 .25 .20 .15 13.6 % 13.6 % .10 .05 0.1 % .00 –4 2.1 % –3 2.1 % –2 –1 0 1 2 0.1 % 3 4 68.3 % 95.4 % 99.7 % Стандартная ошибка среднего служит оценкой распределения экспериментальных средних. Итак, если мы умножим стандартную ошибку на 2, а полученный результат добавим и вычтем из среднего значения одного из наших экспериментов, мы построим 95%-ный доверительный интервал для истинного среднего. # вычислим доверительный интервал np.random.seed(321) exp_data = run_experiment() exp_se = exp_data.std() / np.sqrt(len(exp_data)) exp_mu = exp_data.mean() ci = (exp_mu - 2 * exp_se, exp_mu + 2 * exp_se) print(ci) (73.82718114045632, 74.17341543460314) # визуализируем границы доверительного интервала x = np.linspace(exp_mu - 4*exp_se, exp_mu + 4*exp_se, 100) y = stats.norm.pdf(x, exp_mu, exp_se) plt.plot(x, y) lbl = "95%-ный\nдоверительный\nинтервал" plt.vlines(ci[1], ymin=0, ymax=1) plt.vlines(ci[0], ymin=0, ymax=1, label=lbl) plt.legend(loc='upper left') plt.show()
Доверительные интервалы  45 Конечно, мы можем не ограничиваться 95%-ным доверительным интервалом. Мы могли бы сгенерировать 99%-ный доверительный интервал, найдя z-значение для 99%-ной доверительной вероятности. Вычисленное z-значение, умноженное на стандартную ошибку, даст интервал, который будет охватывать 99 % значений нормального распределения. Функция ppf() в Python дает нам обратную функцию CDF. Вместо того чтобы умножать стандартную ошибку на 2, как мы делали, чтобы найти 95%-ный доверительный интервал, мы умножим ее на z-значение для 99%-ной доверительной вероятности, что приведет к получению 99%-ного доверительного интервала. Таким образом, ppf(0.5) вернет нам 0, говоря о том, что 50 % значений стандартного нормального распределения (со средним значением 0 и стандартным отклонением 1) находятся ниже 0. Точно так же, если мы подставим 99.5 %, мы получим z-значение 2.58, при котором 99.5 % значений стандартного нормального распределения будут ниже 2.58, а 0.5 % значений стандартного нормального распределения будут выше 2.58. # вычислим 99%-ный доверительный интервал from scipy import stats z = stats.norm.ppf(.995) print(z) ci = (exp_mu - z * exp_se, exp_mu + z * exp_se) ci 2.5758293035489004 (73.7773381773405, 74.22325839771896) # визуализируем 99%-ный доверительный интервал x = np.linspace(exp_mu - 4*exp_se, exp_mu + 4*exp_se, 100) y = stats.norm.pdf(x, exp_mu, exp_se) plt.plot(x, y) lbl = "99%-ный\nдоверительный\nинтервал" plt.vlines(ci[1], ymin=0, ymax=1)
46  Обзор статистик: самое опасное уравнение plt.vlines(ci[0], ymin=0, ymax=1, label=lbl) plt.legend() plt.show() Вернемся к нашему эксперименту с занятиями: мы можем построить доверительный интервал для среднего балла за экзамен как для группы учеников, проходивших онлайн-занятия, так и для группы учеников, проходивших обычных занятия. # пишем функцию вычисления 95%-ного # доверительного интервала def ci(y: pd.Series): return (y.mean() - 2 * se(y), y.mean() + 2 * se(y)) print(f"95%-ный доверительный интервал " f"для онлайн-занятий:\n{ci(online)}") print(f"95%-ный доверительный интервал " f"для очных занятий:\n{ci(face_to_face)}") 95%-ный доверительный интервал для онлайн-занятий: (70.56094429049804, 76.7095818797147) 95%-ный доверительный интервал для очных занятий: (76.80278229206951, 80.29218687459715) Мы видим, что 95%-ные доверительные интервалы среднего балла для групп не перекрываются. Нижняя граница доверительного интервала для очных занятий выше верхней границы доверительного интервала для онлайнзанятий. Это свидетельствует о том, что наш результат не случаен и истинное среднее значение балла для учеников, занимающихся очно, выше истинного среднего значения балла для учеников, занимающихся онлайн. Другими словами, при переходе от очных занятий к онлайн-занятиям наблюдается значимое причинно-следственное снижение успеваемости.
Доверительные интервалы  47 Напомним, что доверительные интервалы – это способ включить неопределенность в наши оценки. Чем меньше размер выборки, тем больше стандартная ошибка и тем шире доверительный интервал. Поскольку их очень легко вычислить, отсутствие доверительных интервалов сигнализирует либо о каких-то плохих намерениях, либо просто об отсутствии знаний, что не менее важно. Наконец, вы всегда должны с подозрением относиться к измерениям, в которых не используется метрика неопределенности. Рис. 2. ПРОГНОЗНАЯ КРИВАЯ НАУЧНЫЙ СОВЕТ: ЕСЛИ ВАША МОДЕЛЬ ДОСТАТОЧНО ПЛОХА, ДОВЕРИТЕЛЬНЫЙ ИНТЕРВАЛ ВЫЙДЕТ ЗА ПРЕДЕЛЫ ОБЛАСТИ ПЕЧАТИ И последнее предостережение здесь. Доверительные интервалы интерпретировать сложнее, чем кажется на первый взгляд. Например, не следует говорить, что конкретный 95%-ный доверительный интервал содержит истинное среднее значение генеральной совокупности с вероятностью 95 %. С точки зрения частотного подхода, использующего доверительные интервалы, среднее значение совокупности рассматривается в качестве истинной константы генеральной совокупности. Границы интервала находятся по выборочным данным и поэтому являются случайными величинами, в отличие от истинной константы (т. е. неслучайной величины), поэтому правильнее говорить о том, что интервал накрывает, а не содержит истинное значение
48  Обзор статистик: самое опасное уравнение неизвестного параметра. Наш конкретный доверительный интервал либо покрывает, либо не покрывает истинное среднее значение. Если да, то вероятность его покрытия будет 100 %, а не 95 %. Если нет, то вероятность равна 0 %. Наша вероятность 95 % относится не к конкретному вычисленному интервалу, а к надежности процедуры оценки, к тому, как часто 95%-ные доверительные интервалы, рассчитанные на основе большого количества экспериментов, накрывали истинное среднее значение. Теперь, сказав это, как экономист (статистики, пожалуйста, сейчас отвернитесь) я думаю, эти уточнения были не очень полезны. На практике вы увидите, как люди говорят, что конкретный доверительный интервал содержит истинное среднее значение в 95 % случаев. Хотя это и неверно, это не очень вредно, поскольку все еще вносит определенную степень неопределенности в наши оценки. Более того, если мы перейдем к байесовской статистике и будем использовать вероятностные интервалы вместо доверительных интервалов, мы сможем сказать, что интервал содержит среднее значение распределения в 95 % случаев. Кроме того, из того, что я видел на практике, при достаточных размерах выборки байесовские вероятностные интервалы гораздо больше похожи на доверительные интервалы, чем это готовы признать сторонники частотного и байесовского подходов. Так что если мое слово чего-то стоит, не стесняйтесь говорить все, что хотите, о своем доверительном интервале. Мне все равно, если вы скажете, что он содержит истинное среднее значение в 95 % случаев. Пожалуйста, никогда не забывайте размещать их рядом с вашими оценками, в противном случае вы будете выглядеть глупо. Тестирование гипотез Еще один способ включить неопределенность – сформулировать проверку гипотезы: разница средних значений отличается статистически значимо от нуля (или любого другого значения)? Вспомните, что сумма или разность двух независимых нормальных распределений также является нормальным распределением. Результирующее среднее будет суммой или разницей между двумя распределениями, тогда как дисперсия всегда будет суммой дисперсий: N(μ1, σ12) – N(μ2, σ22) = N(μ1 – μ2, σ12 + σ22); N(μ1, σ12) + N(μ2, σ22) = N(μ1 + μ2, σ12 + σ22). Если вы не вспомнили эти формулы, по-прежнему все нормально. Мы всегда можем воспользоваться программным кодом и искусственными данными для проверки: # визуализируем распределения np.random.seed(123) n1 = np.random.normal(4, 3, 30000)
Тестирование гипотез  49 n2 = np.random.normal(1, 4, 30000) n_diff = n2 - n1 sns.distplot(n1, hist=False, label="$N(4,3^2)$") sns.distplot(n2, hist=False, label="$N(1,4^2)$") sns.distplot(n_diff, hist=False, label=f"$N(1,4^2) - (4,3^2) = N(-3, 5^2)$") plt.legend() plt.show() Если мы возьмем распределения средних значений двух наших групп и вычтем одно из другого, мы получим третье распределение. Среднее значение этого итогового распределения будет разностью средних, а стандартное отклонение этого распределения будет квадратным корнем из суммы стандартных отклонений: μdiff = μ1 – μ2, Вернемся к нашему примеру с занятиями. Построим это распределение разности. Конечно, как только мы его получим, построить 95%-ный доверительный интервал разности будет несложно. # вычислим доверительный интервал разности diff_mu = online.mean() - face_to_face.mean() diff_se = np.sqrt(face_to_face.var()/len(face_to_face) + online.var()/len(online))
50  Обзор статистик: самое опасное уравнение ci = (diff_mu - 1.96*diff_se, diff_mu + 1.96*diff_se) print(ci) (-8.376410208363385, -1.4480327880905248) # визуализируем доверительный интервал разности x = np.linspace(diff_mu - 4*diff_se, diff_mu + 4*diff_se, 100) y = stats.norm.pdf(x, diff_mu, diff_se) plt.plot(x, y) lbl = "95%-ный\nдоверительный\nинтервал" plt.vlines(ci[1], ymin=0, ymax=.05) plt.vlines(ci[0], ymin=0, ymax=.05, label=lbl) plt.legend() plt.show() Имея доверительный интервал разности под рукой, мы можем сказать, что на 95 % уверены, что истинная разница между группой учеников, посещавших онлайн-занятия, и группой учеников, посещавших очные занятия, находится между –8.37 и –1.44. Мы также можем вычислить z-значение, разделив среднее разности на стандартную ошибку разности: где H0 – это значение, с помощью которого мы хотим проверить нашу разницу. z-значение – это показатель того, насколько экстремальна наблюдаемая разница. Мы воспользуемся им для проверки нашей гипотезы о том, что разность средних значений статистически значимо отличается от нуля. Мы
Тестирование гипотез  51 будем предполагать, что верно обратное, т. е. мы будем предполагать, что разница равна нулю. Это предположение называется нулевой гипотезой, или H0. Затем мы спросим себя: «Какова вероятность, что мы наблюдали бы такую разницу, если бы истинная разница была равна нулю?» Мы можем переформулировать этот вопрос, спросив, насколько сильно удалено от нуля наше z-значение в терминах математической статистики. Согласно H0 z-значение подчиняется стандартному нормальному распределению. Итак, если разница действительно равна нулю, мы увидим, что z-статистика попадает в пределы 2 стандартных отклонений от среднего значения в 95 % случаев. Прямое следствие из этого заключается в том, что если z-значение будет выше или ниже 2 стандартных отклонений, мы можем отклонить нулевую гипотезу с уверенностью 95 %. Давайте посмотрим, как это выглядит на примере наших занятий. # вычисляем z-значение z = diff_mu / diff_se print(z) -2.7792810791031224 x = np.linspace(-4,4,100) y = stats.norm.pdf(x, 0, 1) lbl = "Кривая\nстандартного\nнормального\nраспределения" plt.plot(x, y, label=lbl) plt.vlines(z, ymin=0, ymax=.05, label="z-значение", color="C1") plt.legend() plt.show()
52  Обзор статистик: самое опасное уравнение Полученное z-значение выглядит как довольно экстремальное значение. Действительно, по модулю оно больше 2, а это означает, что вероятность получить такое экстремальное значение, при условии что между группами нет разницы, составляет менее 5 %. Это снова приводит нас к выводу, что переход от очных занятий к онлайн-занятиям приводит к статистически значимому падению успеваемости. Еще одна интересная особенность тестирования гипотез заключается в том, что они менее консервативны, чем проверка на пересечение 95%-ного доверительного интервала для группы объектов, испытавших воздействие, и 95%-ного доверительного интервала для группы объектов, не испытавших воздействия. Другими словами, если доверительные интервалы для обеих групп перекрываются, все равно результат может быть статистически значимым. Например, давайте представим, что группа учеников, посещающих очные занятия, имеет средний балл 80 со стандартной ошибкой 4, а группа учеников, посещающих онлайн-занятия, имеет средний балл 71 со стандартной ошибкой 2. # вычислим доверительные интервалы cont_mu, cont_se = (71, 2) test_mu, test_se = (80, 4) diff_mu = test_mu - cont_mu diff_se = np.sqrt(cont_se**2 + test_se**2) print(f"95%-ный доверительный интервал для контрольной группы:\n" f"{(cont_mu-1.96*cont_se, cont_mu+1.96*cont_se)}") print(f"95%-ный доверительный интервал для тестовой группы:\n" f"{(test_mu-1.96*test_se, test_mu+1.96*test_se)}") print(f"95%-ный доверительный интервал разности:\n" f"{(diff_mu-1.96*diff_se, diff_mu+1.96*diff_se)}") 95%-ный доверительный (67.08, 74.92) 95%-ный доверительный (72.16, 87.84) 95%-ный доверительный (0.23461352820082482, интервал для контрольной группы: интервал для тестовой группы: интервал разности: 17.765386471799175) Если мы построим доверительные интервалы для этих групп, то увидим, что они перекрываются. Верхняя граница 95%-ного доверительного интервала для группы учеников, проходивших онлайн-занятия, составляет 74.92, а нижняя граница 95%-ного доверительного интервала для группы учеников, проходивших очные занятия, равна 72.16. Однако как только мы вычислим 95%-ный доверительный интервал разницы между группами, мы увидим, что он не содержит нуля. Несмотря на то что отдельные доверительные интервалы перекрываются, разница по-прежнему может статистически значимо отличаться от нуля.
P-значения  53 P-значения Ранее я говорил, что вероятность получить такое экстремальное значение составляет менее 5 %, при условии что разница между группой лиц, посещающих онлайн-занятия, и группой лиц, посещающих очные занятия, фактически равна нулю. Но можем ли мы точно оценить, какова эта вероятность? Насколько вероятно, что мы увидим такое экстремальное значение? Пришло время познакомиться с p-значениями! Как и в случае с доверительными интервалами (и на самом деле с большинством статистик в рамках частотного подхода), истинное определение p-значения может быть очень запутанным. Итак, чтобы не рисковать, я скопирую определение из Википедии: «p-значение – вероятность того, что случайная величина, имеющая распределение выбранной статистики при условии верности нулевой гипотезы, примет значение, равное вычисленному значению этой статистики, или более экстремальное». Еще можно сказать так: «p-значение – вероятность того, что случайная величина, имеющая распределение выбранной статистики при условии верности нулевой гипотезы, примет значение, не меньшее, чем вычисленное значение этой статистики». Проще говоря, p-значение – это вероятность наблюдать полученное экстремальное значение, когда нулевая гипотеза верна. Если совсем просто, p-значение измеряет, насколько маловероятно, что вы увидите вычисленное значение, когда нулевая гипотеза верна. Естественно, его часто путают с вероятностью того, что нулевая гипотеза верна. Обратите внимание на разницу. P-значение – это НЕ P(H0 | данные), а Р(данные | H0). P-значение – это не вероятность верности нулевой гипотезы при вычисленном значении, а вероятность наблюдать вычисленное значение при верности нулевой гипотезы. Но не позволяйте этим сложностям обмануть вас. С практической точки зрения, они довольно просты в использовании. На рисунке ниже приведен пример вычисления двустороннего p-значения для произвольной z-статистики (2.2) с последующей визуализацией. Мы воспользовались таблицей z-значений. P-значение равно 0.0278. Речь идет об области под кривой, ограниченной по оси абсцисс точкой – вычисленным значением тестовой статистики 2.2, которую мы умножаем на 2 (0.0139 × 2 = 0.0278), поскольку здесь используем двусторонний тест.
54  Обзор статистик: самое опасное уравнение Таблица z-значений (значения показывают площадь СЛЕВА от z-значения) Плотность вероятности P(z ≥ 2.2) = 0.9861 Pvalue = 1 – P(z < 2.2) = 1 – 0.9861 = 0.0139 × 2 = 0.0278 (умножаем на 2, потому что здесь проводим двусторонний тест) Очень высокая вероятность получить вычисленное значение статистики, когда Н0 верна 0.025 Очень низкая вероятность получить вычисленное значение статистики, когда Н0 верна P(Z ≤ –2.2) = 0.0139 область значений, при которых у нас нет оснований отклонить нулевую гипотезу (область допустимых значений) –1.96 критическая область – (вычисленная тестовая статистика) 0.025 Очень низкая вероятность получить вычисленное значение статистики, когда Н0 верна P(Z ≥ 2.2) = 0.0139 1.96 критические значения (определяются уровнем значимости, здесь 0.05) p-значение = 2(0.0139) = 0.0278 Возможные результаты критическая область (вычисленная тестовая статистика)
­ ­ Ключевые идеи  55 К счастью, у нас есть компьютер, который сделает этот расчет за нас, и нам не нужно будет прибегать к таблицам. В рамках нашего исследования двух типов занятий мы можем просто подставить z-статистику для нашего случая в CDF стандартного нормального распределения. # вычисляем p-значение print("p-значение:", stats.norm.cdf(z)) p-значение: 0.0027239680835563383 Обратите внимание, p-значение интересно тем, что оно позволяет нам не указывать доверительную вероятность, например 95 % или 99 %. Но если мы хотим сообщить о доверительной вероятности, исходя из p-значения, мы точно знаем, с какой степенью достоверности наш тест был пройден или провален. Например, при p-значении, равном 0.0027, мы повышаем уровень значимости до 0.2 %. Таким образом, если 95%-ный доверительный интервал и 99%-ный доверительный интервал для разницы не будут содержать ноль, то 99.9%-ный доверительный интервал будет содержать ноль. Это означает, что вероятность наблюдения данного экстремального значения z-статистики составляет всего 0.2 %, при условии что разница между группами равна нулю (т. е. когда верна нулевая гипотеза). # вычисляем разные доверительные интервалы разности diff_mu = online.mean() - face_to_face.mean() diff_se = np.sqrt(face_to_face.var()/len(face_to_face) + online.var()/len(online)) print("95% ДИ:", (diff_mu - stats.norm.ppf(.975)*diff_se, diff_mu + stats.norm.ppf(.975)*diff_se)) print("99% ДИ:", (diff_mu - stats.norm.ppf(.995)*diff_se, diff_mu + stats.norm.ppf(.995)*diff_se)) print("99.9% ДИ:", (diff_mu - stats.norm.ppf(.9995)*diff_se, diff_mu + stats.norm.ppf(.9995)*diff_se)) 95% ДИ: (-8.376346553082909, -1.4480964433710017) 99% ДИ: (-9.46485353526404, -0.3595894611898709) 99.9% ДИ: (-10.728040658245558, 0.9035976617916459) Ключевые идеи Мы убедились, насколько важно знать уравнение Муавра, и использовали его, чтобы придать нашим оценкам определенную степень достоверности. А именно мы выяснили, что онлайн-занятия являются причиной снижения успеваемости по сравнению с очными занятиями. Мы также увидели, что это был статистически значимый результат. Мы получили его, сравнив доверительные интервалы средних значений для двух групп, взглянув на доверительный интервал разницы, выполнив проверку гипотезы и взглянув
56  Обзор статистик: самое опасное уравнение на p-значение. Давайте объединим все в одну функцию, которая выполняет сравнение путем A/B-тестирования аналогично вышеописанным действиям. # пишем функцию A/B-тестирования def AB_test(test: pd.Series, control: pd.Series, confidence=0.95, h0=0): mu1, mu2 = test.mean(), control.mean() se1, se2 = test.std() / np.sqrt(len(test)), control.std() / np.sqrt( len(control)) diff = mu1 - mu2 se_diff = np.sqrt(test.var() / len(test) + control.var() / len(control)) z_stats = (diff - h0) / se_diff p_value = stats.norm.cdf(z_stats) def critial(se): return -se * stats.norm.ppf((1 - confidence) / 2) print(f"Тест {confidence * 100}% CI: {mu1} +- {critial(se1)}") print(f"Контроль {confidence * 100}% CI: {mu2} +- {critial(se2)}") print(f"Тест-Контроль {confidence * 100}% CI: {diff} +- {critial(se_diff)}") print(f"Z-статистика {z_stats}") print(f"P-значение {p_value}") # применяем нашу функцию, запускаем A/B-тест AB_test(online, face_to_face) Тест 95.0% CI: 73.63526308510637 +- 3.0127770572134565 Контроль 95.0% CI: 78.54748458333333 +- 1.7097768273108 Тест-Контроль 95.0% CI: -4.912221498226955 +- 3.4641250548559537 Z-статистика -2.7792810791031224 P-значение 0.0027239680835563383 Поскольку наша функция достаточно универсальна, мы можем проверить и другие нулевые гипотезы. Например, можем ли мы попытаться отклонить гипотезу о том, что разница в успеваемости между группой лиц, посещавших онлайн-занятия, и группой лиц, посещавших очные занятия, составляет –1? Получив результаты, мы можем с 95%-ной уверенностью сказать, что разница является значимой. Но мы не можем сказать это с 99%-ной уверенностью. # запускаем A/B-тест с другой нулевой гипотезой AB_test(online, face_to_face, h0=-1) Тест 95.0% CI: 73.63526308510637 +- 3.0127770572134565 Контроль 95.0% CI: 78.54748458333333 +- 1.7097768273108 Тест-контроль 95.0% CI: -4.912221498226955 +- 3.4641250548559537 Z-статистика -2.2134920404560883 P-значение 0.013431870694630114
Глава 4 Графовые причинно-следственные (каузальные) модели Рассуждая о причинности Вы когда-нибудь замечали, как авторы кулинарных видеороликов на YouTube превосходно описывают приготовление еды? «Уваривайте соус до тех пор, пока он не приобретет бархатистую консистенцию». Если вы только учитесь готовить, вы даже не представляете, что это означает. Просто скажите мне, сколько времени эта штука должна стоять у меня на плите! С причинностью то же самое. Предположим, вы заходите в бар и слышите, как люди обсуждают причинность (вероятно, бар находится рядом с экономическим факультетом). В этом случае вы услышите, как они говорят, что спутывающий фактор в виде дохода затрудняет определение влияния иммиграции на данный район, поэтому им пришлось использовать инструментальную переменную. И сейчас вы, возможно, не понимаете, о чем они говорят. Однако я постараюсь хотя бы частично решить эту проблему прямо сейчас. Графовые модели – это язык причинности. Вы можете ими воспользоваться не только для того, чтобы поговорить с другими смелыми и честными поклонниками причинности, но и для того, чтобы внести ясность в свои собственные мысли.
58  Графовые причинно-следственные (каузальные) модели В качестве отправной точки возьмем, например, условную независимость потенциальных результатов. Это одно из основных предположений, которое нам потребуется, чтобы быть честным, делая причинно-следственный вывод: (Y0, Y1) ⊥ T | X. Условная независимость позволяет нам измерить эффект (т. е. влияние на результат) исключительно за счет воздействия, а не за счет какой-либо другой скрытой переменной. Классический пример – воздействие лекарства на больного. Если лекарство получают только тяжелобольные пациенты, может даже показаться, что прием лекарства ухудшает здоровье пациентов. Это происходит в силу того, что эффект тяжести болезни смешивается с эффектом препарата. Если мы разобьем пациентов на тяжелых и нетяжелых и проанализируем влияние препарата в каждой подгруппе, то получим более четкую картину фактического эффекта. Это разбиение популяции по той или иной характеристике мы называем контролем Х, учетом Х в условиях задачи, обусловленностью X. При учете тяжести случая механизм лечения становится практически случайным. Пациенты в тяжелой группе могут получить или не получить препарат только случайно, а не из-за высокой степени тяжести, поскольку все пациенты одинаковы в этом измерении. И если лечение назначается случайным образом внутри групп, оно становится условно независимым от потенциальных результатов. Независимость и условная независимость являются центральными понятиями для причинно-следственного вывода. Тем не менее разобраться в них может быть довольно сложно. Но все поменяется, если мы воспользуемся правильным языком для описания данной проблемы. Здесь нам на помощь приходят причинно-следственные (каузальные) графовые модели (causal graph models). Причинно-следственная графовая модель – это способ представить, как работает причинность, способ разобраться в том, что на что влияет. Графовая модель выглядит следующим образом. import graphviz as gr g = gr.Digraph() g.edge("Z", "X") g.edge("U", "X") g.edge("U", "Y") g.edge("лекарство", "выжившие") g.edge("тяжесть заболевания", "выжившие") g.edge("тяжесть заболевания", "лекарство") g
Ускоренный курс по графовым моделям  59 Z U X Y тяжесть заболевания лекарство выжившие Каждый узел является случайной величиной. Мы используем стрелки или ребра, чтобы показать, влияет ли одна переменная на другую. В первой графовой модели, расположенной выше, мы говорим, что Z влияет на X, а U влияет на X и Y. Давайте рассмотрим более конкретный пример. Мы представим рассуждения о влиянии лекарства на выживаемость пациентов в виде второго графа, расположенного на рисунке вверху справа. Тяжесть заболевания влияет и на лекарство, и на выживаемость, а лекарство также влияет на выживаемость. Позже мы увидим, как эти причинно-следственные графовые модели помогут нам сделать наши представления о причинности более ясными, поскольку они проясняют наши представления о том, как устроен мир. Ускоренный курс по графовым моделям Графовым моделям посвящают целые семестры https://www.coursera.org/specializations/probabilistic-graphical-models. Но для наших задач просто (очень) важно понимать, какие предположения о независимости и условной независимости выдвигает графовая модель. Как мы увидим, независимость распространяется в графовой модели подобно тому, как вода течет в потоке. Мы можем выключить этот поток или включить его, в зависимости от того, как мы обращаемся с переменными в нем. Для лучшего понимания давайте рассмотрим некоторые распространенные графовые структуры и примеры. Они будут довольно простыми, но их будет достаточно, чтобы получить исчерпывающую информацию о независимости и условной независимости в контексте графовых моделей. Условную независимость мы будем обозначать символом ⊥, а условную зависимость – символом ⊥/. Во-первых, посмотрите на этот очень простой граф внизу. Такую структуру еще называют цепочкой (chain). A является причиной B, B является причиной C. Или X является причиной Y, которая является причиной Z. g = gr.Digraph() g.edge("A", "B") g.edge("B", "C")
60  Графовые причинно-следственные (каузальные) модели g.edge("X", "Y") g.edge("Y", "Z") g.node("Y", "Y", color="red") g.edge("знание каузального анализа", "решение задач") g.edge("решение задач", "продвижение по службе") g A X знание каузального анализа B Y решение задач C Z продвижение по службе На первом графе зависимость следует в направлении стрелок. Обратите внимание, что зависимость симметрична, хотя данный факт менее интуитивно понятен. Чтобы привести более конкретный пример, предположим, что знание каузального анализа – единственный способ решить бизнес-задачи, а решение этих бизнес-задач – единственный способ получить продвижение по службе. Таким образом, знание каузального анализа подразумевает решение задач, которое приводит к продвижению по службе. Здесь мы можем сказать, что продвижение по службе зависит от знания каузального анализа. Чем выше экспертиза в каузальном анализе, тем выше ваши шансы на повышение по службе. Кроме того, чем выше ваши шансы на продвижение по службе, тем больше у вас шансов обладать знаниями в области каузального анализа. В противном случае получить повышение будет сложно. Теперь предположим, что я ввожу обусловленность промежуточной переменной. В этом случае зависимость блокируется. Таким образом, X и Z независимы при заданной переменной Y. На вышеприведенном графе красным цветом показано, что Y является обусловленной переменной. Точно так же и в нашем примере: если я знаю, что вы хорошо решаете задачи, знание того, что вы обладаете экспертизой в области каузального анализа, не дает мне никакой дополнительной информации о ваших шансах на продвижение по службе. С математической точки зрения: E[Продвижение по службе | Решение задач, Знание каузального анализа] = Е[Продвижение по службе | Решение задач]. Верно и обратное. Если я знаю о ваших способностях в области решения задач, знание о вашем карьерном росте не дает мне никакой дополнительной информации о том, какова вероятность, что вы знаете каузальный анализ.
­ Ускоренный курс по графовым моделям  61 Как правило, поток зависимости на прямом пути от A к C блокируется, когда мы вводим обусловленность промежуточной переменной B. Или A ⊥ /C и A ⊥ C | B. Теперь давайте рассмотрим структуру вилка (fork). Одна и та же переменная является причиной двух других переменных в нижней части графа. Можно еще сказать, что у интересующих нас переменных есть общая причина. В этом случае зависимость распространяется в обратном направлении по стрелкам, и у нас есть обходной путь (backdoor path). Мы можем перекрыть обходной путь и выключить зависимость, обусловив общую причину. g = gr.Digraph() g.edge("C", "A") g.edge("C", "B") g.edge("X", "Y") g.edge("X", "Z") g.node("X", "X", color="red") g.edge("статистика", "знание каузального анализа") g.edge("статистика", "машинное обучение") g C A статистика X B Y Z знание каузального анализа машинное обучение Например, ваши знания в области статистики влияют на более высокий уровень знаний в каузальном анализе и машинном обучении. Если я не знаю вашего уровня знаний в области статистики, то знание того, что вы хорошо разбираетесь в каузальном анализе, повышает вероятность того, что вы также хорошо разбираетесь в машинном обучении. Это объясняется тем, что даже если я не знаю вашего уровня знаний в области статистики, я могу вывести его из ваших знаний каузального анализа. Если вы хорошо разбирае тесь в каузальном анализе, вы, вероятно, хорошо разбираетесь в статистике, что повышает вероятность того, что вы хорошо разбираетесь в машинном обучении. Теперь, если я поставлю условием ваши знания в области статистики, то ваши знания о машинном обучении становятся независимыми от ваших знаний о каузальном анализе. Знание вашего уровня статистики уже дает мне всю необходимую информацию, чтобы сделать вывод об уровне ваших
62  Графовые причинно-следственные (каузальные) модели знаний в области машинного обучения. Вы видите, что в данном случае знание вашего уровня экспертизы в каузальном анализе не даст никакой дополнительной информации. Как правило, две переменные, имеющие общую причину, являются зависимыми, но независимыми, когда мы ставим условием общую причину. Или A ⊥ /B и A ⊥ B | C. Единственная структура, которой не хватает, – это коллайдер (collider). Коллайдер – это переменная, на которую влияют две переменные. Стрелки от переменных «сталкиваются» в одной переменной. Коллайдер еще называют перевернутой вилкой (inverted fork). Мы можем сказать, что в данном случае обе переменные имеют общее следствие. g = gr.Digraph() g.edge("B", "C") g.edge("A", "C") g.edge("Y", "X") g.edge("Z", "X") g.node("X", "X", color="red") g.edge("знание статистики", "продвижение по службе") g.edge("лесть", "продвижение по службе") g B A C Y Z X знание статистики лесть продвижение по службе Допустим, существует два способа получить повышение по службе. Вы можете либо хорошо разбираться в статистике, либо уметь льстить своему боссу. Если я не принимаю за условие коллайдер – ваше продвижение по службе, то есть я ничего не знаю, получите вы его или нет, то уровни ваших знаний в области статистики и лести не зависят друг от друга. Другими словами, информация о вашем уровне знаний в области статистики ничего не говорит мне о вашем умении льстить своему боссу. С другой стороны, если вы действительно получили повышение по службе, внезапно информация о вашем уровне знаний в области статистики позволяет судить о вашем умении льстить боссу. Если вы плохо разбираетесь в статистике и все-таки полу-
Ускоренный курс по графовым моделям  63 чили повышение, вы, вероятно, умеете льстить. В противном случае вы не получили бы повышения. И наоборот, если вы плохо умеете льстить, значит, вы хороши в статистике. Обусловленность коллайдером иногда называют парадоксом Берксона, ошибкой коллайдера, попутным объяснением (explaining away), эффектом оправдания, редукцией причины, потому что одна причина уже объясняет следствие, делая другую причину менее вероятной. Нетрудно понять, что обусловленность коллайдером может привести к ложным корреляциям. Как правило, когда мы ставим условием коллайдер, мы открываем путь зависимости. В противном случае мы закрываем путь зависимости. Или и A ⊥ /B A ⊥ /B | C. Чтобы еще лучше понять это, представьте, что мы с вами работаем на водопроводной станции. Каждый из нас может контролировать скорость потока в нашей трубе (X и Y), и наши трубы соединены так, что конечная скорость потока является суммарной скоростью потоков в наших трубах, Z. Я могу вращать свой вентиль независимо от вас, и наоборот. Если вы увеличите скорость, это увеличит поток в Z, но на меня это не повлияет, поэтому наши скорости (X и Y) не должны зависеть друг от друга. Однако затем звонит наш менеджер и говорит нам, что мы должны поддерживать определенную скорость потока в Z (начальство фиксирует Z или ставит условием Z, можно еще сказать, вводит обусловленность Z). Теперь, если я увеличу скорость потока в своей трубе, то вам придется уменьшить скорость потока в своей трубе, чтобы сохранить требуемую скорость в Z. И если вы уменьшите скорость потока в своей трубе, я должен увеличить скорость потока в своей трубе. Таким образом, кто-то, наблюдающий X и Y, увидит зависимость.
  64  Графовые причинно-следственные (каузальные) модели На примере с водонапорной станцией мы наглядно убедились, путь зависимости блокируется коллайдером, если мы не контролируем его (не ставим условием), в противном случае мы открываем путь зависимости. Изучив три структуры, мы можем вывести еще более общее правило. Путь зависимости блокируется тогда и только тогда, когда: 1) он содержит элемент, отличный от коллайдера, который принят за условие; 2) он содержит коллайдер, который не был принят за условие, и не имеет потомков, которые были приняты за условия. Ниже приведена шпаргалка, которая посвящена тому, как зависимость распределяется по графу. Я взял ее из Стэнфордской презентации Марка Паскина http://ai.stanford.edu/~paskin/gm-short-course/lec2.pdf. Стрелки с черточками на концах означают независимость, а стрелки без черточек на концах – зависимость. В качестве последнего примера попытайтесь выяснить некоторые отношения независимости и зависимости в нижеприведенном графе причинноследственных связей. 1. 2. 3. 4. 5. 6. D ⊥ C? D ⊥ C | A? D ⊥ C | G? A ⊥ F? A ⊥ F | E? A ⊥ F | E, C?
 ­  Ускоренный курс по графовым моделям  65 # создаем граф g = gr.Digraph() g.edge("C", "A") g.edge("C", "B") g.edge("D", "A") g.edge("B", "E") g.edge("F", "E") g.edge("A", "G") g D C A B G F E Ответы: 1. D ⊥ C. Путь содержит коллайдер, который не принят за условие. 2. D ⊥ /C | A. Путь содержит коллайдер, который принят за условие. 3. D ⊥ /C | G. Путь содержит потомка коллайдера, который принят за ус ловие. 4. A ⊥ F. Путь содержит коллайдер B->E<-F, который не принят за условие. 5. A ⊥ /F | E. Путь содержит коллайдер B->E<-F, который принят за условие. 6. A ⊥ F | E, C. Путь содержит коллайдер B->E<-F, который принят за условие, но содержит элемент, отличный от коллайдера, который принят за условие. Обусловленность E открывает путь, но обусловленность C снова закрывает его. Знание причинно-следственных графовых моделей позволяет нам понять проблемы, возникающие в ходе причинно-следственного вывода. Как мы видели, проблема всегда сводится к смещению: E[Y | T = 1] – E[Y | T = 0] = E[Y1 – Y0 | T = 1] + E[Y0 | T = 1] – E[Y0 | T = 0]. АТТ Смещение Графовые модели позволяют нам выяснить, со смещением какого вида мы имеем дело и какие средства нам нужны для его корректировки.
 66  Графовые причинно-следственные (каузальные) модели Смещение, вызванное спутывающими факторами (ошибка спутывания, confounding bias) «Это действительно трудное решение. Потому что вы оба – отстой». Оба смещения – смещение из-за спутывающих факторов и смещение из-за отбора – усложняют причинноследственный анализ Первая существенная причина смещения – спутывающие факторы (confounding). Это происходит, когда воздействие и результат имеют общую причину. Например, предположим, что воздействие – это образование, а результат – доход. Трудно понять причинно-следственный эффект – влияние образования на заработную плату, потому что у обоих есть общая причина – интеллект. Таким образом, мы можем утверждать, что более образованные люди зарабатывают больше не в силу самого образования, а потому что обладают более высоким интеллектом. Нам нужно перекрыть все обходные пути между воздействием и результатом, чтобы определить причинно-следственный эффект. Если мы так поступим, единственным эффектом, который у нас останется, будет непосредственный эффект T->Y. В нашем примере, если мы контролируем интеллект, то есть сравниваем людей с одинаковым уровнем интеллекта, но с разным уровнем образования, разница в результатах будет обусловлена только разницей в уровне образования, поскольку интеллект будет одинаковым для всех. Чтобы устранить смещение, вызванное спутыва-
­ Смещение, вызванное спутывающими факторами  67 ющим фактором, нам нужно проконтролировать все общие причины, влия ющие на воздействие и результат. g = gr.Digraph() g.edge("X", "T") g.edge("X", "Y") g.edge("T", "Y") g.edge("Интеллект", "Образование"), g.edge("Интеллект", "Зарплата"), g.edge("Образование", "Зарплата") g X T Интеллект Образование Y Зарплата К сожалению, не всегда можно проконтролировать все общие причины. Иногда существуют неизвестные причины или известные причины, которые мы не можем измерить. Случай с интеллектом относится к числу последних. Несмотря на все усилия, ученые до сих пор не выяснили, как правильно измерять интеллект. Здесь я буду использовать символ U для обозначения неизмеряемых переменных. Теперь предположим на мгновение, что интеллект не может напрямую повлиять на ваше образование. Он влияет на балл SAT1, а SAT определяет ваш уровень образованности, поскольку он открывает возможность поступления в хороший колледж. Даже если мы не можем проконтролировать неизмеряемый интеллект, мы можем проконтролировать SAT и закрыть этот обходной путь. g = gr.Digraph() g.edge("X1", "T") g.edge("T", "Y") g.edge("X2", "T") 1 SAT Reasoning Test (а также «Scholastic Aptitude Test» и «Scholastic Assessment Test», дословно «Академический оценочный тест») – стандартизованный тест для приема в высшие учебные заведения в США.
68  Графовые причинно-следственные (каузальные) модели g.edge("X1", "Y") g.edge("U", "X2") g.edge("U", "Y") g.edge("Семейный доход", "Образование") g.edge("Образование", "Зарплата") g.edge("SAT", "Образование") g.edge("Семейный доход", "Зарплата") g.edge("Интеллект", "SAT") g.edge("Интеллект", "Зарплата") g U X1 X2 Интеллект Семейный доход SAT T Образование Y Зарплата На графе выше (слева) контроля X1 и X2 (в качестве X1 и X2 можно взять SAT и семейный доход) будет достаточно, чтобы перекрыть все обходные пути между воздействием и результатом. Другими словами, (Y0, Y1) ⊥ T | X1, X2. Таким образом, даже если мы не можем измерить все общие причины, мы все же можем достичь условной независимости, если проконтролируем измеряемые переменные, которые опосредуют влияние неизмеряемых переменных на воздействие. Одно небольшое замечание: у нас еще есть (Y0, Y1) ⊥ T | X1, U, но поскольку мы не можем наблюдать переменную U, мы не можем ее проконтролировать. А что, если все-таки можем? Что, если неизмеряемая переменная напрямую влияет на воздействие и результат? На графе выше справа интеллект влияет на образование и зарплату. Таким образом, возникает спутывание в отношениях между воздействием – образованием и результатом – заработной платой. В этом случае мы не можем контролировать спутывающий фактор, потому что его нельзя измерить. Однако у нас есть другие измеряемые переменные, которые могут выступать в качестве прокси спутывающего фактора. Эти переменные не устранят обходной путь, но контроль над ними поможет снизить смещение (хотя и не устранит его). Эти переменные иногда называют суррогатными спутывающими факторами.
Смещение из-за отбора (ошибка отбора, selection bias)  69 В нашем примере мы не можем измерить интеллект, но можем измерить некоторые причины, влияющие на него, например образование отца и матери, и некоторые его следствия, такие как IQ или балл, полученный по итогам SAT. Контроль этих суррогатных переменных недостаточен для устранения смещения, но помогает. g = gr.Digraph() g.edge("X", "U") g.edge("U", "T") g.edge("T", "Y") g.edge("U", "Y") g.edge("Интеллект", g.edge("Интеллект", g.edge("Образование g.edge("Образование "IQ") "SAT") отца", "Интеллект") матери", "Интеллект") g.edge("Интеллект", "Образование") g.edge("Образование", "Зарплата") g.edge("Интеллект", "Зарплата") g X Образование отца U T Интеллект IQ Y Образование матери SAT Образование Зарплата Смещение из-за отбора (ошибка отбора, selection bias) Вам может показаться хорошей идеей добавить в модель все, что вы можете измерить, просто чтобы убедиться, что у вас нет смещения, вызванного спутывающими факторами. Ну, подумайте еще раз.
70  Графовые причинно-следственные (каузальные) модели Этот мем высмеивает исследователей, которые для устранения смещения в силу спутывающих факторов пытаются включить все переменные и сталкиваются со смещением из-за отбора Второй существенный источник смещения – это смещение из-за отбора. Я думаю, что проведение различия между смещением из-за спутывающих факторов и смещением из-за отбора является резонным, поэтому буду придерживаться его. Если смещение из-за спутывающих факторов возникает, когда мы не учитываем общую причину, то смещение из-за отбора больше связано со следствием. Сделаю одно предостережение: экономисты склонны называть все виды смещения смещением из-за отбора. Часто смещение из-за отбора возникает, когда мы учитываем больше переменных, чем должны. Может случиться так, что воздействие и потенциальный результат маргинально независимы, но становятся зависимыми, как только мы ставим условием коллайдер. Представьте, что с помощью какого-то чуда вы, наконец, можете рандомизировать образование, чтобы измерить его влияние на заработную плату. Однако, чтобы исключить спутывающие факторы, вы контролируете много переменных. Среди них вы контролируете инвестиции. Но инвестиции не являются общей причиной образования и заработной платы. Наоборот, инвестиции – это следствие того и другого. Более образованные люди больше зарабатывают и больше инвестируют. Кроме того, те, кто больше зарабатывают, больше инвестируют. Поскольку инвестиции – это коллайдер, поставив его условием, вы открываете второй путь между воздействием и результатом, что затрудняет измерение прямого эффекта. Представьте следующую картину: контролируя инвестиции, вы анализируете небольшие группы популяции, которые характеризуются одинаковым уровнем инвестиций, а затем обнаруживаете влияние образования на эти группы. Но тем самым вы также косвенно и непреднамеренно не позволяете заработной плате сильно меняться. В результате вы не сможете увидеть, как образование меняет заработную плату, потому что вы не позволяете заработной плате меняться должным образом.
Смещение из-за отбора (ошибка отбора, selection bias)  71 g = gr.Digraph() g.edge("T", "X") g.edge("T", "Y") g.edge("Y", "X") g.node("X", "X", color="red") g.edge("Образование", "Инвестиции") g.edge("Образование", "Зарплата") g.edge("Зарплата", "Инвестиции") g T Образование Y X Зарплата Инвестиции Представьте, что инвестиции и образование принимают только 2 значения. Люди либо инвестируют, либо не инвестируют. У людей либо есть образование, либо нет образования. Первоначально, когда мы не контролируем инвестиции, смещение равно нулю: E[Y0 | T = 1] – E[Y0 | T = 0] = 0, потому что образование рандомизировано. Это означает, что заработная плата людей, получивших образование, если бы они не получили образование (E[Y0 | T = 1]), будет равна заработной плате людей, не получивших образование (E[Y0 | T = 0]). Но что произойдет, если мы поставим условием инвестиции? Глядя на тех, кто инвестирует, мы, вероятно, получим, что E[Y0 | T = 0, I = 1] > E[Y0 | T = 1, I = 1]. Другими словами, среди людей, которые инвестируют, независимо от их уровня образования, те, кому удается делать это даже без образования, с большей вероятностью добьются высоких заработков. По этой причине заработная плата, которую получают эти люди, Wage0 | T = 0, вероятно, выше, чем заработная плата группы образованных, если бы у них не было образования Wage0 | T = 1. Аналогичное рассуждение можно применить к тем, кто не инвестирует, где у нас, вероятно, также будет E[Y0 | T = 0, I = 0] > E[Y0 | T = 1, I = 0]. У людей, которые не инвестируют, даже получив образование, вероятно, была бы более низкая заработная плата, если бы они не получили образования, чем у людей, которые не инвестируют, но при этом не имеют образования.
72  Графовые причинно-следственные (каузальные) модели Если человек инвестирует, то информация о наличии у него высшего образования попутно объясняет вторую причину – заработную плату. Обусловленное инвестициями, высшее образование ассоциируется с низкой заработной платой, и у нас есть отрицательное смещение E[Y0 | T = 0, I = i ] > E[Y0 | T = 1, I = i ]. Заметим, что все, что мы обсуждали, верно, если мы ставим условием любого потомка общего следствия. g = gr.Digraph() g.edge("T", "X") g.edge("T", "Y") g.edge("Y", "X") g.edge("X", "S") g.node("S", "S", color="red") g T Y X S То же самое происходит, когда мы ставим условием медиатор воздействия. Медиатор – это переменная, стоящая между воздействием и результатом. Она опосредует причинно-следственный эффект. Например, вновь предположим, вы можете рандомизировать образование. Для надежности вы решаете проконтролировать переменную – наличие у человека работы «белого воротничка». Опять же, эта обусловленность вносит смещение в оценку причинно-следственной связи. На этот раз не потому, что она открывает входную дверь с коллайдером, а потому, что она закрывает один из каналов, по которым осуществляется воздействие. В нашем примере получение работы «белого воротничка» – это один из способов, с помощью которого
Смещение из-за отбора (ошибка отбора, selection bias)  73 повышение уровня образования приводит к повышению заработной платы. Контролируя данную переменную, мы закрываем этот канал и оставляем открытым только прямое влияние образования на заработную плату. g = gr.Digraph() g.edge("T", "X") g.edge("T", "Y") g.edge("X", "Y") g.node("X", "X", color="red") g.edge("образование", "белый воротничок") g.edge("образование", "зарплата") g.edge("белый воротничок", "зарплата") g T X Образование Белый воротничок Y Зарплата Мы знаем, что в силу рандомизации смещение из-за отбора равно нулю E[Y0 | T = 0] – E[Y0 | T = 1] = 0. Однако если мы поставим условием наличие работы «белого воротничка», то получим, что E[Y0 | T = 0, WC = 1] > E[Y0 | T = 1, WC = 1]. Это объясняется тем, что люди, которым удается устроиться на работу «белым воротничком» даже без образования, вероятно, более трудолюбивы, чем те, кому требуется образование, чтобы получить ту же самую работу. По той же логике, E[Y0 | T = 0, WC = 0] > E[Y0 | T = 1, WC = 0], потому что люди, которые не получили работу «белого воротничка», даже с образованием, вероятно, менее трудолюбивы, чем те, кто не получил, при этом не имея никакого образования. В нашем случае обусловленность медиатором индуцирует отрицательное смещение. Из-за этого эффект образования кажется менее существенным, чем он есть на самом деле. Это происходит потому, что причинно-следственный эффект положителен. Если бы эффект был отрицательным, обусловленность медиатором давала бы положительное смещение. Во всех случаях такого рода обусловленность делает эффект слабее, чем он есть на самом деле. Говоря более прозаично, предположим, что вам нужно выбрать между двумя кандидатами на работу в вашей компании. У обоих – одинаково впечат-
74  Графовые причинно-следственные (каузальные) модели ляющие профессиональные достижения, но у одного кандидата нет высшего образования. Кого из них выбрать? Конечно, вы должны выбрать кандидата, у которого нет высшего образования, потому что он сумел добиться того же, что и другой кандидат, хотя шансы были не в его пользу. «Почему, когда что-то происходит, это всегда вы трое?» Героям фильмов о Гарри Поттере соответствуют две разновидности смещения из-за отбора (Гермиона Грейнджер и Гарри Поттер) и смещение из-за спутывания (Рон Уизли) Ключевые идеи Мы изучили графовые модели в качестве языка, позволяющего лучше понимать и выражать идеи причинности. Мы дали краткий обзор правил условной независимости для графа. Это помогло нам исследовать три явления, которые могут привести к смещению. Первое явление – это смещение из-за спутывания. Оно происходит, когда воздействие и результат имеют общую причину, которую мы не учитываем и не контролируем. Второе явление – смещение из-за отбора. Оно возникает в силу обусловленности общим следствием. Третье явление также является видом смещения из-за отбора, на этот раз в силу чрезмерного контроля медиаторных переменных. Этот чрезмерный контроль мог привести к смещению, даже если воздействие было назначено случайным образом. Смещение из-за отбора часто можно исправить, просто ничего не делая, вот почему оно опасно. Поскольку все мы склонны действовать, мы часто считаем идеи о контроле тех или иных переменных разумными, хотя они могут принести больше вреда, чем пользы.
­ Глава 5 Поразительная эффективность линейной регрессии Все, что вам нужно, – это регрессия Рассматривая причинно-следственные связи, мы увидели, что для каждого человека существует два потенциальных результата: Y0 – результат, который получил бы человек, если бы на него/нее не воздействовали (не лечили бы его/ее), а Y1 – результат, который получил бы человек, если бы на него/нее воздействовали (лечили бы его/ее). Значение воздействия Т, равное 0 или 1, претворяет в жизнь один из потенциальных результатов и закрывает для нас возможность когда-либо узнать альтернативный результат. Это приводит к тому, что индивидуальный эффект воздействия (лечения) τi = Y1i – Y0i неизвестен. Yi = Y0i + T i(Y1i – Y0i) = Y0i (1 – T i) + T iY1i. Итак, давайте сейчас сосредоточимся на более простой задаче оценки усредненного причинно-следственного эффекта. Понимая это, мы принимаем тот факт, что некоторые люди реагируют на воздействие (лечение) лучше, чем другие, но мы также признаем, что не знаем, кто эти люди. Вместо этого мы просто попробуем посмотреть, работает ли воздействие (лечение) в среднем: ATE = E[Y1 – Y0]. Это даст нам упрощенную модель с постоянным эффектом воздействия Y 1i = Y 0i + κ. Если значение κ положительно, мы можем сказать, что воз-
76  Поразительная эффективность линейной регрессии действие имеет в среднем положительный эффект. Даже если некоторые люди отреагируют на воздействие плохо, в среднем влияние будет положительным. Кроме того, напомним, что мы не можем просто оценить E[Y1 – Y0] с помощью разности средних E[Y | T = 1] – E[Y | T = 0] из-за смещения. Смещение часто возникает, когда группа объектов, испытавших воздействие, и группа объектов, не испытавших воздействия, отличаются друг от друга по причинам, не связанным с самим воздействием. Один из способов увидеть смещение – посмотреть, как группы различаются по потенциальному результату Y0: E[Y | T = 1] – E[Y | T = 0] = E[Y1 – Y0 | T = 1] + E[Y0 | T = 1] – E[Y0 | T = 0]. АТТ Смещение Ранее мы выяснили, как можно устранить смещение с помощью случайных экспериментов, или рандомизированных контролируемых испытаний – РКИ (Randomised Controlled Trials – RCT), как их иногда называют. РКИ позволяет сделать группу объектов, подвергнутых воздействию, и группу объектов, не подвергнутых воздействию, сопоставимыми, и поэтому смещение исчезает. Мы также выяснили, как учесть неопределенность наших оценок эффекта воздействия. Мы сравнили успеваемость по итогам онлайн-занятий и офлайн-занятий, где T = 0 соответствует офлайн-занятиям, T = 1 – онлайн-занятиям. Студентам случайным образом назначался один из этих двух типов лекций, а затем оценивалась их успеваемость на экзамене. Мы написали функцию A/B-тестирования, которая позволяла сравнить обе группы, определить средний эффект воздействия и даже вычислить доверительный интервал для него. Теперь пришло время увидеть, что мы можем сделать все это с помощью рабочей лошадки причинно-следственного вывода – линейной регрессии! Если бы сравнение среднего значения результата для группы объектов, подвергнутых воздействию, со средним значением результата для группы объектов, не подвергнутых воздействию, было яблоком на десерт, то линейная регрессия была бы холодным тирамису со сливками. Или если сравнение группы объектов, подвергнутых воздействию, и группы объектов, не подвергнутых воздействию, – это печальная и старая буханка белого чудо-хлеба, то линейная регрессия – это деревенский хлеб с воздушным мякишем и хрустящей корочкой на закваске, испеченный самим Чадом Робертсоном.
Все, что вам нужно, – это регрессия  77 Ты: Парень, о котором она говорит, что тебе не стоит волноваться: Посмотрим, как работает эта красота. В нижеприведенном программном коде мы хотим выполнить точно такой же сравнительный анализ эффективности онлайн- и офлайн-занятий. Но вместо того, чтобы применять математику доверительных интервалов, мы просто запускаем регрессию. Более конкретно, мы оцениваем следующую модель: exami = β0 + κOnlinei + ui. Это означает, что мы моделируем результат экзамена как константу β0 плюс κ, если занятия проходят онлайн. Конечно, результат экзамена зависит от дополнительных переменных (таких как настроение ученика в день экзамена, количество часов, потраченных на подготовку, и так далее). Однако здесь мы не вникаем в эти зависимости. Вместо этого мы используем член ui для обозначения всего остального, не волнующего нас. Он называется ошибкой модели. Обратите внимание, что Online является индикатором воздействия и, следовательно, представляет из себя дамми-переменную. Он равен нулю, когда занятия являются очными, и 1, если занятия проводятся онлайн. Учитывая это, мы можем увидеть, что линейная регрессия оценивает E[Y | T = 0] = β0 и E[Y | T = 1] = β0 + κ. κ будет нашим ATE. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np import statsmodels.formula.api as smf import graphviz as gr %matplotlib inline
78  Поразительная эффективность линейной регрессии # загружаем данные, исключив занятия смешанного формата data = pd.read_csv("data/online_classroom.csv").query("format_blended==0") data.head() # обучаем модель регрессии result = smf.ols('falsexam ~ format_ol', data=data).fit() result.summary().tables[1] Это просто удивительно. Мы можем не только оценить ATE, но и бесплатно получаем для него доверительные интервалы и p-значения! Более того, мы видим, что регрессия делает именно то, что и должна делать: сравнивает E[Y | T = 0] и E[Y | T = 1]. Константа – это в точности среднее значение зависимой переменной falsexam (успеваемости) по выборке, когда T = 0, E[Y | T = 0], а регрессионный коэффициент при переменной format_ol (индикаторе онлайн-занятий) в точности соответствует разнице средних значений E[Y | T = 1] – E[Y | T = 0], вычисленной по выборке. Не верите мне? Без проблем. Вы можете убедиться в этом сами. # вычисляем групповые средние (data .groupby("format_ol") ["falsexam"] .mean()) format_ol 0 78.547485 1 73.635263 Name: falsexam, dtype: float64 Как и ожидалось. Если к константе добавить ATE, то есть регрессионный коэффициент при индикаторе онлайн-занятий, то получится выборочное среднее для группы объектов, подвергнутых воздействию: 78.5475 + (–4.9122) = 73.635263.
Теоретические аспекты регрессии  79 Теоретические аспекты регрессии Я не собираюсь слишком глубоко углубляться в то, как строится и оценивается линейная регрессия. Однако немного теории поможет объяснить эффективность ее применения в причинно-следственном анализе. Прежде всего регрессия решает теоретическую задачу получения наилучшего линейного прогноза. Пусть β * – это вектор параметров: Линейная регрессия находит параметры, которые минимизируют среднеквадратичную ошибку (MSE). Воспользовавшись дифференцированием, мы придем к тому, что решение вышеупомянутой задачи описывается формулой β* = E[X′i Xi ]–1E[X′i Yi ]. Теперь получим эквивалент для выборки: β̂ = (X′X )–1X′Y. Но не верьте мне на слово. Если вы из числа тех, кто понимает программный код лучше, чем формулы, попробуйте сами. # создаем массив предикторов и массив меток X = data[["format_ol"]].assign(intercep=1) y = data["falsexam"] # пишем функцию на основе формулы решения def regress(y, X): return np.linalg.inv(X.T.dot(X)).dot(X.T.dot(y)) # получаем регрессионный коэффициент и константу beta = regress(y, X) beta array([-4.9122215 , 78.54748458]) Вышеприведенные формулы являются довольно общими. Однако стоит изучить случай, когда у нас есть только один регрессор. В ходе анализа причинно-следственных связей мы часто хотим оценить причинно-следственное воздействие переменной T на результат y. Итак, мы строим регрессию с одной переменной для оценки ее эффекта. Даже если мы включим в модель другие переменные, они обычно являются просто вспомогательными. Добавление других переменных может помочь нам оценить причинно-следственный эффект воздействия, но мы не очень заинтересованы в оценке их параметров.
80  Поразительная эффективность линейной регрессии При наличии единственного регрессора T связанный с ним параметр вычисляется по формуле Если T присваивается случайным образом, то β1 – это ATE. b_1 = data["falsexam"].cov(data["format_ol"]) / data["format_ol"].var() b_1 -4.912221498226952 Если у нас больше одного регрессора, мы можем расширить формулу yi = β0 + κT i + ui. Давайте предположим, что другие переменные являются просто вспомогательными и мы действительно заинтересованы только в оценке параметра κ, связанного с T: yi = β0 + κT i + β1X1i + … + βk Xki + ui. κ можно получить с помощью следующей формулы: где T̃ i – это остаток от регрессии всех остальных ковариат X1i + … + Xki на T i . А теперь давайте оценим, насколько это круто. Это означает, что коэффициент множественной регрессии является двумерным коэффициентом того же самого регрессора после учета влияния других переменных в модели. Говоря терминами причинно-следственного анализа, κ – это двумерный коэффициент для T после использования всех других переменных для его прогнозирования. За этим определением стоит понятная интуиция. Если мы сможем предсказать T, используя другие переменные, это означает, что T не является случайной величиной. Однако мы можем сделать так, что T будет эквивалентна случайной величине, проконтролировав другие доступные переменные. Для этого мы используем линейную регрессию, чтобы предсказать T по другим переменным, а затем берем остатки этой регрессии T̃. По определению T̃ нельзя предсказать с помощью других переменных X, которые мы уже использовали для прогнозирования T. Довольно элегантно, T̃ – это вариант воздействия, который не связан ни с одной из переменных в X. Кстати, это тоже свойство линейной регрессии. Остатки всегда ортогональны или не скоррелированы ни с одной из переменных в модели, которая их сгенерировала. # получим остатки e = y - X.dot(beta) print(f"Ортогональность означает, что скалярное "
­ Регрессия для неслучайных данных  81 f"произведение равно нулю:\n{np.dot(e, X)}") # строим корреляционную матрицу остатков и предиктора, # использованного при построении модели X[["format_ol"]].assign(e=e).corr() Ортогональность означает, что скалярное произведение равно нулю: [8.24229573e-13 4.68247663e-12] И что еще круче, эти свойства ни от чего не зависят! Они являются математическими истинами, независимо от того, как выглядят ваши данные. Регрессия для неслучайных данных До сих пор мы работали с данными случайного эксперимента, но, как мы знаем, их трудно найти. Эксперименты очень дороги или просто не осущест вимы. Очень трудно убедить консультантов McKinsey & Co случайным образом выбирать компании и предоставлять им свои услуги бесплатно, чтобы мы могли раз и навсегда отличить полезность их услуг от того факта, что фирмы, которые могут позволить себе платить консультантам, уже сами по себе очень богаты. По этой причине мы теперь углубимся в неслучайные, или регистрируемые, данные. В следующем примере мы попытаемся оценить влияние дополнительного года образования на почасовую оплату труда. Как вы могли догадаться, провести эксперимент с контролем образования крайне сложно. Вы не сможете запросто распределить случайным образом людей с разным количеством лет, потраченным на образование. В этом случае регистрируемые данные – это все, что у нас есть. Сначала оценим очень простую модель. Мы строим регрессию, в которой зависимой переменной будет прологарифмированная почасовая заработная плата, а регрессором – количество лет обучения. Мы используем здесь логарифм, чтобы наши оценки параметров получили процентную интерпретацию (если вы никогда не слышали об этих удивительных свойствах логарифма и хотите знать, почему это так, нажмите ссылку https://stats.stackexchange. com/questions/244199/why-is-it-that-natural-log-changes-are-percentage-changeswhat-is-about-logs-th). С помощью логарифмирования мы сможем сказать, что 1 дополнительный год обучения дает увеличение заработной платы на x %. log(hwage)i = β0 + β1educi + ui.
82  Поразительная эффективность линейной регрессии # загружаем данные, удалив пропуски wage = pd.read_csv("data/wage.csv").dropna() # строим модель регрессии model_1 = smf.ols( 'np.log(hwage) ~ educ', data=wage.assign(hwage=wage["wage"] / wage["hours"])).fit() model_1.summary().tables[1] Значение β1 равно 0.0536, 95%-ный доверительный интервал – 0.039–0.068. Данная модель предсказывает, что каждый дополнительный год обучения дает увеличение заработной платы на 5.3 %. Это процентное увеличение соответствует убеждению, согласно которому образование влияет на заработную плату экспоненциально: мы ожидаем, что изменение количества лет, потраченных на образование, с 11 до 12 (в среднем соответствует окончанию средней школы) будет меньше влиять на зарплату, чем изменение с 14 до 16 лет (в среднем соответствует окончанию колледжа). from matplotlib import pyplot as plt from matplotlib import style style.use("fivethirtyeight") x = np.array(range(5, 20)) plt.plot(x, np.exp(model_1.params["Intercept"] + model_1.params["educ"] * x)) plt.xlabel("Количество лет, потраченных на образование") plt.ylabel("Почасовая зарплата") plt.title("Влияние образования на почасовую зарплату") plt.show()
Регрессия для неслучайных данных  83 Конечно, раз мы можем оценить модель, это не означает, что она верна. Обратите внимание, как я был осторожен в своих словах, говоря, что модель прогнозирует заработную плату человека, исходя из его образования. Я никогда не говорил, что это предсказание опирается на причинно-следственный вывод. На самом деле к настоящему моменту у вас, вероятно, есть очень серьезные основания полагать, что эта модель смещена. Поскольку наши данные не были получены в результате случайного эксперимента, мы не знаем, сопоставимы ли те, кто учился больше, с теми, кто учился меньше. Идя еще дальше, исходя из нашего понимания того, как устроен мир, мы совершенно уверены в том, что они несопоставимы. А именно мы можем утверждать, что у тех, кто учился больше, вероятно, более богатые родители, и увеличение заработной платы, которое мы наблюдаем по мере повышения уровня образования, является просто отражением того, как семейное богатство связано с большим количеством лет, потраченным на образование. Говоря математическим языком, мы думаем, что E[Y0 | T = 0] < E[Y0 | T = 1], то есть люди, учившиеся больше, в любом случае обладали бы более высоким доходом, даже не тратя значительное количество лет на образование. Если вы скептически относитесь к образованию, вы можете возразить, что оно может даже снизить заработную плату, не позволяя людям работать в должном объеме и тем самым снижая их опыт. К счастью, в наших данных есть много других переменных. Мы можем посмотреть уровень образования родителей meduc и feduc, уровень IQ этого человека, стаж работы exper и стаж на текущем месте работы tenure. У нас даже есть дамми-переменные для обозначения семейного статуса и принадлежности к черным. # выведем 5 наблюдений wage.head() Мы можем включить все эти дополнительные переменные в модель и оценить ее: log(hwage)i = β0 + κ educi + βXi + ui. Чтобы понять, как это помогает решить проблему смещения, давайте вспомним про двумерный коэффициент множественной линейной регрессии: .
84  Поразительная эффективность линейной регрессии Эта формула гласит, что мы можем предсказать образование educ, используя в качестве регрессоров образование родителей, IQ, стаж и т. д. Сделав это, мы получим вариант educ ed˜uc, который не скоррелирован ни с одной из переменных, включенных ранее. Это разрушит утверждения типа «люди, которые потратили большее количество лет на образование, получили его, потому что обладали более высоким IQ. Это не тот случай, когда образование приводит к более высокой зарплате. Это как раз тот случай, когда образование коррелирует с уровнем IQ, который и определяет заработную плату». Что ж, если мы включим IQ в нашу модель, то κ покажет, как дополнительный год, потраченный на образование, влияет на зарплату при фиксированном значении IQ. Сделайте небольшую паузу, чтобы понять написанное. Даже если мы не можем использовать рандомизированные контролируемые исследования, чтобы уравнять прочие факторы среди объектов, подвергнутых воздействию, и объектов, не подвергнутых воздействию, регрессия может сделать это за нас, включив те же самые факторы в модель, даже если данные не являются случайными! # создаем список переменных для контроля controls = ['IQ', 'exper', 'tenure', 'age', 'married', 'black', 'south', 'urban', 'sibs', 'brthord', 'meduc', 'feduc'] # создаем массив предикторов и массивы меток X = wage[controls].assign(intercep=1) t = wage["educ"] y = wage["lhwage"] # строим регрессию переменной t (образования) на переменные X beta_aux = regress(t, X) # получаем остатки t_tilde = t - X.dot(beta_aux) # вычисляем каппу kappa = t_tilde.cov(y) / t_tilde.var() kappa 0.04114719101005763 Полученный коэффициент говорит нам следующее: для людей с одинаковым IQ, стажем работы, стажем на текущем месте работы, возрастом и т. д. мы должны ожидать, что дополнительный год, потраченный на образование, будет связан с увеличением почасовой заработной платы на 4.11 %. Данный вывод подтверждает наше подозрение, что первая простая модель, в которой использовался только регрессор educ, была смещенной. Кроме того, он подтверждает, что это смещение заключалось в переоценке влияния образования. Как только мы учли другие факторы, предполагаемое влияние образования снизилось. Если мы будем мудрее и воспользуемся программным обеспечением, написанным другими людьми, вместо того чтобы писать код самостоятельно, мы даже сможем вычислить доверительный интервал для полученной оценки. # строим модель регрессии, включив образование # и контрольные предикторы model_2 = smf.ols('lhwage ~ educ +' + '+'.join(controls), data=wage).fit() model_2.summary().tables[1]
 Смещение, вызванное опущенной переменной или спутывающим фактором  85 Смещение, вызванное опущенной переменной или спутывающим фактором (omitted variable bias или confounding bias) Остается вопрос: является ли оцененный параметр каузальным, т. е. можно ли к нему применить причинно-следственную интерпретацию? К сожалению, мы не можем сказать наверняка. Мы можем возразить, что первая простая модель – регрессия, в которой зависимая переменная – зарплата, а регрессор – образование, вероятно, является смещенной. Она опускает (не учитывает) важные переменные, которые коррелируют как с образованием, так и с заработной платой. Без учета этих факторов оцененный эффект образования также отражает влияние других переменных, которые не были включены в модель. Чтобы лучше понять, как работает это смещение, давайте предположим, что истинная модель влияния образования на заработную плату выглядит примерно так: Wagei = α + κ Ei + γAi + ui.
­ 86  Поразительная эффективность линейной регрессии На заработную плату влияет образование (Е), которое измеряется размером κ, и фактор дополнительных способностей A. Если мы опускаем способности А в нашей модели, наша оценка κ будет выглядеть следующим образом: где δAE – это регрессия A по E. Ключевым моментом здесь является то, что в итоге мы получим не совсем тот κ, который нам нужен. Теперь его сопровождает этот дополнительный раздражающий член γδA. Этот член отражает влияние пропущенной переменной A на заработную плату Wage. γ умножает во столько-то раз влияние опущенной переменной А на включенную переменную E. Для экономистов важно, что Джошуа Ангрист превратил это в мантру, чтобы студенты могли повторять ее в ходе медитации: "Short equals long plus the effect of omitted times the regression of omitted on included" ("Короткая регрессия равна длинной регрессии плюс эффект опущенной переменной, умноженной на регрессию опущенной переменной по включенной переменной"). Здесь короткая регрессия (короткая модель регрессии) – это регрессия, в которой важные переменные опущены, тогда как длинная регрессия (длинная модель регрессии) – это регрессия, которая включает их. Эта формула, или мантра, дает нам дальнейшее представление о природе смещения. Во-пер вых, смещение будет равно нулю, если опущенные переменные не оказывают влияния на зависимую переменную Y. В этом есть здравый смысл. Мне не нужно контролировать факторы, которые не имеют отношения к зарплате, когда я пытаюсь оценить влияние образования на зарплату (например, мне не нужно контролировать высоту полевых лилий). Во-вторых, смещение также будет равно нулю, если опущенные переменные не оказывают влияния на переменную воздействия. Это также имеет интуитивный смысл. Если в модель было включено все, что влияет на образование, то оцененный эффект образования никоим образом не смешивается с корреляцией между образованием и какой-то другой переменной, также влияющей на зарплату. Моя короткая регрессия: Смещение из-за опущенной переменной: γδAЕ
­ Смещение, вызванное опущенной переменной или спутывающим фактором  87 Говоря более кратко, смещение из-за опущенной переменной отсутствует, если в модели учтены все спутывающие переменные. Здесь для иллюстрации мы тоже можем использовать графы причинно-следственных (каузальных) связей. Спутывающая переменная – это переменная, которая влияет как на воздействие, так и на результат. В примере с заработной платой IQ является спутывающей переменной. Люди с высоким IQ, как правило, характеризуются бóльшим количеством лет, потраченных на образование, потому что процесс обучения легко им дается, поэтому мы можем сказать, что IQ влияет на образование. Кроме того, люди с высоким IQ, как правило, более продуктивны и, следовательно, имеют более высокую зарплату, поэтому IQ также влияет на зарплату. Поскольку спутывающие факторы – это переменные, влияющие как на воздействие, так и на результат, мы помечаем их стрелкой, идущей к T и Y. Здесь я обозначил спутывающую переменную символом W. Я также отметил положительную причинно-следственную связь красным цветом, а отрицательную причинно-следственную связь – синим цветом. g = gr.Digraph() g.edge("W", "T"), g.edge("W", "Y"), g.edge("T", "Y") g.edge("IQ", "Образование", color="red"), g.edge("IQ", "Зарплата", color="red"), g.edge("Образование", "Зарплата", color="red") g.edge("Уровень преступности", "Численность полиции", color="red"), g.edge("Уровень преступности", "Уровень насилия", color="red"), g.edge("Численность полиции", "Уровень насилия", color="blue") g IQ W Образование T Y Зарплата Уровень преступности Численность полиции Уровень насилия Графы причинно-следственных связей отлично подходят для иллюстрации нашего понимания мира и понимания того, как работает смещение, вызванное спутывающими факторами. В нашем первом примере у нас есть граф, в котором образование влияет на заработную плату: большее количест во лет, потраченных на образование, приводит к более высокой зарплате.
88  Поразительная эффективность линейной регрессии Однако IQ также влияет на зарплату и образование: высокий IQ приводит как к большему количеству лет, потраченному на образование, так и к большей зарплате. Если мы не будем учитывать IQ в нашей модели, то часть его влияния на зарплату будет проявляться через корреляцию с образованием. В этом случае влияние образования на зарплату будет выглядеть выше, чем оно есть на самом деле. Это пример положительного смещения. Для иллюстрации отрицательного смещения рассмотрим граф причинноследственных связей, который используется для анализа влияния полиции на уровень насилия в городе. Мы часто наблюдаем, что в городах с более высокой численностью полиции также совершается больше насилия. Означает ли это, что полиция является причиной насилия? Ну, может быть, и так, но я не думаю, что стоит вдаваться в это обсуждение здесь. Однако существует также большая вероятность того, что существует какая-то спутывающая переменная, заставляющая нас видеть смещенное влияние полиции на уровень насилия. Вполне возможно, что увеличение численности полиции снижает уровень насилия. Но третья переменная уровень преступности (собственно спутывающая переменная) приводит как к большему уровню насилия, так и к увеличению численности полиции. Если мы ее не учтем, влияние уровня преступности на уровень насилия будет «протекать» через численность полицейских, создавая впечатление, что именно полиция усиливает насилие. Это пример отрицательного смещения. Графы причинно-следственных связей также могут показать нам, насколько корректна регрессия и рандомизированные контролируемые исследования для выявления смещения, вызванного спутывающим фактором. Рандомизированные контролируемые исследования делают это путем разрыва связи между спутывающим фактором и переменной воздействия. Назначая T случайно, мы устраняем причины, вызывающие смещение. g = gr.Digraph() g.edge("W", "Y"), g.edge("T", "Y") g.edge("IQ", "Зарплата", color="red"), g.edge("Образование", "Зарплата", color="red") g W T Y IQ Образование Зарплата С другой стороны, регрессия устраняет смещение, оценивая влияние T при сохранении спутывающего фактора W в зафиксированном значении. Если
­ Ключевые идеи  89 мы применяем регрессию, у нас невозможна ситуация, когда спутывающий фактор W перестанет влиять на T и Y. Просто он остается зафиксированным, поэтому не может повлиять на изменения T и Y. g = gr.Digraph() g.node("W=w"), g.edge("T", "Y") g.node("IQ=x"), g.edge("Образование", "Зарплата", color="red") g W=w T Y IQ = x Образование Зарплата Теперь вернемся к нашему вопросу: является ли параметр, который мы получили для оценки влияния образования на заработную плату, причинноследственным? Мне грустно это говорить, но ответ будет зависеть от нашей способности доказать тот факт, что все спутывающие факторы были включены в модель. Лично я думаю, что мы включили не все спутывающие факторы. Например, мы не включили семейное богатство. Даже если бы мы включили образование родителей, его можно рассматривать только как прокси богатства. Кроме того, мы не учли такие факторы, как личные амбиции. Возможно, амбиции – это то, что приводит как к большему количеству лет, потраченных на образование, так и к более высокой зарплате, поэтому амбиции – это спутывающий фактор. Это говорит о том, что к причинно-следственным выводам на основе неслучайных или регистрируемых данных всегда следует относиться с долей скепсиса. Мы никогда не сможем быть уверены, что были учтены все спутывающие факторы. Ключевые идеи Мы рассмотрели много вопросов с помощью регрессии. Мы увидели, как можно использовать регрессию для проведения A/B-тестирования и как она легко позволяет получить доверительные интервалы. Затем перешли к изучению того, как регрессия решает задачу прогнозирования и представляет собой наилучшую линейную аппроксимацию функции условного математического ожидания. Мы также обсудили, как в двумерном случае регрессионный коэффициент при переменной воздействия представляет собой ковариацию между воздействием и результатом, деленную на дисперсию воздействия. Обобщая для многомерного случая, мы выяснили, как
­ 90  Поразительная эффективность линейной регрессии регрессия дает нам немного иную интерпретацию регрессионного коэффициента при воздействии: коэффициент показывает, как результат меняется по мере изменения воздействия, при этом все другие включенные переменные остаются зафиксированными. Это то, что экономисты любят называть «при прочих равных условиях». Наконец, мы подошли к пониманию того, как возникает смещение. Мы увидели, что короткая регрессия равна длинной регрессии плюс эффект опущенной переменной, умноженный на регрессию опущенной переменной по включенной переменной. Это проливает свет на то, как возникает смещение. Мы обнаружили, что источник смещения, возникающий из-за опущенной переменной, – это спутывающий фактор, т. е. переменная, которая влияет как на переменную воздействия, так и на результат. Наконец, мы использовали графы причинно-следственных связей, чтобы посмотреть, как рандомизированные конт ролируемые испытания и регрессия устраняют спутывающий фактор.
Глава 6 Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными Регрессия, обученная на сгруппированных данных Не все точки данных одинаковы. Если мы снова посмотрим на наш набор данных ENEM, то увидим, что мы доверяем оценкам успеваемости, полученным для больших школ, в гораздо большей степени, чем оценкам успеваемости, полученным для маленьких школ. Данный факт не значит, что большие школы чем-то лучше или что-то в этом роде. Он обусловлен тем, что большой размер школы подразумевает меньшую дисперсию. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from scipy import stats from matplotlib import style import seaborn as sns from matplotlib import pyplot as plt import statsmodels.formula.api as smf style.use("fivethirtyeight")
92  Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными np.random.seed(876) enem = pd.read_csv("data/enem_scores.csv").sample(200) plt.figure(figsize=(8,4)) sns.scatterplot( y="avg_score", x="number_of_students", data=enem ) sns.scatterplot( y="avg_score", x="number_of_students", s=100, label="Надежно", data=enem.query(f"number_of_students=={enem.number_of_students.max()}") ) sns.scatterplot( y="avg_score", x="number_of_students", s=100, label="Не так много данных", data=enem.query(f"avg_score=={enem.avg_score.max()}") ) plt.title("Распределение баллов ENEM по количеству учеников в школе"); В вышеприведенных данных интуитивно понятно, что точки слева должны иметь меньшее влияние на мою модель, чем точки справа. По сути, каждая точка справа – это множество других точек данных, сгруппированных в одну. Если бы мы могли отделить эти точки друг от друга и обучить линейную регрессию на несгруппированных данных, они действительно внесли бы гораздо больший вклад в оценку модели, чем точка слева. Наличие областей с низкой и высокой дисперсиями называется гетероскедастичностью (heteroskedasticity). Проще говоря, гетероскедастичность – это когда дисперсия не является постоянной для всех значений признаков. В вышеприведенном случае мы видим, что дисперсия уменьшается по мере увеличения количества учеников в школе. Приведем еще один пример гетероскедастичности: вы строите график заработной платы по возрасту и видите, что разброс значений заработной платы для пожилых людей выше, чем для молодых. Однако, безусловно, наиболее распространенной причиной различий в дисперсии являются сгруппированные данные.
Регрессия, обученная на сгруппированных данных  93 Сгруппированные данные, аналогичные вышеприведенным, широко распространены. Одной из причин такой распространенности является конфиденциальность. Правительства и фирмы не могут разглашать личные данные, потому что это нарушит требования конфиденциальности данных, которым они должны следовать. Если им нужно отправить данные внешнему исследователю, они могут сделать это только посредством группировки данных. Таким образом, люди сгруппированы вместе и больше не могут быть однозначно идентифицированы. К счастью для нас, регрессия может довольно хорошо обрабатывать такие данные. Чтобы понять, как это сделать, давайте сначала возьмем набор несгруппированных данных, подобный тому, который у нас был по заработной плате и образованию. Он содержит по одной строке на каждого работника, поэтому мы знаем заработную плату каждого человека в этом наборе данных, а также его зарплату и количество лет, потраченных им на образование. # прочитываем данные wage = pd.read_csv("data/wage.csv")[ ["wage", "lhwage", "educ", "IQ"] ] wage.head() Если мы обучим регрессионную модель, чтобы выяснить, как образование связано с прологарифмированной почасовой оплатой труда, мы получим следующий результат. # обучаем модель model_1 = smf.ols('lhwage ~ educ', data=wage).fit() model_1.summary().tables[1] А теперь давайте на мгновение представим, что эти данные находятся под каким-то ограничением в целях соблюдения конфиденциальности. Поставщик не может предоставить данные по отдельному работнику. Поэтому мы просим поставщика сгруппировать всех работников по количеству лет, по-
94  Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными траченных на образование, дать нам только усредненную прологарифмированную почасовую заработную плату и количество человек в каждой группе. Таким образом, у нас остается всего 10 точек данных – 10 групп. # получаем сгруппированные данные group_wage = (wage .assign(count=1) .groupby("educ") .agg({"lhwage":"mean", "count":"count"}) .reset_index()) group_wage Не бойтесь! Регрессии не требуются большие данные! Однако в нашей модели линейной регрессии мы можем воспользоваться весами. Таким образом, модель будет учитывать группы большего размера в большей степени, чем небольшие группы. Обратите внимание, как я заменил smf.ols на smf. wls, чтобы воспользоваться методом взвешенных наименьших квадратов. Это трудно заметить, но это важно. # обучаем модель на сгруппированных данных с весами model_2 = smf.wls('lhwage ~ educ', data=group_wage, weights=group_wage["count"]).fit() model_2.summary().tables[1] Обратите внимание, как оценка параметра educ в модели, обученной на сгруппированных данных, очень близка к оценке параметра в индивиду-
Регрессия, обученная на сгруппированных данных  95 альных данных (фактически они одинаковы). Кроме того, даже имея всего 10 точек данных, нам удалось получить статистически значимый коэффициент. Это обусловлено тем, что хотя группировка сокращает количество точек данных, она также значительно снижает дисперсию. Также обратите внимание, что стандартная ошибка немного уменьшилась, а t-статистика немного увеличилась. Это связано с тем, что некоторая информация о дисперсии теряется, поэтому мы должны быть более консервативными. Сгруппировав данные, мы уже не знаем, насколько велика дисперсия внутри каждой группы. А сейчас сравним вышеприведенные результаты с результатами, которые мы получили бы, обучив модель на сгруппированных данных без весов. # обучаем модель на сгруппированных данных без весов model_3 = smf.ols('lhwage ~ educ', data=group_wage).fit() model_3.summary().tables[1] Оценка параметра стала меньше. Здесь регрессия придает равный вес всем точкам. Если мы построим график линии регрессии по сгруппированным данным, мы увидим, что модель без весов придает точкам в левом нижнем углу важность больше, чем следовало бы. В итоге линия регрессии имеет меньший наклон. # визуализируем линии регрессии для модели # без весов и модели с весами sns.scatterplot(x="educ", y="lhwage", size="count", legend=False, data=group_wage, sizes=(40, 400)) plt.plot(wage["educ"], model_2.predict(wage["educ"]), c="C1", label="С весами") plt.plot(wage["educ"], model_3.predict(wage["educ"]), c="C2", label="Без весов") plt.xlabel("Количество лет, потраченных на образование") plt.ylabel("Прологарифмированная\n" "почасовая зарплата") plt.legend();
96  Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными Суть в том, что регрессия – это замечательный инструмент, который работает как с индивидуальными, так и с агрегированными данными, но в последнем случае вы должны использовать веса. Чтобы использовать взвешенную регрессию, вам нужна такая статистика, как среднее. Не суммы, не стандартные отклонения, не медианы, а средние значения! Как для ковариат, так и для зависимой переменной. Просто имейте в виду, что результат взвешенной регрессии, обученной на сгруппированных данных, не будет в точности соответствовать результату регрессии, обученной на несгруппированных данных, но будет довольно схож с ним. Приведу последний пример, в котором мы используем дополнительные ковариаты для модели регрессии, обучающейся на сгруппированных данных с весами.
Регрессия c дамми-переменными  97 # используем дополнительные ковариаты для модели регрессии, # обучающейся на сгруппированных данных с весами group_wage = (wage .assign(count=1) .groupby("educ") .agg({"lhwage":"mean", "IQ":"mean", "count":"count"}) .reset_index()) model_4 = smf.wls('lhwage ~ educ + IQ', data=group_wage, weights=group_wage["count"]).fit() print("Количество наблюдений:", model_4.nobs) model_4.summary().tables[1] В этом примере мы включили IQ в качестве регрессора в дополнение к количеству лет, потраченных на образование. Методика почти та же: получаем групповые средние и групповые частоты (групповая частота – количество наблюдений в группе), обучаем на этих групповых средних регрессию и используем групповые частоты в качестве весов. Регрессия c дамми-переменными Дамми-переменные – это категориальные переменные, которые мы закодировали в виде бинарных столбцов. Предположим, у вас есть переменная Пол, которую вы хотите включить в свою модель. Эта переменная имеет три категории: мужской, женский и другой. пол мужской женский женский другой мужской Поскольку наша модель принимает только количественные значения, нам нужно преобразовать категории в числа. В линейной регрессии мы используем для этого дамми-переменные. Каждая дамми-переменная – это бинарный
98  Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными столбец со значениями 0/1, где 1 обозначает наличие категории. Кроме того, одна из категорий становится опорной. Это необходимо, поскольку последняя категория представляет собой линейную комбинацию остальных. Иными словами, мы можем узнать последнюю категорию, если кто-то предоставит нам информацию о других. В нашем примере, если человек не относится ни к женскому полу, ни к другому полу, мы можем сделать вывод о том, что у него – мужской пол. пол мужской женский женский другой мужской женский 0 1 1 0 0 другой 0 0 0 1 0 Мы уже строили регрессию с дамми-переменными, когда имели дело с A/Bтестированием. В целом, когда мы имеем дело с бинарным воздействием, мы представляем его в виде дамми-переменной. В этом случае регрессионный коэффициент для дамми-переменной представляет собой слагаемое, прибавляемое к константе, или разницу между средним значением, полученным для группы объектов, получивших воздействие, и средним значением, полученным для группы объектов, не получивших воздействия. Для иллюстрации вышесказанного попробуем оценить, как факт окончания 12-го класса влияет на почасовую заработную плату (и давайте пока проигнорируем спутывающие факторы). В нижеприведенном программном коде мы создадим дамми-переменную воздействия T, указывающую, превышает ли количество лет, потраченных на образование, 12-летний рубеж. # создаем дамми-переменную T wage = (pd.read_csv("data/wage.csv") .assign(hwage=lambda d: d["wage"] / d["hours"]) .assign(T=lambda d: (d["educ"] > 12).astype(int))) wage[["hwage", "IQ", "T"]].head() Дамми-переменная работает как своего рода переключатель. В нашем примере, если дамми-переменная включена (т. е. принимает значение 1),
Регрессия c дамми-переменными  99 прогнозное значение представляет собой константу плюс регрессионный коэффициент для дамми-переменной. Если дамми-переменная исключена (т. е. принимает значение 0), прогнозное значение просто представляет собой константу. # обучаем модель регрессии с дамми-переменной smf.ols('hwage ~ T', data=wage).fit().summary().tables[1] В нашем случае, если человек не потратил на образование 12 лет, т. е. по сути не закончил 12-й класс (дамми-переменная исключена, поскольку принимает значение 0), средний доход составляет 19.9. Если же человек закончил 12-й класс (дамми-переменная включена, поскольку принимает значение 1), прогнозное значение или средний доход составляет 24.8449 (19.9405 + 4.9044). Следовательно, регрессионный коэффициент для даммипеременной отражает разницу средних значений, которая в нашем случае составляет 4.9044. Давайте убедимся в этом и выведем средние значения дохода для обеих групп. # выведем средние значения дохода для обеих групп wage.groupby("T")["hwage"].mean() T 0 19.940483 1 24.844863 Name: hwage, dtype: float64 Говоря более формально, когда независимая переменная является бинарной, как это часто бывает с индикаторами воздействия, регрессия точно отражает ATE. Это связано с тем, что регрессия является линейной аппроксимацией функции условного математического ожидания (conditional expectation function – CEF) E[Y | X] и, в этом конкретном случае, CEF является линейной. Мы можем определить E[Yi | T i = 0] = α и E[Yi | T i = 1] = α + β, что приведет к следующей формуле CEF: E[Yi | T i] = E[Yi | T i = 0] + βT i = α + βT i, и β – это разница средних значений или ATE, если работаем со случайными данными: β = [Yi | T i = 1] – [Yi | T i = 0]. Если мы добавим дополнительные переменные, регрессионный коэффициент станет условной (conditional) разницей средних. Например, предпо-
100  Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными ложим, что мы добавили IQ в предыдущую модель. Теперь регрессионный коэффициент при дамми-переменной говорит нам, какой прирост дохода мы должны ожидать, окончив 12-й класс, при фиксированном IQ. Если мы построим прогноз, то увидим две параллельные линии. Переход от одной линии к другой говорит о сумме, которую мы должны ожидать, закончив 12-й класс. Здесь еще можно сказать, что эффект является постоянным. Независимо от своего IQ каждый получает одинаковую выгоду, закончив 12-й класс. # обучаем модель регрессии с дамми-переменной и IQ m = smf.ols('hwage ~ T+IQ', data=wage).fit() plt_df = wage.assign(y_hat=m.fittedvalues) plt.plot(plt_df.query("T==1")["IQ"], plt_df.query("T==1")["y_hat"], c="C1", label="T=1") plt.plot(plt_df.query("T==0")["IQ"], plt_df.query("T==0")["y_hat"], c="C2", label="T=0") plt.title(f"E[T=1|IQ] - E[T=0|IQ] = {round(m.params['T'], 2)}") plt.ylabel("Зарплата") plt.xlabel("IQ") plt.legend(); Теперь мы представим эту модель в виде уравнения: wagei = β0 + β1T i + β2IQ i + ei. Здесь β1 – условная разница средних значений, она представляет собой константу и в нашем случае равна 3.16. Мы можем сделать эту модель более гибкой, добавив член взаимодействия: wagei = β0 + β1T i + β2IQ i + β3IQ i ∗ T i + ei.
­ ­ Регрессия c дамми-переменными  101 Ситуация становится чуть более сложной, поэтому давайте посмотрим, что означает каждый параметр в этой модели. Начнем с константы β0. У нее нет особенно интересной интерпретации. Это ожидаемая заработная плата, когда воздействие равно нулю (человек не закончил 12-й класс) и IQ равен нулю. Поскольку мы не ожидаем, что IQ для кого-то будет равен нулю, этот параметр не имеет большого значения. Что касается β1, здесь аналогичная си туация. Этот параметр показывает, насколько увеличится заработная плата, если человек закончит 12-й класс, когда IQ равен нулю. Опять же, поскольку IQ никогда не равен нулю, этот параметр не особо нам интересен. А вот с β2 ситуация немного интереснее. Он говорит нам, насколько IQ увеличивает заработную плату для лиц, не подвергнутых воздействию. В нашем случае он равен 0.11. Это означает, что за каждый дополнительный балл IQ человек, не закончивший 12-й класс, может рассчитывать на дополнительные 11 центов в час. Наконец, разберем самый интересный параметр β3. Он говорит нам, насколько IQ усиливает эффект окончания 12-го класса. В нашем случае данный параметр равен 0.024, это означает, что за каждый дополнительный балл IQ окончание 12-го класса дает 2 дополнительных цента. Это может показаться немного, но давайте сравним прибавку к зарплате для человека с IQ 60 и для человека с IQ 140. Человек с IQ 60 получит прибавку к зарплате около 1.44 доллара (60 * 0.024), а человек с IQ 140 получит дополнительно 3.36 доллара (140 * 0.024), при условии что они окончили 12-й класс. Член взаимодействия позволяет изменять эффект воздействия в зависимости от значений характеристик (в данном примере только в зависимости от значений IQ). В результате если мы визуализируем прогнозные линии, то увидим, что они больше не параллельны: у тех, кто закончил 12-й класс (T=1), более высокий коэффициент для IQ (т. е. более крутой наклон линии), таким образом, человек с более высоким IQ после окончания 12-го класса будет зарабатывать больше, чем человек с более низким IQ. Это иногда называют модификацией эффекта (effect modification), эффектом взаимодействия (interaction effect) или гетерогенным эффектом воздействия (heterogeneous treatment effect). # обучаем модель регрессии с взаимодействием # дамми-переменной и IQ m = smf.ols('hwage ~ T*IQ', data=wage).fit() plt_df = wage.assign(y_hat=m.fittedvalues) plt.plot(plt_df.query("T==1")["IQ"], plt_df.query("T==1")["y_hat"], c="C1", label="T=1") plt.plot(plt_df.query("T==0")["IQ"], plt_df.query("T==0")["y_hat"], c="C2", label="T=0") plt.title(f"E[T=1|IQ] - E[T=0|IQ] = {round(m.params['T'], 2)}") plt.ylabel("Зарплата") plt.xlabel("IQ") plt.legend();
­ ­ 102  Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными Наконец, давайте рассмотрим случай, когда все признаки в нашей модели являются дамми-переменными. Для этого мы разделим IQ на 4 группы и будем рассматривать количество лет, потраченных на обучение, как категорию. # выполняем категоризацию (биннинг) переменной IQ wage_ed_bins = ( wage .assign(IQ_bins = lambda d: pd.qcut( d["IQ"], q=4, labels=range(4))) [["hwage", "educ", "IQ_bins"]]) wage_ed_bins.head() Рассматривая образование как категориальную переменную, мы больше не ограничиваем влияние образования каким-либо одним параметром. Вместо этого мы позволяем каждому значению переменной educ (каждому количеству лет, потраченных на образование) оказывать свое отдельное влияние на зарплату. Поступая таким образом, мы обретаем гибкость, поскольку эффект образования больше не является параметрическим. Эта модель просто вычисляет среднюю заработную плату для каждого количества лет, потраченных на образование.
Регрессия c дамми-переменными  103 # обучаем модель регрессии, превратив значения # образования в категории (получаем группу # дамми-переменных) model_dummy = smf.ols('hwage ~ C(educ)', data=wage).fit() model_dummy.summary().tables[1] # визуализируем влияние образования на зарплату plt.scatter(wage["educ"], wage["hwage"]) plt.plot(wage["educ"].sort_values(), model_dummy.predict(wage["educ"].sort_values()), c="C1") plt.xlabel("Количество лет, потраченных на образование") plt.ylabel("Почасовая зарплата"); Прежде всего обратите внимание, как данный способ обработки переменной устраняет любые предположения о функциональной форме влияния об-
104  Регрессия, обученная на сгруппированных данных, и регрессия с дамми-переменными разования на заработную плату. Нам больше не нужно беспокоиться о логарифмировании. По сути, эта модель является полностью непараметрической. Она вычисляет выборочные средние значения заработной платы для каждого количества лет, потраченных на образование. Это можно увидеть на графике выше, где полученная в ходе обучения линия не имеет определенной формы. Вместо этого используется интерполяция выборочных средних значений для каждого года обучения. Мы можем убедиться в этом, рассмотрев один из параметров, например 17 лет, потраченных на обучение. В нашей модели этот параметр равен 9.5905. Ниже мы можем увидеть, что речь идет всего лишь о разнице между базовым количеством лет, потраченных на образование (9 лет, потраченных на образование), и 17 годами, потраченными на образование: β17 = E[Y | T = 17] – E[Y | T = 9]. Однако ради гибкости мы жертвуем статистической значимостью. Обратите внимание, как увеличились p-значения. # выясним, как был получен коэффициент для С(educ)[T.17] t1 = wage.query("educ==17")["hwage"] t0 = wage.query("educ==9")["hwage"] print("E[Y|T=9]:", t0.mean()) print("E[Y|T=17]-E[Y|T=9]:", t1.mean() - t0.mean()) E[Y|T=9]: 18.56 E[Y|T=17]-E[Y|T=9]: 9.590472362353516 Если мы включим в модель больше дамми-переменных, параметр образования станет средневзвешенным значением эффекта для каждой даммигруппы: E{(E[Yi | T = 1, Groupi ] – E[Yi | T = 0, Groupi ])w(Groupi )}. Здесь w(Groupi ) не является точным значением, однако пропорционален дисперсии воздействия в группе Var(T i | Groupi). Один естественный вопрос, который возникает в связи с этим уравнением, заключается в том, почему бы нам не использовать полностью непараметрическую модель, в которой вес группы был бы равен размеру выборки? Мы бы получили действительно достоверную модель, но это не то, что делает регрессия. Используя дисперсию воздействия, регрессия придает больший вес группам, в которых воздействие сильно варьирует. Это имеет интуитивно понятный смысл. Если воздействие было почти неизменным (скажем, 1 человек подвергся воздействию, а все остальные не подверглись), размер выборки не имеет значения. Это не дало бы много информации об эффекте воздействия. # обучаем модель регрессии, превратив значения # образования и IQ в категории # (будут 2 группы дамми-переменных) model_dummy_2 = smf.ols('hwage ~ C(educ) + C(IQ_bins)', data=wage_ed_bins).fit() model_dummy_2.summary().tables[1]
Ключевые идеи  105 Некультурный специалист по data science: «Ты не можешь использовать линейную регрессию. У нее – масса ограничений!» Я: "y-"+"+".join(map(lambda x: f"C(C)*C({x})", features)) Некультурный специалист по data science: Слушай сюда, ты, мелкий засранец… Ключевые идеи Мы начали эту главу с рассмотрения того, насколько некоторые точки данных важнее других. Точкам, полученным по выборкам большего размера и имеющим меньшую дисперсию, следует придавать больший вес при оценке линейной модели. Затем мы рассмотрели, как линейная регрессия может элегантно обрабатывать сгруппированные анонимизированные данные, при условии что мы используем веса групп в нашей модели. Потом перешли к регрессии с дамми-переменными. Мы увидели, как можно создать непараметрическую модель, которая не делает никаких предположений о функциональной форме влияния воздействия на результат. Затем мы исследовали принципы, лежащие в основе регрессии с дамми-переменными.
Глава 7 Помимо спутывающих переменных «Хорошие» контрольные переменные Мы увидели, как добавление дополнительных контрольных переменных в нашу регрессионную модель может помочь найти причинно-следственный эффект. Если контрольная переменная является спутывающей переменной, ее добавление в модель не просто желательно, но и является обязательным требованием. Когда неопытный специалист понимает это, его естественной реакцией является включение в модель всего, что он только может измерить. В современном мире больших данных легко включить в модель более 1000 переменных. Как выясняется, это не только не нужно, но и может помешать поиску причинно-следственных эффектов. Теперь мы обратим наше внимание на контрольные переменные, которые не являются спутывающими. Во-первых, давайте взглянем на «хорошие» контрольные переменные. Затем мы углубимся в изучение «плохих» контрольных переменных. В качестве мотивирующего примера давайте предположим, что вы специалист по обработке данных в отделе по взысканию долгов финтехкомпании. Ваша очередная задача – выяснить, как отправка электронного письма с просьбой к должнику погасить свой долг повлияет на выплаты долга. Зависимая переменная – это сумма платежей, поступившая от должника. Для решения задачи команда выбирает случайным образом 5000 человек из базы должников, чтобы провести случайный тест. Для каждого клиента вы подбрасываете монетку если выпадает решка, клиент получает электронное письмо, в противном случае он остается в контрольной группе. С помощью этого теста вы надеетесь выяснить, сколько дополнительных денег принесет электронное письмо.
«Хорошие» контрольные переменные  107 import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from scipy import stats from matplotlib import style import seaborn as sns from matplotlib import pyplot as plt import statsmodels.formula.api as smf import graphviz as gr style.use("fivethirtyeight") data = pd.read_csv("data/collections_email.csv") data.head() Поскольку данные являются случайными, вы уже знаете, что простая разница средних значений позволяет оценить средний эффект воздействия. Другими словами, ничто не может повлиять на воздействие, кроме процедуры рандомизации, поэтому потенциальные результаты не зависят от воздействия: (Y0, Y1) ⊥ T. ATE = E[Y | T = 1] – E[Y | T = 0]. Поскольку вы умны и хотите получить доверительный интервал разницы средних, вы строите линейную регрессию. print("Разница средних:", data.query("email==1")["payments"].mean() data.query("email==0")["payments"].mean()) model = smf.ols('payments ~ email', data=data).fit() model.summary().tables[1] Разница средних: -0.6202804021329484
108  Помимо спутывающих переменных К сожалению, вычисленное значение ATE составляет –0.62, что довольно странно. Как отправка электронного письма может заставить должников платить меньше, чем в среднем? Тем не менее p-значение настолько велико, что, вероятно, его не стоит принимать во внимание. Что теперь делать? Понуро вернуться к своей команде и сказать коллегам, что тест неубедителен и вам нужно больше данных? Не торопитесь. Обратите внимание, что в ваших данных есть еще несколько интересных столбцов. Например, credit_limit представляет собой кредитный лимит. Показатель risk_score соответствует спрогнозированному риску клиента до отправки электронного письма. Предположим, что кредитный лимит и спрогнозированный риск, вероятно, являются очень хорошими предикторами платежей. Но как извлечь из них пользу? Во-первых, давайте разберемся, почему мы не можем обнаружить статистическую значимость воздействия даже тогда, когда она на самом деле есть. Возможно, как и в данном случае, воздействие очень слабо влияет на результат. Если вдуматься, по большому счету, факторы, которые заставляют людей выплачивать свои долги, не подконтрольны отделу взыскания. Люди оплачивают свои долги, потому что находят новую работу, начинают более рационально управлять своими финансами, доходами и т. д. Мы можем сказать, что изменчивость платежей в гораздо большей степени объясняется другими факторами, нежели отправкой письма по электронной почте. Чтобы получить визуальное представление об этом, мы можем визуализировать платежи по переменной воздействия – обращению по электронной почте. Кроме того, я выделил линию регрессии, построенной выше, красным цветом. Чтобы облегчить визуализацию, я добавил немного шума в переменную email, дабы ее значения немного отличались от 0 и 1. sns.scatterplot("email", "payments", alpha=0.8, data=data.assign( email=data["email"] + np.random.normal(0, 0.01, size=len(data["email"])))) plt.plot(np.linspace(-0.2, 1.2), model.params[0] + np.linspace(-1, 2) * model.params[1], c="C1") plt.xlabel("Email") plt.ylabel("Платежи");
­ ­ «Хорошие» контрольные переменные  109 Мы видим, насколько сильно варьируют платежи в рамках каждой группы. Визуально это выглядит так, как будто в обеих группах размер платежей варьирует от 350 до 1000. Если влияние электронного письма составляет порядка, скажем, 5 или 10 бразильских реалов, неудивительно, что его будет трудно найти среди всей этой изменчивости. К счастью, регрессия может помочь нам снизить эту изменчивость. Хит рость заключается в использовании дополнительных контрольных переменных. Если переменная является хорошим предиктором результата, она объяснит значительную часть ее дисперсии. Если риск и кредитный лимит являются хорошими предикторами размера платежа, мы можем рассмот реть их в качестве контрольных переменных, чтобы было легче определить влияние отправки электронного письма на размер платежей. Вспомним про интуитивное объяснение эффективности такого подхода. Добавление дополнительных переменных в регрессию означает, что мы фиксируем их при оценивании воздействия. Итак, если мы учтем risk_score и credit_limit, то дисперсия зависимой переменной payments должна уменьшиться. Или, другими словами, если risk_score и credit_limit очень хорошо предсказывают размер платежей, клиенты с соответствующими уровнем риска и кредитным лимитом должны иметь соответствующий уровень платежей, в итоге дисперсия зависимой переменной уменьшается.
110  Помимо спутывающих переменных Когда переменная не уменьшает смещение, но улучшает статистическую значимость: Чтобы продемонстрировать это, разобьем процесс построения регрессии на два этапа. На первом этапе мы построим две регрессии, в первой регрессии зависимой переменной будет переменная воздействия – отправка письма по электронной почте, а предикторами – кредитный лимит и оценка риска, во второй регрессии зависимой переменной будет размер платежей, а предикторами – все те же кредитный лимит и оценка риска. На втором этапе мы построим регрессию, в которой зависимой переменной будут остатки модели, прогнозирующей размер платежей, а предиктором – остатки модели, прогнозирующей воздействие – отправку электронного письма, обе модели получены на первом этапе. (Мы делаем это чисто в образовательных целях, на практике вам не нужно будет проходить через все эти хлопоты.) model_email = smf.ols('email ~ credit_limit + risk_score', data=data).fit() model_payments = smf.ols('payments ~ credit_limit + risk_score', data=data).fit() residuals = pd.DataFrame(dict(res_payments=model_payments.resid, res_email=model_email.resid)) model_treatment = smf.ols('res_payments ~ res_email', data=residuals).fit() Вышеописанная процедура снижает дисперсию зависимой переменной. Получив остатки модели, прогнозирующей платежи по кредитному лимиту и оценке риска, мы создаем новую зависимую переменную с гораздо меньшей изменчивостью, чем исходная. Последняя модель оценивает ATE с приемлемым уровнем значимости.
­ «Хорошие» контрольные переменные  111 Кроме того, просто из любопытства мы можем убедиться в том, что модель, которая предсказывает воздействие, не может снизить дисперсию воздействия. Это обусловлено тем, что отправка письма по электронной почте по своей природе случайна, поэтому ничто не может ее предсказать. print(f"Дисперсия переменной payments {np.var(data['payments'])}") print(f"Дисперсия остатков модели, прогнозирующей payments " f"{np.var(residuals['res_payments'])}") print(f"Дисперсия переменной email {np.var(data['email'])}") print(f"Дисперсия остатков модели, прогнозирующей email " f"{np.var(residuals['res_email'])}") model_treatment.summary().tables[1] Дисперсия Дисперсия Дисперсия Дисперсия переменной payments 10807.61241599994 остатков модели, прогнозирующей payments 5652.4535584662 переменной email 0.24991536000001294 остатков модели, прогнозирующей email 0.24918421069820038 Обратите внимание, как дисперсия платежей снизилась с 10 807 до 5652. Мы снизили ее почти вдвое после того, как проконтролировали оценку рис ка и кредитный лимит. Кроме того, обратите внимание, что нам не удалось уменьшить дисперсию воздействия – отправки электронного письма. Это логично, поскольку риск и кредитный лимит не могут предсказать отправку письма (ни один предиктор не сможет предсказать отправку письма, которая по определению является случайной). Теперь мы видим гораздо более разумные результаты. Новая оценка говорит нам следующее: мы можем ожидать, что клиенты, получившие электронное письмо, заплатят в среднем на 4.4 реала больше, чем те, кто не получил письмо (т. е. те, кто входил в контрольную группу). Эта оценка статистически значимо отличается от нуля. Кроме того, мы можем визуализировать, насколько снизилась дисперсия в каждой группе. sns.scatterplot("res_email", "res_payments", data=residuals) plt.plot(np.linspace(-0.7, 1), model_treatment.params[0] + np.linspace(-1, 2) * model_treatment.params[1], c="C1") plt.xlabel("Остатки модели,\nпрогнозирующей email") plt.ylabel("Остатки модели,\nпрогнозирующей payments");
112  Помимо спутывающих переменных Как я уже сказал, мы привели эти операции в образовательных целях. На практике вы можете вместе с переменной воздействия добавить контрольные переменные в регрессионную модель, и оценки будут точно такими же. model_2 = smf.ols('payments ~ email + credit_limit + risk_score', data=data).fit() model_2.summary().tables[1] Подводя итог, мы можем сказать, что всякий раз, когда у нас есть контрольная переменная, которая является хорошим предиктором результата, при этом она необязательно должна быть спутывающим фактором, добавление ее в нашу модель выглядит хорошей идеей. Она помогает снизить дисперсию оценки воздействия. Ниже приведен рисунок, на котором показано, как нашу ситуацию можно проиллюстрировать с помощью графов причинноследственных связей. g = gr.Digraph() g.edge("X", "Y"), g.edge("T", "Y") g.node("T", color="gold") g.node("email", color="gold")
Преимущественно вредные контрольные переменные  113 g.edge("credit_limit", "payments") g.edge("risk_score", "payments") g.edge("email", "payments") g X Т Y email credit_limit risk_score payments Преимущественно вредные контрольные переменные В качестве второго примера давайте рассмотрим сценарий тестирования лекарства в двух больницах. Обе больницы проводят рандомизированные испытания нового препарата для лечения определенного заболевания. Интересующий результат – количество дней пребывания в больнице. Если лечение будет эффективным, то оно сократит количество дней пребывания пациента в больнице. В первой больнице схема эксперимента заключается в том, что случайным образом 7 % пациентов получают лекарство, а 93 % пациентов – плацебо. Во второй больнице действует иная схема: случайным образом 94 % пациентов получают лекарство, а 6 % пациентов – плацебо. В первой больнице – 29 пациентов, во второй больнице – 51 пациент. Кроме того, вам сообщают, что больница, которая выдает в 94 % случаев настоящее лекарство, а в 6 % случаев – плацебо, обычно лечит более тяжелых пациентов. hospital = pd.read_csv("data/hospital_treatment.csv") hospital.head() # в первой больнице примерно 93 % получают плацебо, # примерно 7 % получают лекарство hospital[hospital['hospital'] == 0]['treatment'].value_counts(
114  Помимо спутывающих переменных normalize=True) 0 0.931034 1 0.068966 Name: treatment, dtype: float64 # во второй больнице примерно 6 % получают плацебо, # примерно 94 % получают лекарство hospital[hospital['hospital'] == 1]['treatment'].value_counts( normalize=True) 1 0.941176 0 0.058824 Name: treatment, dtype: float64 Поскольку вы имеете дело с рандомизированными данными, ваш первый порыв – просто обучить регрессию, в которой зависимой переменной является количество дней пребывания пациента в больнице (результат), а регрессором – переменная лечения. hosp_1 = smf.ols('days ~ treatment', data=hospital).fit() hosp_1.summary().tables[1] Однако вы обнаружите в некоторой степени противоречивые результаты. Каким образом лечение может увеличить количество дней пребывания в больнице? Ответ заключается в том, что мы проводим два разных эксперимента. Тяжесть заболевания положительно связана с бóльшим количеством дней пребывания в больнице, а поскольку в больнице с более тяжелыми случаями чаще дают лекарство, лекарство начинает положительно коррелировать с бóльшим количеством дней пребывания в больнице. # смотрим корреляцию между лечением и # количеством дней госпитализации hospital['treatment'].corr(hospital['days']) 0.42978187379865956 Если мы посмотрим на обе больницы вместе, то получим, что E[Y0 | T = 0] < E[Y0 | T = 1], то есть потенциальный результат у пациентов, не получивших лечение, в среднем ниже, чем у пациентов, получивших лечение, поскольку у нас больше пациентов с тяжелыми симптомами, получивших лечение. # взглянем на количество дней госпитализации # среди не получивших / получивших лечение hospital.groupby('treatment')['days'].mean()
Преимущественно вредные контрольные переменные  115 treatment 0 33.266667 1 47.420000 Name: days, dtype: float64 # у нас больше пациентов с тяжелыми симптомами, получивших лечение print(hospital[hospital['hospital'] == 0]['treatment'].value_counts()) print(hospital[hospital['hospital'] == 1]['treatment'].value_counts()) 0 27 1 2 Name: treatment, dtype: int64 1 48 0 3 Name: treatment, dtype: int64 Другими словами, степень тяжести заболевания действует как спутывающий фактор, определяющий, в какую больницу попадет пациент, и, следовательно, определяющий вероятность получения препарата. Есть два способа это исправить. Первый способ, который противоречит цели использования данных из обеих больниц, заключается в простом вычислении ATE по каждой больнице отдельно. hosp_2 = smf.ols( 'days ~ treatment', data=hospital.query("hospital==0")).fit() hosp_2.summary().tables[1] hosp_3 = smf.ols( 'days ~ treatment', data=hospital.query("hospital==1")).fit() hosp_3.summary().tables[1] В данном случае мы действительно получили интуитивно понятное значение ATE. Похоже, что теперь препарат на самом деле сокращает количество дней пребывания в больнице. Однако поскольку мы рассматриваем каждую больницу по отдельности, данных недостаточно. Как следствие мы не можем получить статистически значимые результаты.
116  Помимо спутывающих переменных Другой подход, который использует возможности регрессии, заключается в контроле тяжести заболевания путем включения ее в модель. hosp_4 = smf.ols('days ~ treatment + severity', data=hospital).fit() hosp_4.summary().tables[1] Следующий вопрос, который возникает, заключается в том, должны ли мы еще включить номер больницы (переменную hospital) в модель. В конце концов, мы знаем, что пребывание в той или иной больнице влияет на лечение, не так ли? Что ж, это правда, но как только мы зададим тяжесть заболевания, пребывание в той или иной больнице больше не будет коррелировать с количеством дней госпитализации. При этом мы знаем: чтобы переменная стала спутывающей, она должна влиять как на лечение (предиктор), так и на результат (зависимую переменную). В этом случае у нас есть переменная, которая влияет только на лечение. Но, возможно, контроль переменной hospital снизит дисперсию оценок? Нет, это не так. Чтобы контрольная переменная снизила дисперсию оценок, она должна быть хорошим предиктором результата, а не лечения, в нашем же случае переменная hospital является хорошим предиктором лечения. И все же нам нужно проконтролировать ее, не так ли? Она ведь не навредит… Или все-таки навредит? hosp_5 = smf.ols( 'days ~ treatment + severity + hospital', data=hospital).fit() hosp_5.summary().tables[1] Удивительно, но она вредит!
Преимущественно вредные контрольные переменные  117 Учитель: «Ты не должен добавлять любые переменные в свою модель». Я: «Добавляю все переменные в модель». P-значение: 0.9999 Я: Добавление номера больницы к тяжести заболевания в качестве контрольных переменных привело к УВЕЛИЧЕНИЮ дисперсии нашей оценки ATE. Как такое может быть? Ответ кроется в формуле стандартной ошибки коэффициента регрессии: Из этой формулы мы видим, что стандартная ошибка обратно пропорциональна дисперсии переменной X. Это означает, что если переменная X слабо меняется, то будет трудно оценить ее влияние на результат. Это тоже имеет интуитивный смысл. Доведем данную мысль до крайности и представим, что вы хотите оценить действие лекарства и проводите тест с участием 10 000 человек, но только один из них получает лечение. Это очень затруднит вычисление ATE, мы будем полагаться на сравнение одного человека со всеми остальными. Другими словами, нам нужна изменчивость в плане получения лечения, чтобы было легче обнаружить влияние лечения. Включение номера больницы в модель увеличивает стандартную ошибку нашей оценки, потому что пребывание в той или иной больнице – это хороший предиктор лечения, а не результата (при условии что мы контролируем тяжесть заболевания). Таким образом, прогнозируя лечение, этот предиктор эффективно снижает дисперсию лечения! Вновь мы можем прибегнуть к разбиению нашей вышеприведенной регрессии на два этапа, чтобы убедиться в этом. Мы прогнозируем лечение с помощью тяжести заболевания и номера больницы. Затем прогнозируем количество дней госпитализации тоже с помощью тяжести заболевания и номера больницы. Записываем остатки
118  Помимо спутывающих переменных обеих моделей. Наконец, прогнозируем остатки регрессии, предсказавшей количество дней госпитализации, с помощью остатков регрессии, предсказавшей воздействие. model_treatment = smf.ols('treatment ~ severity + hospital', data=hospital).fit() model_days = smf.ols('days ~ severity + hospital', data=hospital).fit() residuals = pd.DataFrame(dict(res_days=model_days.resid, res_treatment=model_treatment.resid)) model_treatment = smf.ols('res_days ~ res_treatment', data=residuals).fit() model_treatment.summary().tables[1] Однако не верьте мне на слово! Вы можете проверить, верна ли вышеприведенная формула стандартной ошибки коэффициента. print(f"Дисперсия переменной treatment {np.var(hospital['treatment'])}") print(f"Дисперсия остатков модели, прогнозирующей treatment " f"{np.var(residuals['res_treatment'])}") Дисперсия переменной treatment 0.234375 Дисперсия остатков модели, прогнозирующей treatment 0.05752909187211909 sigma_hat = sum(model_treatment.resid**2)/(len(model_treatment.resid)-2) var = sigma_hat/sum((residuals["res_treatment"] residuals["res_treatment"].mean())**2) print("SE коэффициента:", np.sqrt(var)) SE коэффициента: 3.4469737674869023 Итак, суть в том, что мы должны добавить контрольные переменные, которые коррелируют как с лечением, так и с результатом (являются спутывающими переменными), например тяжесть заболевания в вышеприведенной модели. Кроме того, мы должны добавить контрольные переменные, которые являются хорошими предикторами результата, даже если они не являются спутывающими переменными, поскольку они снижают дисперсию наших оценок. Однако мы НЕ должны добавлять контрольные переменные, которые являются просто хорошими предикторами лечения, потому что они увеличат дисперсию наших оценок. Ниже наша ситуация проиллюстрирована с помощью причинно-следственного графа.
Плохие контрольные переменные – смещение из-за отбора  119 g = gr.Digraph() g.edge("X", "T"), g.edge("T", "Y") g.node("T", color="gold") g.node("лечение", color="gold") g.edge("тяжесть заболевания", "номер больницы") g.edge("тяжесть заболевания", "длительность госпитализации") g.edge("номер больницы", "лечение") g.edge("лечение", "длительность госпитализации") g Х T Y тяжесть заболевания номер больницы лечение длительность госпитализации Плохие контрольные переменные – смещение из-за отбора Давайте вернемся к примеру с отправкой электронных писем должникам. Мы случайным образом определили электронные адреса клиентов, которым отправим письма. Мы уже знаем, что из себя представляют переменные credit_limit и risk_score. Теперь давайте посмотрим на оставшиеся переменные. Переменная opened – это дамми-переменная, указывающая, открыл клиент электронное письмо или нет. Переменная agreement – это еще одна дамми-переменная, указывающая, связался ли клиент с отделом по взысканию задолженности, чтобы обсудить свой долг, после получения электронного письма. Какую из следующих моделей вы считаете более подходящей? Первая – это модель с переменной воздействия (переменной email) плюс переменные credit_limit и risk_score. Вторая модель, помимо перечисленных переменных, включает дамми-переменные opened и agreement.
­ ­ 120  Помимо спутывающих переменных email_1 = smf.ols('payments ~ email + credit_limit + risk_score', data=data).fit() email_1.summary().tables[1] formula = ('payments ~ email + credit_limit + ' 'risk_score + opened + agreement') email_2 = smf.ols(formula, data=data).fit() email_2.summary().tables[1] Если первая модель дает статистически значимый результат для предиктора email, то вторая – нет. Но, возможно, вторая модель является корректной, просто отсутствует эффект влияния email. В конце концов, эта модель учитывает больше факторов, поэтому она должна быть более надежной, не так ли? К настоящему времени вы, вероятно, уже знаете, что это не так. Все, что осталось, – это выяснить, что у нас происходит на самом деле. Мы знаем, что нам НУЖНО добавить спутывающие переменные. Переменные, которые влияют как на воздействие, так и на результат. Кроме того, мы знаем, что целесообразно добавить контрольные переменные, которые очень хорошо предсказывают результат. Это не обязательно, но включить такие переменные в модель полезно. Мы также знаем, что добавлять конт рольные переменные, которые предсказывают только воздействие, – это плохая идея. Опять же, это не смертельный грех, но лучше их избегать. Итак, к какому типу контрольных переменных можно отнести предикторы opened и agreement? Оказывается, они не являются ни тем, ни другим из вышеперечисленного. Если вдуматься, то переменные opened и agreement наверняка коррелируют с переменной email. В конце концов, вы не сможете открыть электронное письмо, если вы его не получали, и мы также сказали, что agreement преду
Плохие контрольные переменные – смещение из-за отбора  121 сматривает пересмотр условий только после отправки электронного письма. Но они не являются причиной отправки письма на электронную почту! Вместо этого они являются следствиями отправки письма на электронную почту! Всякий раз, когда мне нужно понять, с какими переменными я имею дело, я всегда думаю о графе их причинно-следственной связи. Давайте нарисуем его здесь. g = gr.Digraph() g.edge("email", "payments") g.edge("email", "opened") g.edge("email", "agreement") g.edge("opened", "payments") g.edge("opened", "agreement") g.edge("agreement", "payments") g.edge("credit_limit", "payments") g.edge("credit_limit", "opened") g.edge("credit_limit", "agreement") g.edge("risk_score", "payments") g.edge("risk_score", "opened") g.edge("risk_score", "agreement") g email credit_limit risk_score opened agreement payments Мы ничего не знаем о причинах email, поскольку по нашему замыслу отправка писем осуществлялась случайно. И мы знаем (или по крайней мере у нас есть веские основания полагать), что переменные credit_limit и risk_ score влияют на payments. Мы также считаем, что переменная email влияет на платежи. Что касается переменной opened, мы считаем, что она действитель-
122  Помимо спутывающих переменных но влияет на payments. Интуитивно люди, открывшие электронное письмо о взыскании, более охотно ведут переговоры и выплачивают свой долг. Кроме того, мы считаем, что переменная opened влияет на agreement в силу тех же причин, которые лежат в основе влияния opened на payments. Более того, мы знаем, что причиной opened является email, и у нас есть основания полагать, что у людей с разным уровнем риска и кредитным лимитом – разные вероятности открыть электронное письмо, поэтому credit_limit и risk_score также являются причинами opened. Что касается переменной agreement, мы считаем, что ее причиной является переменная opened. Если мы подумаем о зависимой переменной payments, мы можем представить ее в виде результата воронки: email → opened → agreement → payments Кроме того, мы считаем, что разные уровни риска и кредитного лимита определяют различную готовность заключить соглашение, поэтому мы пометим их как влияющие на переменную agreement. Что касается email и agreement, мы могли бы привести аргумент, что некоторые люди просто читают тему электронного письма, и это уже повышает их вероятность заключить соглашение. Ключевая мысль здесь заключается в том, что переменная email тоже может привести к agreement, минуя opened. На графике мы видим, что opened и agreement являются звеньями причинноследственного пути, проходящего от email к payments. Если бы мы включили их в качестве контрольных переменных в модель регрессии, мы бы интерпретировали регрессионный коэффициент при предикторе email как «влияние email на payments при фиксированных значениях opened и agreement». Однако и opened, и agreement являются частью причинно-следственного эффекта email, поэтому мы не будем фиксировать эти переменные. Вместо этого мы могли бы утверждать, что отправка письма по электронной почте увеличивает платежи именно потому, что она повышает вероятность заключить соглашение о погашении долга. Если мы зафиксируем эти переменные, мы удалим часть фактического эффекта из переменной email. Используя нотацию потенциального результата, можно сказать, что в силу рандомизации E[Y0 | T = 0] = E[Y0 | T = 1]. Однако когда мы контролируем agreement, даже используя рандомизацию, тестовая и контрольная группы больше не являются сопоставимыми. На самом деле, обладая некоторым интуитивным мышлением, мы можем даже догадаться, чем теперь группы отличаются друг от друга: E[Y0 | T = 0, Agreement = 0] > E[Y0 | T = 1, Agreement = 0], E[Y0 | T = 0, Agreement = 1] > E[Y0 | T = 1, Agreement = 1]. Согласно первому уравнению, мы считаем, что клиенты, не получившие электронное письмо и не заключившие соглашения, лучше клиентов, которые получили электронное письмо и не заключили соглашения. Это связано с тем, что если воздействие дает положительный эффект, клиенты, которые не заключили соглашения даже после получения электронного письма,
Плохие контрольные переменные – смещение из-за отбора  123 вероятно, хуже с точки зрения совершения платежей по сравнению с клиентами, которые тоже не заключили соглашения, но при этом не получили дополнительного стимула в виде электронного письма. Что касается второго уравнения, то клиенты, которые заключили соглашение, даже не испытав воздействия (не получив электронного письма), вероятно, лучше клиентов, которые заключили соглашение, но при этом получили дополнительный стимул в виде электронного письма. Приведенное объяснение может сильно сбить с толку при первом прочтении (как в моем случае), убедитесь, что вы его поняли. Прочтите еще раз, если необходимо. Для переменной opened можно привести аналогичное рассуждение. Попробуйте построить его сами. Этот вид смещения настолько распространен, что у него есть свое собственное название. Если спутывание – это смещение, возникающее из-за неспособности проконтролировать общую причину, то смещение из-за отбора (смещение отбора) возникает, когда мы контролируем общее следствие или переменную, лежащую на пути от причины к следствию. Как правило, всегда включайте в вашу модель спутывающие переменные и переменные, которые являются хорошими предикторами Y. Всегда исключайте переменные, которые являются хорошими предикторами только T, медиаторы между воздействием и результатом, общее следствие воздействия и результата. Смещение из-за отбора настолько распространено, что даже рандомизация не может его исправить. Еще лучше то, что он часто привносится неосмотрительными людьми, даже в случайных данных! Выявление и предотвращение смещения из-за отбора требует больше практики, чем мастерства. Часто смещение скрывается за какой-нибудь якобы умной идеей, что еще больше затрудняет его обнаружение. Вот несколько примеров смещения изза отбора, с которыми я уже столкнулся:
     124  Помимо спутывающих переменных 1. Добавление дамми-переменной – факта выплаты всей задолженности при попытке оценить влияние стратегии взыскания на платежи. 2. Контроль наличия у сотрудника работы «белого воротничка» / работы «синего воротничка» при попытке оценить влияние образования на зарплату. 3. Контроль конверсии при оценке влияния процентных ставок на срок кредита. 4. Контроль семейного счастья при оценке влияния детей на внебрачные связи. 5. Разбиение моделирования платежей E[Payments] на два этапа: на первом этапе строим бинарную модель, которая предсказывает, произойдет ли платеж, а на втором этапе строим модель, которая предсказывает сумму платежей, учитывая, что какие-то платежи будут: E[Payments|Payments>0]*P(Payments>0). Примечательно, что все пять вышеперечисленных идей разумно звучат. Смещение из-за отбора так и возникает. Пусть это будет предупреждением. На самом деле я сам много-много раз попадал в описанные выше ловушки, прежде чем узнал, насколько они плохи. Одна из этих ловушек, в частности последняя, заслуживает подробного объяснения, потому что выглядит очень разумной и застает врасплох многих специалистов по обработке данных. Она является настолько распространенной, что у нее есть свое собственное название: плохой COP-эффект! Плохой COP-эффект Представьте, вам нужно спрогнозировать непрерывную переменную с очень большим количеством нулевых значений. Например, если вы хотите смоделировать потребительские расходы, у вас будет что-то вроде гамма-распределения, но с большим количеством нулей. plt.hist(np.concatenate([ np.random.gamma(5, 50, 1000), np.zeros(700) ]), bins=20) plt.xlabel("Потребительские расходы") plt.title("Распределение потребительских расходов");
Плохой COP-эффект  125 Когда специалист по data science видит это, первая идея, которая приходит ему в голову, – разбить моделирование на два этапа. Первый этап – это моделирование готовности участвовать, т. е. вероятности того, что Y > 0. В нашем примере с расходами это была бы модель, прогнозирующая, решил клиент тратить или нет. На втором этапе строим модель, прогнозирующую Y, для тех, кто решил участвовать (т. е. решил тратить). Мы имеем дело с COP-эффектом (Conditional-on-Positives effect – эффект обусловленности положительными результатами). В нашем случае это будет сумма, которую клиент тратит после того, как он решил что-нибудь потратить. Если мы хотим оценить эффект воздействия на траты, то это можно записать примерно так: E[Y | T ] = E[Y | Y > 0, T ]P(Y > 0 | T ). В модели, прогнозирующей участие P(Yi > 0 | Ti ), нет ничего плохого. По факту, если T назначается случайным образом, она будет отражать увеличение вероятности трат из-за воздействия (например, из-за рекламной кампании). Кроме того, в вышеприведенной декомпозиции нет ничего плохого. Она математически верна, получена в соответствии с формулой полной вероятности. Проблема заключается в оценке COP-эффекта. Она будет смещена даже при случайном присваивании. На интуитивном уровне нет ничего безумного в том, чтобы представить, что у некоторых объектов нулевые расходы только потому, что они не подверглись воздействию. Воздействие приведет к тому, что их расходы перестанут быть нулевыми. С другой стороны, у некоторых объектов никогда не бывает нулевых расходов. Воздействие могло бы увеличить их расходы, но даже без него у объектов не было бы нулевых расходов. Теперь ключевым моментом является понимание того, что эти два типа объектов несопоставимы. Объекты, у которых никогда не бывает нулевых расходов, имеют высокое значение Y0 по сравнению с объектами,
­ 126  Помимо спутывающих переменных у которых расходы равны нулю, если они не испытали воздействия. По факту для последних мы можем записать Y0 = 0. Если мы удалим объекты с нулевыми расходами, мы сохраним объекты, у которых никогда не бывает нулевых расходов, как в тестовой, так и в конт рольной группах. Но тогда мы уберем из контрольной группы объекты, у которых при воздействии нулевые расходы меняются на ненулевые. Это привело бы к тому, что тестовая и контрольная группы стали бы уже несопоставимыми, поскольку контрольная группа включала бы только те объекты, у которых никогда не бывает нулевых расходов и у них – более высокие значения Y0, в то время как тестовая группа будет содержать оба типа объектов. Теперь, когда у нас есть интуитивное понимание проблемы, давайте проверим ее с математической точки зрения. Чтобы убедиться в этом, разберем эффект воздействия. При случайном распределении он равен разнице средних значений E[Y | T = 1] – E[Y | T = 0] = = E[Y | Y > 0, T = 1]P(Y > 0 | T = 1) – E[Y | Y > 0, T = 0]P(Y > 0 | T = 0) = = {P(Y > 0 | T = 1) – P(Y > 0 | T = 0)} ∗ E[Y | Y > 0, T = 1] Эффект участия + {E[Y | Y > 0, T = 1] – E[Y | Y > 0, T = 0]} ∗ P(Y > 0 | T = 0). СОР-эффект Здесь последнее равенство получается в результате сложения и вычитания E[Yi | Yi > 0, T i = 1]P(Yi > 0 | T i = 0) и перестановки членов. Это означает, что разница средних значений состоит из двух частей: во-первых, это разница вероятностей того, что результат Y является положительным. Ее называют эффектом участия, поскольку она измеряет увеличение вероятности того, что клиенты будут участвовать в расходовании средств. Во-вторых, это разница результатов, обусловленная участием, COP-эффект. Пока все хорошо. Ничего дурного не произошло. Это математическая истина. Проблема возникает, когда мы пытаемся оценить каждую часть отдельно. Проблема становится более очевидной, если мы еще глубже проанализируем COP-эффект: E[Y | Y > 0, T = 1] – E[Y | Y > 0, T = 0] = E[Y1 | Y1 > 0] – E[Y0 | Y0 > 0] = E[Y1 – Y0 | Y1 > 0] + {E[Y0 | Y1 > 0] – E[Y0 | Y0 > 0]}, Причинно-следственный эффект Смещение из-за отбора где второе равенство возникает после того, как мы складываем и вычитаем E[Yi0 | Yi1 > 0]. Когда мы раскладываем COP-эффект, мы сначала получаем причинно-следственный эффект для субпопуляции участников. В нашем при-
Плохой COP-эффект  127 мере речь идет о причинно-следственном эффекте для тех, кто решит что-то потратить. Во-вторых, мы получаем смещение, которое представляет собой разницу значений Y0 для тех, кто решил принять участие, когда был отнесен к тестовой группе (E[Yi0 | Yi1 > 0]), и тех, кто участвует, даже не попав в тестовую группу (E[Yi0 | Yi0 > 0]). В нашем случае это смещение, вероятно, является отрицательным, поскольку объекты, которые тратят деньги, будучи назначенными в тестовую группу, если бы не попали в нее, вероятно, потратили бы меньше, чем объекты, которые тратят, даже не попав в тестовую группу E[Yi0 | Yi1 > 0] < E[Yi0 | Yi0 > 0]. Мой друг: «Не волнуйся, он – хороший парень». Хороший парень: Я знаю, что поначалу COP-смещение выглядит очень контринтуитивно1, поэтому я думаю, что стоит привести наглядный пример. Допустим, мы хотим оценить, как маркетинговая кампания увеличивает количество людей, покупающих наш продукт. Эта маркетинговая кампания была рандомизированной, поэтому нам не нужно беспокоиться о спутывающих факторах. В этом примере мы можем разбить клиентов на два сегмента. Во-первых, есть те, кто купит наши продукты только в том случае, если увидит маркетинговую кампанию. Давайте назовем этих клиентов бережливыми. Они не потратят деньги, пока мы не подтолкнем их дополнительно. Кроме того, есть клиенты, которые будут тратить деньги даже без кампании. Кампания заставляет их тратить больше, но они все равно потратят, не видя рекламы. Давайте назовем их богатыми клиентами. На рисунке я отобразил контрфактические результаты светлыми цветами и пунктирными линиями. 1 Данный материал и в самом деле вызывает споры, рекомендуем ознакомиться со следующими дискуссиями: https://stats.stackexchange.com/questions/643929/conditional-on-positives-bias и https://github.com/matheusfacure/python-causality-handbook/ issues/261. – Прим. перев.
128  Помимо спутывающих переменных Нужна маркетинговая кампания, чтобы потратить (бережливые) Траты Тратят в любом случае (богатые) E[Y | T = 1] ATE E[Y | T = 0] Маркетинг Нам нужно оценить ATE кампании. Поскольку у нас есть рандомизация, все, что нам нужно сделать, – это сравнить тестовую и контрольную группы. Но предположим, что мы используем COP-формулировку, в которой мы разбиваем оценку на две модели: модель участия, которая оценивает P(Yi > 0 | T i), и COP-модель, которая оценивает E[Yi | Yi > 0]. Данная процедура удаляет из анализа всех, кто не потратил деньги. Нужна маркетинговая кампания, чтобы потратить (бережливые) Траты Тратят в любом случае (богатые) E[Y | T = 1] ATE E[Y | T = 0] Маркетинг Нужна маркетинговая кампания, чтобы потратить (бережливые) Траты Тратят в любом случае (богатые) E[Y | T = 1] E[Y | T = 0] Маркетинг E[Y | T = 1] – E[Y | T = 0]
Плохой COP-эффект  129 Когда мы это делаем, тестовая и контрольная группы больше не являются сопоставимыми. Как мы видим, контрольная группа теперь состоит только из тех клиентов, которые будут тратить деньги даже без участия в маркетинговой кампании. Также обратите внимание, что здесь мы даже знаем направление смещения. Мы получим E[Yi0 | Yi1 > 0] – E[Yi0 | Yi0 > 0], или E[Yi0 | Бережливые и Богатые – E[Yi0 | Богатые]. Очевидно, что смещение является отрицательным, поскольку богатые тратят больше, чем бережливые клиенты. В результате как только мы фильтруем популяцию участников, наша оценка ATE становится смещенной, даже если сначала не было смещения в силу рандомизации. Я искренне надеюсь, что это убедит вас избегать COP как чумы. Я вижу, что слишком много специалистов по data science осуществляют отдельное моделирование, не подозревая о проблемах, которые оно влечет за собой. Чтобы устранить смещение из-за отбора, нам нужно всегда напоминать себе, что никогда не нужно контролировать переменную, которая либо находится между воздействием и результатом, либо является общим следствием воздействия и результата. На языке графов плохая контрольная переменная выглядит следующим образом: g = gr.Digraph() g.edge("T", "X_1"), g.node("T", color="gold"), g.edge("X_1", "Y"), g.node("X_1", color="red") g.edge("T", "X_2"), g.edge("Y", "X_2"), g.node("X_2", color="red") g T X_1 Y X_2
­ 130  Помимо спутывающих переменных Ключевые идеи В этой главе мы рассмотрели переменные, которые не являются спутывающими, и вопрос заключается в том, следует ли нам добавлять их в нашу модель для выявления причинно-следственных связей. Мы увидели, что переменные, которые являются хорошими предикторами результата Y, должны быть добавлены в модель, даже если они не предсказывают воздействие T (не являются спутывающими переменными). Это обусловлено тем, что наличие надежных предикторов, предсказывающих Y, снижает дисперсию оценок и повышает вероятность того, что мы увидим статистически значимые результаты при оценке причинно-следственной связи. Далее, мы увидели, что добавлять переменные, которые предсказывают воздействие, но не результат, – плохая идея. Эти переменные уменьшают вариабельность воздействия, затрудняя нам поиск причинно-следственной связи. Наконец, мы рассмот рели смещение из-за отбора. Это смещение, которое возникает, когда мы контролируем переменную, располагающуюся на причинно-следственном пути от воздействия к результату, или переменные, являющиеся общими следствиями воздействия и результата.
­ Глава 8 Инструментальные переменные Обход смещения, возникшего из-за опущенной переменной Одним из способов контроля смещения, возникающего из-за опущенной переменной, является добавление опущенной переменной в нашу модель. Однако это не всегда возможно, главным образом потому, что у нас просто нет данных об опущенных переменных. Например, давайте вернемся к нашей модели влияния образования на заработную плату: log(wage)i = β0 + κ educi + βAbilityi + ui. Чтобы выяснить причинно-следственное влияние образования на прологарифмированную зарплату, нам нужно проконтролировать фактор Abilityi. Если мы этого не сделаем, у нас, вероятно, будет некоторое смещение, в конце концов, способности, вероятно, являются спутывающим фактором, влия ющим как на воздействие (образование), так и на результат – заработок. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from scipy import stats from matplotlib import style import seaborn as sns from matplotlib import pyplot as plt import statsmodels.formula.api as smf import graphviz as gr
132  Инструментальные переменные from linearmodels.iv import IV2SLS %matplotlib inline pd.set_option("display.max_columns", 10) style.use("fivethirtyeight") g = gr.Digraph() g.edge("ability", "educ") g.edge("ability", "wage") g.edge("educ", "wage") g ability educ wage Один из способов избежать смещения – контролировать уровни способностей при измерении влияния образования на заработную плату. Мы могли бы сделать это, включив способности в нашу модель линейной регрессии. Однако у нас нет точных показателей способностей. Лучшее, что у нас есть, – это довольно сомнительный прокси типа IQ. Но еще не все потеряно. Вот тут-то и вступают в игру инструментальные переменные. Идея инструментальной переменной состоит в том, чтобы найти другую переменную, которая влияет на переменную воздействия и коррелирует с переменной результата только через переменную воздействия. Инструментальная переменная Zi не коррелирует с Y0, но коррелирует с T. Это иногда называют «исключающим ограничением» (exclusion restriction). g = gr.Digraph() g.edge("ability", "educ") g.edge("ability", "wage") g.edge("educ", "wage") g.edge("instrument", "educ") g
Обход смещения, возникшего из-за опущенной переменной  133 instrument ability educ wage Если у нас есть такая переменная, мы можем восстановить причинноследственный эффект κ с помощью формулы инструментальной переменной. Давайте подумаем об идеальном уравнении, которое нам нужно решить. Используя более общие термины, T для воздействия и W для спутывающего фактора, мы получаем нужную формулу: Y i = β 0 + κ T i + β W i + u i. Однако у нас нет данных о W, поэтому все, что мы можем сделать, – это построить: Y i = β 0 + κ T i + v i, v i = β W i + u i. Поскольку W – это спутывающий фактор, Cov(T, v) ≠ 0. У нас здесь короткое уравнение. Применительно к нашему примеру это означало бы, что способности коррелируют с образованием. Если это так, то обучение регрессии короткого вида дало бы смещенную оценку для κ из-за опущенных переменных. А теперь взгляните на магию инструментальной переменной! Поскольку инструментальная переменная Z коррелирует с результатом только через T, это означает, что Cov(Z, v) = 0, в противном случае существовал бы второй путь от Z к Y через W. Помня об этом, мы можем записать Cov(Z, Y ) = Cov(Z, β0 + κ T i + vi) = κ Cov(Z, T ) + Cov(z, v) = κ Cov(Z, T ). Разделив каждую часть на V(Zi) и переставив члены, мы получаем
134  Инструментальные переменные Обратите внимание, что и числитель, и знаменатель являются регрессионными коэффициентами (ковариациями, деленными на дисперсии). Числитель – это результат регрессии Y по Z. Другими словами, это «влияние» Z на Y. Помните, что это не означает, что Z является причиной Y, поскольку у нас есть требование, чтобы Z воздействовал на Y только через T. Скорее, это только отражает, насколько велико влияние Z на Y через T. Этот числитель настолько известен, что имеет свое собственное название: коэффициент регрессии короткого вида (коэффициент короткой регрессии). Знаменатель также является регрессионным коэффициентом. На этот раз это регрессия T по Z. Эта регрессия отражает влияние Z на T, и она также настолько известна, что называется коэффициентом регрессии 1-го этапа. Еще интересно посмотреть на это уравнение с точки зрения частных производных. Мы можем показать, что влияние T на Y равно влиянию Z на Y, масштабированному по влиянию Z на T (или, проще, влиянию Z на Y, деленному на влияние Z на T): Далеко не все понимают вышеприведенный тонкий момент. Записывая инструментальную переменную подобным образом, мы говорим: «Послушайте, трудно определить влияние T на Y из-за спутывающих факторов. Но я могу легко найти влияние Z на Y, поскольку нет ничего, что влияло бы на Z и Y (“исключающее ограничение”). Однако меня интересует влияние T на Y, а не влияние Z на Y. Итак, я оценю простой эффект Z на Y и масштабирую его по эффекту Z на T, чтобы получить эффект в T единицах вместо Z единиц». Кроме того, мы можем увидеть этот момент в упрощенном случае, когда инструментальная переменная является дамми-переменной. В этом случае оценка для инструментальной переменной еще больше упрощается, превращаясь в отношение двух разностей средних: Это отношение иногда называют оценкой Вальда (Wald estimator). Опять же, мы можем рассказать знакомую историю, когда нам нужно определить влияние T на Y, что непросто. Итак, мы сосредоточимся на влиянии Z на Y, а это уже легко. По определению, Z влияет на Y только через T, поэтому теперь мы можем преобразовать влияние Z на Y во влияние T на Y. Мы делаем это, масштабируя влияние Z на Y по влиянию Z на T.
Квартал рождения человека и влияние образования на заработную плату  135 Квартал рождения человека и влияние образования на заработную плату До сих пор мы относились к инструментальной переменной как к некой магической переменной Z, которая обладает чудесным свойством влиять на результат через воздействие. Честно говоря, хорошие инструментальные переменные так трудно найти, что с таким же успехом мы могли бы считать их чудесами. Давайте просто скажем, что это не для слабонервных. Ходят слухи, что крутые ребята из Чикагской школы экономики рассказывают о том, как они придумали ту или иную инструментальную переменную в баре. «Они предложили хорошую инструментальную переменную для уборки кладбища в одиночку с 10 вечера до 5 утра... Взялся бы за эту работу? Эй, демоны, это я – ваш парень». Мем подчеркивает сложность поиска хорошей инструментальной переменной, поэтому найденной инструментальной переменной часто приписывают магические свойства Тем не менее у нас есть несколько интересных примеров инструментальных переменных, позволяющих подробнее раскрыть тему. Мы снова попытаемся оценить влияние образования на заработную плату. Для этого мы в качестве инструментальной переменной Z возьмем квартал рождения человека. Эта идея использует преимущества закона США об обязательном посещении занятий. В нем прописано, что к 1 января года поступления в школу ребенку должно исполниться 6 лет. По этой причине дети, родившиеся в начале года, пойдут в школу в более старшем возрасте. Закон об обязательном посещении школы также требует, чтобы учащиеся посещали школу до тех пор, пока им не исполнится 16 лет, после чего им по закону разрешается бросить учебу. В результате получается, что в среднем у людей, родившихся в конце года, количество лет, потраченных на обучение в школе, больше, чем у людей, родившихся в начале года.
136  Инструментальные переменные Q1 Q2 Q3 Q4 Q1 Q2 День рождения Q1 Q2 Q3 Q3 Q4 Возраст, когда можно бросить школу Q4 Q1 День рождения Q2 Q3 Q4 Возраст, когда можно бросить школу Зачислен в школу Если мы примем, что квартал рождения человека (qob – quarter of birth) не зависит от способностей, то есть он не вносит путаницу во влияние образования на заработную плату, мы можем использовать его как инструментальную переменную. Другими словами, мы должны верить, что квартал, в котором родился человек, никак не влияет на его заработную плату, только опосредованно через свое влияние на образование. Если вы не верите в астрологию, то это очень убедительный аргумент. g = gr.Digraph() g.edge("ability", "educ") g.edge("ability", "wage") g.edge("educ", "wage") g.edge("qob", "educ") g qob ability educ wage Для проведения нашего анализа мы можем воспользоваться данными трех десятилетних переписей, теми же данными, которые использовали Ангрист
Коэффициент регрессии 1-го этапа  137 и Крюгер в своей статье, посвященной инструментальным переменным: http://piketty.pse.ens.fr/files/AngristKrueger1991.pdf. Этот набор данных содержит информацию о прологарифмированной заработной плате – переменной результата, и количестве лет, потраченных на образование, – переменной воздействия. В нем также есть данные о квартале рождения – инструментальной переменной – и дополнительные контрольные переменные, такие как год рождения и место рождения. data = pd.read_csv("data/ak91.csv") data.head() Коэффициент регрессии 1-го этапа Прежде чем использовать квартал рождения в качестве инструментальной переменной, нам нужно убедиться, что он является валидной переменной. Это подразумевает подтверждение двух предположений об инструментальных переменных: 1. Cov(Z, T ) ≠ 0. У нас должен быть сильный коэффициент регрессии 1-го этапа, т. е. инструментальная переменная действительно должна влиять на переменную воздействия. 2. Y ⊥ Z | T. Это «исключающее ограничение», согласно которому инструментальная переменная Z влияет на результат Y только через воздействие T. Первое предположение, к счастью, поддается проверке. Исходя из данных, мы можем увидеть, что Cov(Z, T ) не равна нулю. В нашем примере если квартал рождения действительно является инструментальной переменной, нам следует ожидать, что люди, родившиеся в последнем квартале года, будут иметь чуть большее количество лет, потраченных на обучение в школе, чем те, кто родился в начале года. Прежде чем запускать какой-либо статистический тест, чтобы убедиться в этом, давайте просто построим график наших данных и увидим это собственными глазами. group_data = (data .groupby(["year_of_birth", "quarter_of_birth"]) [["log_wage", "years_of_schooling"]]
138  Инструментальные переменные .mean() .reset_index() .assign(time_of_birth = lambda d: d["year_of_birth"] + (d["quarter_of_birth"])/4)) plt.figure(figsize=(15,6)) plt.plot(group_data["time_of_birth"], group_data["years_of_schooling"], zorder=-1) for q in range(1, 5): x = group_data.query(f"quarter_of_birth=={q}")["time_of_birth"] y = group_data.query(f"quarter_of_birth=={q}")["years_of_schooling"] plt.scatter(x, y, marker="s", s=200, c=f"C{q}") plt.scatter(x, y, marker=f"${q}$", s=100, c=f"white") plt.title("Количество лет, потраченных на обучение в школе в зависимости\n" "от квартала рождения (первый этап)") plt.xlabel("Год рождения") plt.ylabel("Количество лет, потраченных на обучение в школе"); Примечательно, что для количества лет, потраченных на обучение в школе, характерна квартальная сезонность. Визуально мы можем увидеть, что те, кто родился в первом квартале года, почти всегда имеют меньшее количество лет, потраченных на обучение, чем те, кто родился в последнем квартале (если учесть год рождения, то в целом рожденные в более поздние годы потратили на обучение большее количество лет). Для строгости вычислим коэффициент регрессии 1-го этапа, т. е. обучим линейную регрессию. Перед этим мы преобразуем квартал рождения в дамми-переменные. factor_data = data.assign(**{f"q{int(q)}": (data["quarter_of_birth"] == q).astype(int) for q in data["quarter_of_birth"].unique()}) factor_data.head()
Коэффициент регрессии 1-го этапа  139 Для простоты давайте пока будем использовать в качестве инструментальной переменной только последний квартал, q4. Мы построим регрессию количества лет, потраченных на обучение в школе (переменной воздействия), по кварталу рождения (инструментальной переменной). Она покажет нам, действительно ли квартал рождения положительно влияет на количество лет обучения, как мы видели на графике выше. Кроме того, нам нужно указать здесь в качестве дополнительных контрольных переменных год рождения (year_of _birth, в тексте также будем использовать сокращение yob) и штат рождения (state_of_birth, в тексте также будем использовать сокращение sob). first_stage = smf.ols( "years_of_schooling ~ C(year_of_birth) + C(state_of_birth) + q4", data=factor_data).fit() print("оценка параметра q4:, ", first_stage.params["q4"]) print("p-value параметра q4:, ", first_stage.pvalues["q4"]) оценка параметра q4:, 0.10085809272786139 p-value параметра q4:, 5.464829416631669e-15 Похоже, что те, кто родился в последнем квартале года, на 0.1 года «образованнее» тех, кто родился в другие кварталы года. Здесь p-значение близко к нулю. Это закрывает вопрос о том, приводит ли квартал рождения к большему или меньшему количеству лет обучения в школе. Когда у тебя есть отличная идея для инструментальной переменной, но первый этап оказывается незначимым Невероятно! Может, архивы не полные?
140  Инструментальные переменные Коэффициент короткой регрессии К сожалению, мы не можем проверить второе предположение касательно инструментальных переменных. Мы можем только аргументировать в его пользу. Мы можем выразить убеждение, что квартал рождения не влияет на потенциальный заработок. Другими словами, время рождения людей не является показателем их личных способностей или каким-либо другим фактором, который может вызвать разницу в доходах, если не считать его влияние на образование. Хороший способ порассуждать об этом – предположить, что квартал рождения практически является случайной величиной, когда мы оцениваем его влияние на заработок. (Хотя на самом деле он не случаен. Есть свидетельства того, что люди, как правило, беременеют примерно в конце лета или во время какого-либо отпуска. Но я не могу придумать ни одной веской причины, по которой квартал рождения влияет на доход каким-либо иным образом, кроме как через образование.) Приведя аргументы в пользу «исключающего ограничения», мы можем перейти к вычислению коэффициента короткой регрессии. Коэффициент короткой регрессии позволяет выяснить, как инструментальная переменная влияет на переменную результата. Поскольку, согласно предположению, все это влияние осуществляется через переменную воздействия, это прольет некоторый свет на то, как переменная воздействия влияет на переменную результата. Еще раз, давайте оценим влияние визуально, прежде чем всерьез приступать к обучению регрессии. plt.figure(figsize=(15,6)) plt.plot(group_data["time_of_birth"], group_data["log_wage"], zorder=-1) for q in range(1, 5): x = group_data.query(f"quarter_of_birth=={q}")["time_of_birth"] y = group_data.query(f"quarter_of_birth=={q}")["log_wage"] plt.scatter(x, y, marker="s", s=200, c=f"C{q}") plt.scatter(x, y, marker=f"${q}$", s=100, c=f"white") plt.title("Средняя недельная зарплата по кварталу " "рождения (сокращенная форма)") plt.xlabel("Год рождения") plt.ylabel("Прологарифмированная недельная зарплата");
Инструментальные переменные, созданные вручную  141 И снова мы можем наблюдать сезонную закономерность в распределении доходов по кварталам рождения. Те, кто родился в конце года, имеют несколько более высокий доход, чем те, кто родился в начале года. Чтобы проверить эту гипотезу, мы снова построим регрессию прологарифмированной заработной платы по инструментальной переменной q4. Мы также добавим те же самые контрольные переменные, что и при оценивании коэффициента регрессии 1-го этапа. reduced_form = smf.ols( "log_wage ~ C(year_of_birth) + C(state_of_birth) + q4", data=factor_data).fit() print("оценка параметра q4:, ", reduced_form.params["q4"]) print("p-value параметра q4:, ", reduced_form.pvalues["q4"]) оценка параметра q4:, 0.008603484260136768 p-value параметра q4:, 0.0014949127183721367 И снова у нас есть статистически значимый результат. У тех, кто родился в последнем квартале года, заработная плата в среднем на 0.8 % выше. На этот раз p-значение не так близко к нулю, как раньше, но оно все еще является довольно значимым и составляет всего 0.0015. Инструментальные переменные, созданные вручную Получив коэффициент регрессии 1-го этапа и коэффициент короткой регрессии, мы теперь можем поделить второй коэффициент на первый. Поскольку коэффициент регрессии 1-го этапа был примерно равен 0.1, он умножит эффект коэффициента короткой регрессии почти в 10 раз. Это даст нам несмещенную, полученную при помощи ИП оценку усредненного причинноследственного влияния образования на заработную плату: # оцениваем усредненный причинно-следственный эффект reduced_form.params["q4"] / first_stage.params["q4"] 0.08530286492082466 Это означает, что каждый дополнительный учебный год будет увеличивать заработную плату на 8.5 %. Другой способ получить оценки инструментальных переменных – это использовать двухэтапную регрессию наименьших квадратов, 2SLS. На первом
142  Инструментальные переменные этапе получаем предсказанные значения для количества лет, потраченных на обучение, как мы это делали раньше, вычисляя коэффициент регрессии 1-го этапа, а затем на втором этапе заменяем переменную воздействия предсказанными значениями, полученными на первом этапе. educi = γ0 + γ1 × q4i + γ2 × yobi + γ3 × sobi + vi. регрессия 1-го этапа log(wage)i = β0 + β1 × educi + β2 × yobi + β3 × sobi + ui. короткая регрессия log(wage)i = β0 + β1 [γ0 + γ1 × q4i + γ2 × yobi + γ3 × sobi + vi] + β2 × yobi + β3 × sobi + ui. Следует отметить одну вещь: при создании инструментальной переменной любая дополнительная контрольная переменная, которую мы добавляем в регрессию на втором этапе, также должна быть добавлена в регрессию на первом этапе. formula = ("log_wage ~ C(year_of_birth) + C(state_of_birth) + " "years_of_schooling_fitted") iv_by_hand = smf.ols(formula, data=factor_data.assign( years_of_schooling_fitted=first_stage.fittedvalues)).fit() iv_by_hand.params["years_of_schooling_fitted"] 0.08530286492080791 Видим, что оценка параметра точно такая же. Этот альтернативный взгляд на инструментальную переменную может быть полезен, поскольку интуитивно понятен. В 2SLS на первом этапе создается новая версия переменной воздействия, которая очищена от смещения, возникающего из-за опущенной переменной. Затем мы используем эту очищенную версию переменной воздействия – предсказанные значения, полученные на первом этапе, в линейной регрессии. Однако на практике мы не создаем инструментальные переменные вручную. Не потому, что это хлопотно, а потому, что стандартные ошибки, которые мы получаем на втором этапе, в некоторой степени некорректны. Мы всегда можем поручить машине выполнить работу за нас. В Python можно воспользоваться библиотекой linearmodels для корректного обучения 2SLS. Формула для 2SLS немного отличается. Мы должны добавить в формулу первый этап, записанный внутри квадратных скобок [ ]. В нашем случае мы добавляем years_of_schooling ~ q4. Дополнительные контрольные переменные на первом этапе добавлять не нужно, потому что компьютер сделает это автоматически, если мы включим их на втором этапе. По этой причине мы добавляем year_of_birth и state_of_birth в формулу вне квадратных скобок. # пишем функцию вывода сводки по модели def parse(model, exog="years_of_schooling"): param = model.params[exog] se = model.std_errors[exog] p_val = model.pvalues[exog]
Несколько инструментальных переменных  143 print(f"Оценка параметра: {param}") print(f"Стандартная ошибка: {se}") print(f"95%-ный доверительный интервал:\n" f"{(-1.96*se,1.96*se) + param}") print(f"p-значение: {p_val}") # обучаем двухэтапную регрессию наименьших квадратов formula = ("log_wage ~ 1 + C(year_of_birth) + C(state_of_birth) + " "[years_of_schooling ~ q4]") iv2sls = IV2SLS.from_formula(formula, factor_data).fit() parse(iv2sls) Оценка параметра: 0.08530286493623862 Стандартная ошибка: 0.025540812814018717 95%-ный доверительный интервал: [0.03524287 0.13536286] p-значение: 0.0008381914643396104 Еще раз, мы видим, что параметр точно такой же, как и полученный ранее. Дополнительным преимуществом является то, что теперь у нас корректно вычислены стандартные ошибки. Учитывая это, мы можем опять сказать, что один дополнительный год обучения увеличивает заработную плату в среднем на 8.5 %. Несколько инструментальных переменных Еще одно преимущество использования компьютера для обучения 2SLS заключается в том, что легко добавить несколько инструментальных переменных. В нашем примере мы будем использовать все дамми-переменные для переменной quarter of birth в качестве инструментальных переменных, помогающих спрогнозировать количество лет, потраченных на обучение в школе. # обучаем двухэтапную регрессию наименьших квадратов # с несколькими инструментальными переменными formula = ("log_wage ~ 1 + C(year_of_birth) + C(state_of_birth) + " "[years_of_schooling ~ q1+q2+q3]") iv_many_zs = IV2SLS.from_formula(formula, factor_data).fit() parse(iv_many_zs) Оценка параметра: 0.10769370489288121 Стандартная ошибка: 0.01955714900954945 95%-ный доверительный интервал: [0.06936169 0.14602572] p-значение: 3.657974678716869e-08
144  Инструментальные переменные Для всех трех дамми-переменных оцененная отдача от образования составляет 0.1, что означает, что мы должны ожидать увеличения заработка в среднем на 10 % за каждый дополнительный год обучения. Давайте сравним это с традиционной оценкой по методу OLS. Чтобы сделать это, мы можем снова воспользоваться 2SLS, но теперь без первого этапа. # обучаем регрессию по методу OLS, просто # строим 2SLS без первого этапа formula = ("log_wage ~ years_of_schooling + C(state_of_birth) + " "C(year_of_birth) + C(quarter_of_birth)") ols = IV2SLS.from_formula(formula, data=data).fit() parse(ols) Оценка параметра: 0.06732572817658156 Стандартная ошибка: 0.00038839984390485886 95%-ный доверительный интервал: [0.06656446 0.06808699] p-значение: 0.0 Отдача от обучения при использовании OLS оценена немного ниже, чем при использовании 2SLS. Это говорит о том, что смещение, вызванное опущенной переменной, возможно, не так сильно, как мы предполагали вначале. Кроме того, обратите внимание на доверительные интервалы. Оценка параметра в 2SLS имеет гораздо более широкий CI, чем оценка параметра OLS. Давайте разберем этот момент подробнее. ДОКТОР: Будет немного больно РЕБЕНОК: ОК ДОКТОР: У тебя – валидный, но слабый инструмент Когда мы имеем дело с инструментальной переменной, нам нужно помнить, что мы оцениваем ATE косвенно. Наша оценка зависит как от первого этапа, так и от второго этапа. Если влияние переменной воздействия на переменную результата действительно будет сильным, то и оценка, полученная
Несколько инструментальных переменных  145 на втором этапе, также будет значимой. Однако не имеет значения, насколько значима оценка, полученная на втором этапе, если на первом этапе мы получили незначимую оценку. Незначимая оценка на первом этапе означает, что инструментальная переменная очень слабо коррелирует с переменной воздейcтвия. Таким образом, мы не сможем много узнать о переменной воздействия с помощью этой инструментальной переменной. Формулы вычисления ИП-стандартных ошибок немного сложны и не столь интуитивно понятны, поэтому сейчас что-нибудь придумаем, чтобы разобраться с этой проблемой. Мы сгенерируем данные, в которых у нас есть переменная воздействия T с влиянием 2.0 на переменную результата Y, ненаблюдаемая спутывающая переменная U и дополнительная контрольная переменная X. Кроме того, на 1-м этапе мы сгенерируем инструментальные переменные разной силы: X ∼ N(0, 22), U ∼ N(0, 22), T ∼ N(1 + 0.5U, 52), Y ∼ N(2 + X – 0.5U + 2T, 52), Z ∼ N(T, σ2) для σ2 в диапазоне от 0.1 до 100. np.random.seed(12) n = 10000 # наблюдаемая переменная X = np.random.normal(0, 2, n) # ненаблюдаемая (опущенная) переменная U = np.random.normal(0, 2, n) # переменная воздействия T = np.random.normal(1 + 0.5*U, 5, n) # переменная результата Y = np.random.normal(2 + X - 0.5*U + 2*T, 5, n) stddevs = np.linspace(0.1, 100, 50) # инструментальные переменные с уменьшающимся # значением \mathrm{Cov}(Z, T) Zs = {f"Z_{z}": np.random.normal(T, s, n) for z, s in enumerate(stddevs)} sim_data = pd.DataFrame(dict(U=U, T=T, Y=Y)).assign(**Zs) sim_data.head()
146  Инструментальные переменные Убедимся, что корреляция между Z и T действительно уменьшается. corr = (sim_data.corr()["T"] [lambda d: d.index.str.startswith("Z")]) corr.head() Z_0 Z_1 Z_2 Z_3 Z_4 Name: 0.999807 0.919713 0.773434 0.634614 0.523719 T, dtype: float64 Теперь мы обучим по одной модели для каждой имеющейся у нас инструментальной переменной и соберем как оценки ATE, так и стандартные ошибки. se = [] ate = [] for z in range(len(Zs)): formula = f'Y ~ 1 + X + [T ~ Z_{z}]' iv = IV2SLS.from_formula(formula, sim_data).fit() se.append(iv.std_errors["T"]) ate.append(iv.params["T"]) plot_data = pd.DataFrame( dict(se=se, ate=ate, corr=corr)).sort_values(by="corr") plt.scatter(plot_data["corr"], plot_data["se"]) plt.xlabel("Corr(Z, T)") plt.ylabel("ИП-стандартная ошибка ATE"); plt.title("Дисперсия ИП-оценок ATE,\n" "полученных на 1-м этапе");
 ­ Несколько инструментальных переменных  147 plt.scatter(plot_data["corr"], plot_data["ate"]) plt.fill_between(plot_data["corr"], plot_data["ate"]+1.96*plot_data["se"], plot_data["ate"]-1.96*plot_data["se"], alpha=.5) plt.xlabel("Corr(Z, T)") plt.ylabel("$\hat{ATE}$"); plt.title("ИП-оценки ATE, полученные на 1-м этапе"); Как можно увидеть на графиках выше, оценки сильно различаются, когда корреляция между T и Z становится слабой. Это связано с тем, что SE су щественно увеличивается при низкой корреляции. Еще одна вещь, на которую следует обратить внимание, – это то, что модель 2SLS смещена! Даже при высокой корреляции оценка параметра все равно не достигает истинного значения ATE 2.0. На самом деле 2.0 даже не входит в 95%-ный доверительный интервал! Модель 2SLS является лишь состоятельной, это означает, что вычисленная оценка приближается к истинному значению параметра при достаточно большом размере выборки. Однако мы не знаем, насколько большим должен быть достаточно большой размер выборки. Мы можем только придерживаться некоторых эмпирических правил, чтобы понять, как ведет себя данное смещение. 1. Модель 2SLS смещена в сторону модели OLS. Это означает, что если модель OLS имеет отрицательное/положительное смещение, то у модели 2SLS будет такое же смещение. Преимущество модели 2SLS заключается в том, что она в случае опущенных переменных по крайней мере состоятельна, в то время как OLS – нет. В вышеприведенном примере наша ненаблюдаемая (опущенная) переменная U отрицательно влияет на переменную результата, но положительно коррелирует с переменной воздействия, что приведет к отрицательному смещению. Вот почему мы видим оценку ATE ниже истинного значения (отрицательное смещение).
­    148  Инструментальные переменные 2. Смещение будет увеличиваться с увеличением количества добавляемых инструментальных переменных. Если мы добавляем слишком много инструментальных переменных, модель 2SLS становится все больше похожей на модель OLS. Помимо информации о том, как ведет себя данное смещение, дадим несколько советов, которые помогут избежать некоторых распространенных ошибок при создании инструментальных переменных: 1. Создавать инструментальные переменные вручную. Как мы убедились, создавая инструментальные переменные вручную, мы получим некорректно вычисленные стандартные ошибки, даже если оценки параметров будут верными. Да, вы получите какие-то стандартные ошибки, и все же лучше воспользоваться программным обеспечением и получить правильно вычисленные стандартные ошибки. 2. Использовать другую модель вместо OLS на 1-м этапе. Многие специа листы по data science сталкиваются с инструментальными переменными и думают, что могут добиться большего. Например, они видят дамми-переменную воздействия и подумывают о замене 1-го этапа логистической регрессией, в конце концов, они могут предсказать дамми-переменную, ведь верно? Проблема в том, что такая постановка задачи является явно неверной. Состоятельность инструментальной переменной зависит от свойства, которое может дать только OLS, этим свойством является ортогональность остатков, поэтому любая модель, отличающаяся от OLS на 1-м этапе, приведет к смещенным результатам. (Есть несколько современных методов, которые используют машинное обучение для работы с инструментальными переменными, но их результаты были в лучшем случае сомнительными.) Ключевые идеи Мы потратили здесь некоторое время, чтобы понять, как можно обойти смещение, вызванное опущенной переменной, если у нас есть инструментальная переменная. Инструментальная переменная – это переменная, которая коррелирует с переменной воздействия (первый этап), но влияет на переменную результата только через переменную воздействия (исключающее ограничение). Мы показали пример инструментальной переменной, когда использовали квартал рождения для оценки влияния образования на доход. Затем мы углубились в механику оценки причинно-следственной связи с помощью инструментальных переменных, а именно применили модель 2SLS. Мы также узнали, что инструментальная переменная – это не серебряная пуля. С ней могут быть проблемы, когда у нас – слабые оценки на первом этапе. Кроме того, несмотря на состоятельность, модель 2SLS по-прежнему является смещенным методом оценки причинно-следственных связей.
Глава 9 Несоблюдение требований и LATE Погружаемся в разнородный мир Ранее мы рассматривали инструментальные переменные через традиционную призму. Инструментальная переменная рассматривалась как своего рода естественный эксперимент, которым мы можем воспользоваться. Напротив, современная практика работы с инструментальными переменными опирается на знания в области медицины. Она делит мир на 4 вида субъектов в зависимости от того, как они реагируют на инструментальную переменную: 1) 2) 3) 4) «послушные исполнители» (compliers); «всегда отказывающиеся» (never-takers); «всегда принимающие» (always-takers); «бунтари» (defiers). Эти названия пришли из медицины. Представьте, что вы проводите эксперимент, чтобы проверить действие нового лекарства на какое-то заболевание. Каждому испытуемому назначается определенное лечение: лекарственное средство или плацебо. «Послушные исполнители» – это субъекты, которые придерживаются того, что им было назначено. Если они получают плацебо, они принимают его; если они получают лекарство, они тоже принимают его. «Всегда отказывающиеся» – это субъекты, которые отказываются принимать свои лекарства. Даже если им назначат новое лекарство, они не будут его принимать. С другой стороны, «всегда принимающие» – это те, кто может каким-то образом получить новое лекарство, даже если им было назначено плацебо. Наконец, «бунтари» – это те, кто принимают лекарство, если они отнесены к контрольной группе (т. е. не должны принимать лекарство), и отказываются принимать лекарство, если они отнесены к тестовой группе (т. е. должны принимать лекарство). Они напоминают вредного ре-
150  Несоблюдение требований и LATE бенка, который делает противоположное тому, что ему говорят. На практике они встречаются не так часто («бунтари», а не дети), поэтому мы нередко игнорируем их. Исследователь: «Вы – в контрольной группе. Нет необходимости принимать лечение». «Бунтарь»: «Но я хочу лечение!» Исследователь: «Шучу, вы – в тестовой группе. Вот оно». «Бунтарь»: Ну, теперь я не буду это делать Современная практика работы с инструментальными переменными рассматривает инструментальную переменную как квазиэксперимент, в которой соответствие требованиям не является идеальным. Поступая таким образом, она проводит различие между внутренним и внешним валидными причинно-следственными эффектами. Внутренний валидный эффект – это эффект, который мы можем идентифицировать. Он валиден для имеющихся условий и данных. В случае с IV речь идет об эффекте воздействия на тех, у кого инструментальная переменная изменяет переменную воздействия. С другой стороны, внешняя валидность связана с предсказательной силой этого причинно-следственного эффекта. Возникает вопрос, можем ли мы обобщить эффект, обнаруженный нами в этой выборке, на другие популяции. Например, вы провели РКИ в своем университете, чтобы выяснить, проявляют люди щедрость или нет, когда у них есть стимул делать пожертвования. Эксперимент хорошо спланирован, но вы приглашаете к участию только студентов-экономистов. Затем вы обнаруживаете, что все они – эгоистичные придурки. Это внутренне валидный вывод. Он валиден для этих наблюдений. Но можете ли вы на основании этого эксперимента сделать вывод, что человечество эгоистично? Вряд ли. Поэтому следует задаться вопросом, имеет ли ваш эксперимент внешнюю валидность для обобщения его результатов. В любом случае, вернемся к инструментальной переменной.
Погружаемся в разнородный мир  151 Давайте рассмотрим случай, когда вы хотите повысить вовлеченность пользователей, которая измеряется покупками, совершаемыми в приложении. Один из способов сделать это – попросить ваш отдел маркетинга придумать пуш-уведомление, которое вы могли бы использовать для привлечения пользователей. Маркетологи предлагают великолепный дизайн и очень эффектное взаимодействие с пользователем. После разработки пуш-уведомления вы переходите к разработке рандомизированного исследования. Вы выбираете случайным образом 10 000 клиентов и назначаете им пуш-уведомления с вероятностью 50 %. Однако когда вы выполняете тест, вы замечаете, что некоторым клиентам, которым было назначено пуш-уведомление, это уведомление не пришло. Поговорив с инженерами, вы выяснили, что это обусловлено тем, что у таких клиентов, вероятно, более старая модель телефона, не поддерживающая тот тип пуш-уведомления, который был разработан маркетинговой командой. Поначалу вы можете подумать, что в этом нет ничего особенного. В качестве переменной воздействия я вместо воздействия на объекты, которым пуш-уведомление назначено, буду использовать воздействие на объекты, которым пуш-уведомление доставлено, ведь верно? Оказывается, все не так просто. Давайте построим причинно-следственный граф всей этой ситуации. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from scipy import stats from matplotlib import style import seaborn as sns from matplotlib import pyplot as plt import statsmodels.formula.api as smf from linearmodels.iv import IV2SLS import graphviz as gr %matplotlib inline style.use("fivethirtyeight") g = gr.Digraph() g.edge("push assigned", "push delivered") g.edge("push delivered", "in app purchase") g.edge("income", "in app purchase") g.edge("income", "push delivered") g.node("income", color="blue") g
­ ­ 152  Несоблюдение требований и LATE push assigned income push delivered in app purchase У причинно-следственного графа есть узел «пуш присвоен» (push assigned). Он случаен по замыслу, таким образом, причины, породившие его, нам неизвестны. Затем у вас есть узел «пуш доставлен», определяющий, было ли доставлено пуш-уведомление (push delivered). Не все, кто должен был получить пуш-уведомление, получили его, поэтому здесь имеет место несоблюдение требований (non-compliance). Говоря точнее, у нас есть некоторое количество «всегда отказывающихся» – тех, кто не принимает лечение, даже если оно ему назначено. Кроме того, у вас есть основания подозревать, что появление таких клиентов не является случайным. Поскольку люди со старыми телефонами – это люди, не получившие пуш-уведомления, можно утверждать, что доход также является причиной, влияющей на доставку пуш-уведомлений. Чем богаче человек, тем больше вероятность того, что у него или нее будет более продвинутый телефон, что, в свою очередь, повышает вероятность того, что клиент получит пуш-уведомление. Наконец, у вас есть переменная результата – покупка в приложении (in app purchase). Имейте в виду, что мы не знаем дохода, поэтому не можем контролировать его. Помня об этом, давайте посмотрим, что произойдет, если попробовать push assigned в качест ве переменной воздействия, а затем попробовать push delivered в качестве переменной воздействия. Сначала мы оценим причинно-следственный эффект с помощью следующей разницы средних значений: ATE = E[Y | pushAssigned = 1] – E[Y | pushAssigned = 0]. Как мы уже прекрасно знаем, это всего лишь несмещенная оценка E[Y1] – E[Y0], при условии что смещение E[Y0 | pushAssigned = 0] – E[Y0 | pushAssigned = 1] равно нулю. Поскольку pushAssigned случаен, мы знаем, что смещение равно нулю. Проб лема решена? Не совсем. Видите ли, если мы применим вышеприведенный сценарий, мы на самом деле ответим на совершенно другой вопрос, отличающийся от того, на который собирались ответить. Мы обнаружим причинно-следственный эффект назначения воздействия, а не самого
Погружаемся в разнородный мир  153 воздействия. Но отличаются ли они друг от друга или мы можем экстраполировать причинно-следственный эффект назначения воздействия на ATE? Другими словами, является ли причинно-следственный эффект назначения воздействия объективной оценкой ATE? Оказывается, это не так. Из-за несоблюдения требований результаты клиентов, которые были подвергнуты воздействию, будут сдвинуты в сторону результатов клиентов, которые были отнесены к контрольной группе (т. е. не подвергались воздействию). Несоблюдение требований непреднамеренно меняет воздействие, делая тестовую и контрольную группы более схожими по результатам. Не путайте это со сходством по переменным. Мы хотим, чтобы тестовая и контрольная группы были схожи по переменным. Это сделает их сопоставимыми. Однако мы не хотим, чтобы группы были схожи по результату, если эффект воздействия действительно существует. Чтобы убедиться в этом, предположим, что у нас есть «всегда испытывающие воздействие». Некоторые из них попадут в контрольную группу (группу, в которой участники не испытывают воздействие) случайно. Но те, кто попадут, все равно будут испытывать воздействие. Это делает их, по сути, группой объектов, испытавших воздействие, смешанной с контрольной группой. В результате такого смешивания причинно-следственный эффект будет труднее обнаружить, когда у нас происходит несоблюдение требований. Тестовая (участники испытывают воздействие) Выборка E[Y | pushAssigned = 1] – E[Y | pushAssigned = 0] Тестовая (участники испытывают воздействие) Выборка E[Y | pushAssigned = 1] – E[Y | pushAssigned = 0] «Всегда испытывающие» Контрольная (участники не испытывают воздействия) Контрольная (участники не испытывают воздействия) По той же самой причине «всегда отказывающиеся от воздействия» сделают тех, кто отнесен к тестовой группе (группе, в которой участники испытывают воздействие), немного похожими на тех, кто не испытывает воздействия, потому что «всегда отказывающиеся» не испытывают воздействия, даже если оно им назначено. В этом смысле причинно-следственный эффект назначения воздействия смещается к нулю, поскольку несоблюде-
154  Несоблюдение требований и LATE ние требований снижает обнаруживаемое влияние. Другой способ увидеть это – представить крайний случай. Допустим, уровень несоблюдения требований действительно является высоким. В назначении воздействия ничего не говорится о полученном воздействии. Полученное воздействие в данном случае является чисто случайным. На языке инструментальных переменных это означало бы, что у нас очень слабый 1-й этап. Обозначив назначение воздействия с помощью Z, мы получим следующее выражение: E[Y | Z = 1] – E[Y | Z = 0] = 0. Соответственно, больше не будет причинно-следственной связи между назначением воздействия и результатом. Z будет просто бессмысленной случайной величиной. g = gr.Digraph() g.node("push assigned") g.edge("push delivered", "in app purchase") g.edge("income", "in app purchase") g.edge("income", "push delivered") g.node("income", color="blue") g push assigned income push delivered in app purchase Итак, мы не будем использовать причинно-следственный эффект назначения в качестве способа оценить причинно-следственный эффект воздействия. А как насчет того, чтобы просто использовать воздействие доставки? ATE = E[Y | push = 1] – E[Y | push = 0]. Еще раз, нам нужно подумать, является оно смещенным или нет, т. е. E[Y0 | push = 0] – E[Y0 | push = 1]. Просто взглянув на причинно-следственный граф выше, мы понимаем, что ничего у нас не выйдет. У нас есть неизмеримый спутывающий фактор – доход, который таится повсюду, и он обязательно все испортит. Как мы уже говорили ранее, мы знаем, что сбой при доставке пуш-уведомления в нашем случае вызван тем, что у клиентов – старые телефоны. Это означает, что у нас, вероятно, E[Y0 | push = 0] – E[Y0 | push = 1]. Мы
­ ­ ­ ­ Локальный средний эффект воздействия (local average treatment effect – LATE)  155 думаем, что это верно, потому что у клиентов с меньшим доходом – старые телефоны, что приведет к push = 0, а также к более низкому потенциалу покупок в приложении Y0. Как бы не так! Мы не можем использовать назначение воздействия (при своение пуш-уведомлений) или само воздействие (доставку пуш-уведомле ний) для оценки ATE. Но, к счастью, мы знаем, что можно использовать. Это инструментальные переменные. Здесь назначение воздействия (присвоение пуш-уведомлений) является идеальной инструментальной переменной для воздействия. Это практически случайное событие, и оно влияет на покупку в приложении только через воздействие. Локальный средний эффект воздействия (local average treatment effect – LATE) Локальный средний эффект воздействия четко определяет популяцию, для которой мы можем оценить причинно-следственный эффект. Кроме того, это еще один способ взглянуть на инструментальные переменные, который дает нам прочие интересные идеи для применения. В современной практике использования инструментальных переменных мы рассматриваем инструментальную переменную как начало причинно-следственной цепочки: переменная Z влияет на переменную T, которая влияет на переменную Y. В этом контексте исключающее ограничение означает, что Z влияет на Y только через переменную T. 1-й этап теперь рассматривается как причинно-следственное влияние Z на T. Мы также перепишем потенциальные результаты, используя двойную индексацию (для инструментальной переменной – назначения воздействия и воздействия). Вновь для каждого отдельного клиента мы наблюдаем только одно из четырех возможных значений (так как один и тот же человек не мог одновременно получить и не получить пуш-уведомление или одновременно попасть в число тех, кому должны прислать пуш-уведомление, и не попасть). В каком-то смысле воздействие становится результатом по крайней мере на первом этапе. Это означает, что мы можем записать его, используя нотацию потенциального результата:
   ­ 156  Несоблюдение требований и LATE Она замечательная Но мне нравится эта Предположения об инструментальных переменных теперь можно перепи сать следующим образом: 1. T0i, T1i ⊥ Zi и Yi(T1i, 1), Yi(T0i, 0) ⊥ Zi. Это предположение о независимости. Оно говорит о том, что инструментальная переменная является случайной. Другими словами, инструментальная переменная Z не коррелирует с потенциальными воздействиями, а это то же самое, что сказать, что люди в разных IV-группах сопоставимы. Применительно к нашему примеру попадание в число тех, кому пуш-уведомление должно быть отправлено, случайно, не зависит от характеристик клиента. 2. Yi(1, 0) = Yi(1, 1) = Yi1 и Yi(0, 0) = Yi(0, 1) = Yi0. Это исключающее ограничение. Оно говорит о том, что если я смотрю потенциальный результат, полученный для объектов, подвергнутых воздействию, то он одинаков для обеих IV-групп. Другими словами, инструментальная переменная не влияет на потенциальный результат, а это то же самое, что сказать, что инструментальная переменная влияет на переменную результата только через переменную воздействия. Применительно к нашему примеру попадание в число тех, кому пуш-уведомление должно быть отправлено, само по себе не влияет на вовлеченность, измеряемую покупками в приложении. Но оно влияет на вероятность получения пуш-уведомлений. А уже получение пуш-уведомления, в свою очередь, может влиять на вовлеченность. 3. E[T1i – T0i] ≠ 0. Это предположение о первом этапе. Речь идет о релевантности инструментальной переменной. Другими словами, ин-
 Локальный средний эффект воздействия (local average treatment effect – LATE)  157 струментальная переменная действительно влияет на воздействие. В нашем примере это означает, что попадание в число тех, кому пушуведомление должно быть отправлено, увеличивает вероятность получения пуш-уведомления. 4. T i1 > T i0. Это предположение о монотонности. Оно говорит о том, что для всех участников включение инструментальной переменной дает более высокий уровень воздействия, чем в ситуации, когда инструментальная переменная не была бы включена. Теперь давайте рассмотрим оценку Вальда, чтобы получить дополнительную информацию об инструментальной переменной: ATE Давайте возьмем первый член E[Y | Z = 1]. Используя предположение об исключающем ограничении, мы можем переписать Y с точки зрения потенциального результата следующим образом: E[Yi | Zi = 1] = E[Yi0 + T i1(Yi1 – Yi0) | Zi = 1]. Используя предположение о независимости, мы можем убрать обусловленность Z: E[Yi | Zi = 1] = E[Yi0 + T i1(Yi1 – Yi0)]. Используя аналогичные рассуждения, мы получаем, что E[Yi | Zi = 0] = E[Yi0 + T i1(Yi1 – Yi0)]. Теперь мы можем переписать числитель оценки Вальда следующим образом: E[Y | Z = 1] – E[Y | Z = 0] = E[Yi0 + T i1(Yi1 – Yi0)] – E[Yi0 + T i0(Yi1 – Yi0)] = = E[Yi0 + T i1Yi1 – T i1Yi0)] – E[Yi0 + T i0Yi1 – T i1Yi0)] = = E[T i1Yi1 – T i1Yi0)] – E[T i0Yi1 – T i0Yi0)] = E[Yi1T i1 – Yi0T i1 – Yi1T i0 + Yi0T i0)] = = E[Yi1T i1 – Yi1T i0 – Yi0T i1 + Yi0T i0)] = E[(Yi1 – Yi0)(T i1 – T i0)]. Используя предположение о монотонности, мы знаем, что T i1 – T i0 равно 0 или 1, поэтому E[(Yi1 – Yi0)(T i1 – T i0)] = E[(Yi1 – Yi0) | T i1 > T i0]P(T i1 > T i0). Используя аналогичную аргументацию для знаменателя, мы получаем, что E[T | Z = 1] – E[T | Z = 0] = E[T i1 – T i0] = P(T i1 > T i0). Все это позволяет нам посмотреть на оценку Вальда следующим образом:
    158  Несоблюдение требований и LATE То есть ATE, оцененный с помощью инструментальной переменной, представляет собой ATE для субпопуляции, в которой T i1 > T i0. Таким образом, речь идет о сегменте популяции, в которой включение инструментальной переменной для каждого участника дает более высокий уровень воздействия, чем в ситуации, когда инструментальная переменная не была бы включена. Другими словами, речь идет о субпопуляции «послушных исполнителей». Давайте просто напомним. 1. «Послушный исполнитель» (complier): если попал в рассылку пушуведомлений, получает пуш-уведомление, а если не попал в рассылку, не получает его T i1 – T i0 = T i1 > T i0 = 1 – 0 = 1. 2. «Всегда отказывающийся» (never-taker): независимо от того, попал или не попал в рассылку, НЕ получает пуш-уведомление T i1 – T i0 = T i1 = T i0 = 0 – 0 = 0. 3. «Всегда принимающий» (always-taker): независимо от того, попал или не попал в рассылку, получает пуш-уведомление T i1 – T i0 = T i1 = T i0 = 1 – 1 = 0. 4. «Бунтарь» (defier): если попал в рассылку, не получает пуш-уведомление, а если не попал в рассылку, то получает его. T i1 – T i0 = T i1 < T i0 = 0 – 1 = –1. Предпосылка о монотонности как раз предполагает, что «бунтарей» не существует. Вывод из всего этого состоит в том, что инструментальная переменная ничего не говорит о воздействии на «всегда отказывающихся», «всегда принимающих» и «бунтарей». «Бунтарей» согласно предпосылке о монотонности мы отсекаем. Для «всегда отказывающихся» у нас нет ни одного наблюдения, показывающего, что с ними будет, если они подвергнутся воздействию. Для «всегда принимающих» у нас нет ни одного наблюдения, показывающего, что с ними будет, если они не подвергнутся воздействию. Еще можно сказать, что для «всегда отказывающихся» и «всегда принимающих» воздействие остается неизменным! Инструментальная переменная находит эффект воздействия только для «послушных исполнителей». Влияние на вовлеченность Давайте проиллюстрируем все вышесказанное с помощью примера, в котором мы пытаемся оценить влияние пуш-уведомления на покупку в приложении. Причинно-следственный граф я уже приводил выше, поэтому повторять его здесь не буду. У нас есть данные как о присвоении пуш-уведомлений (рандомизированная инструментальная переменная push_assigned), так и о доставке push-уведомлений (переменная воздействия push_delivered).
Влияние на вовлеченность  159 data = pd.read_csv("data/app_engagement_push.csv") data.head() Сначала давайте запустим OLS и посмотрим, что она нам даст. ols = IV2SLS.from_formula( "in_app_purchase ~ 1 + push_assigned + push_delivered", data).fit() ols.summary.tables[1] OLS сообщает, что регрессионный коэффициент для переменной воздействия (переменной push_delivered) составляет 27.60 бразильского реала, то есть пуш-уведомление увеличивает покупки в приложении на 27.6 реала. Однако у нас есть основания полагать, что это смещенная оценка. Мы знаем, что у старых телефонов возникают проблемы с получением пуш-уведомления, поэтому, вероятно, более богатые клиенты с более современными телефонами являются «послушными исполнителями». Поскольку у тех, кто подвергся воздействию (получил пуш-уведомление), выше доход, мы считаем, что смещение является положительным, и реальный эффект получения пушуведомления будет ниже. Другими словами, у нас, вероятно, E[Y0 | T = 0] < E[Y0 | T = 1]. Теперь давайте попробуем оценить этот эффект с помощью инструментальных переменных. Для начала давайте обучим регрессию первого этапа. # обучаем регрессию первого этапа first_stage = IV2SLS.from_formula( "push_delivered ~ 1 + push_assigned", data).fit() first_stage.summary.tables[1]
160  Несоблюдение требований и LATE Похоже, у нас сильный первый этап (коэффициент первого этапа является значимым). Клиенты, которым мы решили отправить пуш-уведомления, получают их в 71.8 % случаев. Это означает, что у нас около 28 % «всегда отказывающихся». Кроме того, у нас также есть веские основания полагать, что не существует «всегда принимающих», поскольку оценка константы равна нулю. Это означает, что никто не получит пуш-уведомление, если оно ему не назначено. Учитывая план нашего эксперимента, это ожидаемо. Теперь давайте обучим короткую регрессию. # обучаем короткую регрессию reduced_form = IV2SLS.from_formula( "in_app_purchase ~ 1 + push_assigned", data).fit() reduced_form.summary.tables[1] Коэффициент короткой регрессии показывает, что причинно-следственный эффект назначения воздействия (переменной push_assigned) составляет 2.36. Это означает, что если включить кого-то в выборку рассылки pushуведомлений, покупка в приложении увеличится на 2.36 реала. Если разделить коэффициент короткой регрессии на коэффициент первого этапа, мы масштабируем эффект инструментальной переменной по переменной воздействия, получаем 2.3636 / 0.7176 = 3.29. Обучив двухэтапную регрессию наименьших квадратов (2SLS), мы получим ту же самую оценку 3.29, а еще бонус в виде правильно вычисленных стандартных ошибок. # обучаем двухэтапную регрессию наименьших квадратов iv = IV2SLS.from_formula( "in_app_purchase ~ 1 + [push_delivered ~ push_assigned]", data).fit() iv.summary.tables[1] Результат, полученный с помощью 2SLS, намного ниже результата, который мы получили с помощью OLS: 3.29 против 27.60. Это не лишено смысла, поскольку причинно-следственный эффект, оцененный с помощью OLS, имеет положительное смещение. Еще нам нужно помнить о LATE. 3.29 – это усредненное причинно-следственное влияние на «послушных исполнителей». К сожалению, мы ничего не можем сказать о «всегда отказывающихся».
Ключевые идеи  161 Это означает, что мы оцениваем влияние, оказываемое на представителей более богатой части населения, у которых есть более современные телефоны. Ключевые идеи В этой главе мы рассмотрели более современный взгляд на инструментальные переменные. Мы увидели, что инструментальные переменные можно рассматривать в виде причинно-следственной цепочки, в которой инструментальная переменная влияет на воздействие, а воздействие влияет на результат. Мы рассмотрели проблему несоблюдения требований, чтобы понять средний эффект воздействия (ATE), оценив инструментальные переменные, и выяснили, что он эквивалентен локальному среднему эффекту воздействия (LATE) для «послушных исполнителей».
Глава 10 Матчинг (сопоставление объектов тестовой и контрольной групп) Что же в конце концов делает регрессия? Как мы уже увидели, регрессия отлично справляется с контролем дополнительных переменных, когда мы сравниваем тестовую и контрольную группы. Если у нас выполняется независимость (Y0, Y1) ⊥ T | X, то регрессия сможет идентифицировать ATE, контролируя X. Способ, с помощью которого регрессия делает это, является своего рода волшебством. Чтобы получить некоторое представление о нем, давайте вспомним случай, когда все переменные X были дамми-переменными.
Что же в конце концов делает регрессия  163 В этом случае регрессия разбивала непрерывную переменную – количество лет, потраченных на образование, на группу дамми-переменных, можно сказать, на группу дамми-категорий и вычисляла среднюю разницу между тестовой и контрольной группами. При вычислении разницы средних значений значения X’ов остаются неизменными, поскольку мы вычисляем эту разницу для зафиксированной дамми-категории. Это как если бы мы получили E[Y | T = 1] – E[Y | T = 0] | X = x, где x является дамми-категорией. Затем регрессия объединяет оценки по каждой категории для вычисления окончательного значения ATE. К каждому наблюдению применяются веса, пропорциональные дисперсии воздействия в этой категории. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from matplotlib import style from matplotlib import pyplot as plt import statsmodels.formula.api as smf import graphviz as gr %matplotlib inline style.use("fivethirtyeight") В качестве примера предположим, что я пытаюсь оценить эффект препарата и у меня есть 6 мужчин и 4 женщины. Моя зависимая переменная – количество дней, проведенных в больнице, и я надеюсь, что мой препарат поможет снизить этот показатель. У мужчин истинный причинно-следственный эффект равен –3, поэтому препарат сокращает срок пребывания на 3 дня. У женщин он равен –2. Что еще интереснее, мужчины в гораздо большей степени подвержены этому заболеванию и дольше остаются в больнице. Кроме того, они получают препарат гораздо чаще. Только один из 6 мужчин не получает препарат. С другой стороны, женщины более устойчивы к этому заболеванию, поэтому реже остаются в больнице. 50 % женщин получают препарат. # сгенерируем данные drug_example = pd.DataFrame(dict( sex= ["M","M","M","M","M","M", "W","W","W","W"], drug=[1,1,1,1,1,0, 1,0,1,0], days=[5,5,5,5,5,8, 2,4,2,4] )) drug_example
  ­ 164  Матчинг (сопоставление объектов тестовой и контрольной групп) Обратите внимание, что простое сравнение тестовой и контрольной групп дает отрицательно смещенный эффект, то есть препарат кажется менее эффективным, чем он есть на самом деле. Это ожидаемо, поскольку мы опус тили спутывающий фактор – пол. В этом случае оцененное значение ATE меньше истинного, поскольку мужчины получают препарат чаще и больше подвержены болезни. # делаем сравнение тестовой и контрольной групп ( drug_example.query("drug==1")["days"].mean() drug_example.query("drug==0")["days"].mean() ) -1.1904761904761898 Поскольку истинный эффект для мужчин равен –3, а истинный эффект для женщин –2, оценка ATE должна быть равна Эта оценка вычисляется следующим образом: 1) разбиваем данные на категории спутывающего фактора – пола, в данном случае разбиваем данные на мужчин и женщин; 2) оцениваем эффект по каждой категории; 3) получаем средневзвешенное значение, вес представляет собой выборочный размер категории или ковариатной группы. Если бы у нас было одинаковое количество мужчин и женщин в данных, оценка ATE была бы средним значением ATE двух групп, –2.5.
­ ­ Что же в конце концов делает регрессия  165 Поскольку в нашем наборе данных больше мужчин, чем женщин, оценка ATE немного ближе к оценке ATE для мужчин. Она называется непарамет рической оценкой, поскольку она не делает никаких предположений о том, как были сгенерированы данные. Если мы проконтролируем пол с помощью регрессии, мы добавим предположение о линейности. Регрессия тоже разделит данные на мужчин и женщин и оценит эффекты для обеих групп. Пока все идет нормально. Однако когда дело доходит до объединения эффектов групп, регрессия не взвешивает их по размеру выборки. Вместо этого регрессия использует веса, пропорциональные дисперсии воздействия в каждой группе. В нашем случае дисперсия воздействия у мужчин меньше, чем у женщин, поскольку в конт рольной группе находится только один мужчина. Точнее, дисперсия T для мужчин равна 0.139 = 1/6 ∗ (1 – 1/6), а для женщин равна 0.25 = 2/4 ∗ (1 – 2/4). Таким образом, регрессия придаст женщинам более высокий вес в нашем примере, и оценка ATE будет немного ближе к оценке ATE для женщин, которая равна –2. # обучаем модель регрессии smf.ols('days ~ drug + C(sex)', data=drug_example).fit().summary().tables[1] С использованием дамми-переменных результат более интуитивно понятен, но, что странно, при оценке эффекта регрессия сохраняет неизменными непрерывные переменные. Кроме того, в случае непрерывных переменных на оценку ATE будут сильнее всего влиять ковариаты с наибольшей дисперсией. Итак, мы увидели, что регрессия имеет свои особенности. Она линейная, параметрическая, любит признаки с высокой дисперсией… Это может быть хорошо или плохо в зависимости от контекста. По этой причине важно знать о других методах, которые мы можем использовать для контроля спутывающих факторов. Речь здесь не только о том, что эти методы являются дополнительными инструментами в вашем арсенале причинно-следственных связей. Понимание различных способов борьбы со спутывающими факторами расширяет наше понимание проблемы. По этой причине я представляю вашему вниманию субклассификационную оценку!
166  Матчинг (сопоставление объектов тестовой и контрольной групп) Субклассификационная оценка Никто не просит Я объясняю все методы причинно-следственного анализа Если существует какой-то причинно-следственный эффект, который мы хотим оценить, например влияние профессионального обучения на заработок, и воздействие назначается неслучайным образом, нам нужно остерегаться спутывающих факторов. Возможно, профессиональное обучение проходят только более мотивированные люди, и они будут иметь более высокий заработок независимо от обучения. Нам необходимо оценить эффект обучения в небольших группах людей, которые примерно одинаковы по уровню мотивации и любым другим спутывающим факторам, которые могут у нас возникнуть. В более общем плане, если мы хотим оценить какой-то причинно-следственный эффект, но это сложно сделать из-за спутывания некоторых переменных X, нам нужно провести сравнения тестовой и контрольной групп небольшого размера, в которых переменные X принимают одно и то же значение. Если у нас есть условная независимость (Y0, Y1) ⊥ T | X, то мы можем записать ATE следующим образом: Этот интеграл проходит через все пространство распределения признаков Х и вычисляет разность средних для всех этих крошечных пространств и объединяет все в АТЕ. Еще один способ оценить ATE – подумать о дискретном наборе признаков. В этом случае можно сказать, что признаки X имеют K разных уровней {X1, X2, …, Xk } и мы вычисляем эффект воздействия для каждого уровня и объединяем их в ATE. В этом дискретном случае, преобразуя интеграл в сумму, мы можем получить субклассификационную оценку
Матчинг-оценка  167 где черточка представляет собой среднее значение зависимой переменной – для группы объектов, испытавших воздействие Yk1, и группы объектов, не ис– пытавших воздействия Yk0, в категории k, а Nk – это количество наблюдений в данной категории. Как видите, мы вычисляем локальную оценку ATE для каждой категории и объединяем эти оценки, вычисляя средневзвешенное значение, весом здесь будет размер категории в выборке. В нашем вышеприведенном примере с препаратом это будет первая оценка, которая даст нам –2.6. # вычислим субклассификационную оценку ATE = ( ( drug_example.query("sex=='M' & drug==1")["days"].mean() drug_example.query("sex=='M' & drug==0")["days"].mean() ) * drug_example['sex'].value_counts(normalize=True)[0] + ( drug_example.query("sex=='W' & drug==1")["days"].mean() drug_example.query("sex=='W' & drug==0")["days"].mean() ) * drug_example['sex'].value_counts(normalize=True)[1] ) -2.5999999999999996 Матчинг-оценка Субклассификационная оценка редко используется на практике (вскоре мы увидим, что редкость ее использования обусловлена проклятием размерности), но она дает нам хорошее представление о том, что должна делать модель причинно-следственных связей, как она должна контролировать спутывающие факторы. Это позволяет нам исследовать другие виды оценок, например матчингоценку. Идея аналогична вышеприведенным. Поскольку какой-то спутывающий фактор X делает так, что объекты, подвергнутые воздействию, и объекты, не подвергнутые воздействию, изначально
­ 168  Матчинг (сопоставление объектов тестовой и контрольной групп) несопоставимы, я могу сделать их сравнимыми, сопоставив каждого объекта из тестовой группы (группы объектов, подвергнутых воздействию) с аналогичным объектом из контрольной группы (группы объектов, не подвергнутых воздействию). Это как если бы я для каждого участника тестовой группы подыскивал близнеца в контрольной группе. С помощью таких сравнений тестовая и контрольная группы снова становятся сопоставимыми. В качестве примера давайте предположим, что мы пытаемся оценить влияние профессиональной подготовки на заработок. Вот как выглядят сотрудники, прошедшие профессиональную подготовку. trainee = pd.read_csv("data/trainees.csv") trainee.query("trainees==1") А вот и те, кто не прошел профессиональную подготовку. trainee.query("trainees==0")
Матчинг-оценка  169 Если я выполню простое сравнение средних, то получу, что сотрудники, прошедшие профессиональную подготовку, зарабатывают меньше денег, чем те, кто не прошел подготовку. # простое сравнение средних ( trainee.query("trainees==1")["earnings"].mean() trainee.query("trainees==0")["earnings"].mean() ) -4297.49373433584 Однако если мы посмотрим на таблицу выше, мы заметим, что сотрудники, прошедшие профессиональную подготовку, намного моложе, чем сотрудники, не прошедшие профессиональную подготовку, это указывает на то, что возраст, вероятно, является фактором, влияющим на результат. Давайте воспользуемся сопоставлением по возрасту, чтобы попытаться исправить это. Мы возьмем объект 1 в тестовой группе (группе сотрудников, которые прошли профессиональную подготовку) и сопоставим его с объектом 27 в контрольной группе (группе сотрудников, которые не прошли профессиональную подготовку), поскольку им обоим по 28 лет. Объект 2 мы сопоставим
170  Матчинг (сопоставление объектов тестовой и контрольной групп) с объектом 34, объект 3 – с объектом 37, объект 4 – с объектом 35… Когда дело доходит до объекта 5, нам нужно найти кого-то в возрасте 29 лет из контрольной группы, под это требование подходит объект 37, но у него уже подобрана пара. Это не является проблемой, поскольку мы можем использовать один и тот же объект несколько раз. Если несколько объектов в группе совпадают по своим характеристикам (например, имеют один и тот же возраст), мы можем случайным образом отобрать один из них. Вот так будет выглядеть набор данных с сопоставленными объектами. # создаем набор, в котором у объектов # не будет одинакового возраста unique_on_age = (trainee .query("trainees==0") .drop_duplicates("age")) matches = ( trainee .query("trainees==1") .merge(unique_on_age, on="age", how="left", suffixes=("_t_1", "_t_0")) .assign(t1_minus_t0=lambda d: d["earnings_t_1"] - d["earnings_t_0"])) matches
Матчинг-оценка  171 Обратите внимание, что в последнем столбце показана разница зарплат сотрудника, прошедшего профессиональную подготовку (объекта из тестовой группы), и сотрудника, не прошедшего профессиональную подготовку (объекта из контрольной группы). Если мы возьмем среднее значение по последнему столбцу, мы получим оценку ATET с учетом возраста. Обратите внимание, что оценка теперь представляет собой очень большое положительное значение по сравнению с предыдущей оценкой, когда мы использовали простую разницу средних значений. # смотрим среднюю разность между # группами с учетом возраста matches["t1_minus_t0"].mean() Но это был очень притянутый за уши пример, чтобы просто познакомить вас с матчингом (сопоставлением). В реальности у нас обычно больше одного признака, и объекты не совпадают идеально. В этом случае нам нужно задать некоторую меру близости, чтобы сравнить, насколько объекты похожи друг на друга. Одной из распространенных мер близости является евклидова норма || Xi – Xj ||. Однако эта разность зависит от масштаба характеристик объектов. Это означает, что при вычислении евклидовой нормы возраст, который измеряется в десятках, будет гораздо менее важной характеристикой по сравнению с доходом, измеряемым в тысячах и десятках тысяч. По этой причине, прежде чем применять норму, нам необходимо масштабировать (стандартизировать) характеристики объектов так, чтобы они находились примерно в одном и том же масштабе. Задав меру расстояния, мы теперь для нашего объекта, нуждающегося в сопоставлении, ищем ближайшего к нему соседа. В математических терминах мы можем записать матчинг-оценку следующим образом: где Yjm(i ) – это объект из альтернативной группы, который наиболее похож на Yi . Операцию сопоставления выполняем двумя способами: сравниваем тестовую группу с контрольной и контрольную группу с тестовой. Чтобы попробовать эту оценку, давайте рассмотрим медицинский пример. Мы снова хотим выяснить влияние лекарства на количество дней, проведенных в больнице (количество дней до выздоровления). К сожалению, этот эффект зависит от тяжести заболевания, пола и возраста. У нас есть основания полагать, что пациенты с более тяжелым течением имеют более высокие шансы получить лекарство. med = pd.read_csv("data/medicine_impact_recovery.csv") med.head()
172  Матчинг (сопоставление объектов тестовой и контрольной групп) Если посмотреть на простую разницу средних, E[Y | T = 1] – E[Y | T = 0], получается, что у участников, принимающих лекарство, на выздоровление уходит в среднем на 16.9 дня больше, чем у тех, кто не принимал лекарство. Вероятно, это связано со спутывающими факторами, поскольку мы не ожидаем, что лекарство причинит вред пациенту. # вычисляем простую разницу средних ( med.query("medication==1")["recovery"].mean() med.query("medication==0")["recovery"].mean() ) 16.895799546498726 Чтобы исправить это смещение, мы будем контролировать переменные X, используя сопоставление. Во-первых, нам нужно не забыть отмасштабировать (стандартизировать) наши признаки, иначе при вычислении расстояний между точками такие характеристики, как возраст, будут иметь более важное значение, чем тяжесть заболевания. # выполняем стандартизацию X = ["severity", "age", "sex"] y = "recovery" med = med.assign(**{f: (med[f] - med[f].mean()) / med[f].std() for f in X}) med.head() Теперь о самом сопоставлении. Вместо написания функции сопоставления мы воспользуемся алгоритмом K ближайших соседей от библиотеки sklearn. Этот алгоритм делает прогнозы, находя ближайшую точку данных в обучающем наборе.
­ Матчинг-оценка  173 Для сопоставления нам нужно создать два обучающих набора. В первом наборе под названием untreated мы будем хранить объекты – точки из конт рольной группы. Во втором наборе под названием treated мы будет хранить объекты – точки из тестовой группы. Обучаем модели KNN на этих наборах, по сути, запоминаем наши объекты. Теперь мы можем воспользоваться этими моделями KNN для прогнозирования, прогнозы и будут нашими сопоставлениями. from sklearn.neighbors import KNeighborsRegressor # создаем тестовую и контрольную группы treated = med.query("medication==1") untreated = med.query("medication==0") # обучаем модель KNN на объектах контрольной группы mt0 = KNeighborsRegressor(n_neighbors=1).fit( untreated[X], untreated[y] ) # обучаем модель KNN на объектах тестовой группы mt1 = KNeighborsRegressor(n_neighbors=1).fit( treated[X], treated[y] ) predicted = pd.concat([ # находим совпадения с объектами тестовой группы, # используя для поиска модель KNN, обученную # на объектах контрольной группы treated.assign(match=mt0.predict(treated[X])), # находим совпадения с объектами контрольной группы, # используя для поиска модель KNN, обученную # на объектах тестовой группы untreated.assign(match=mt1.predict(untreated[X])) ]) predicted.head() Выполнив сопоставление, мы можем получить матчинг-оценку ATE с помощью формулы .
­ ­ 174  Матчинг (сопоставление объектов тестовой и контрольной групп) # вычисляем матчинг-оценку ATE ( np.mean((2 * predicted["medication"] - 1) * (predicted["recovery"] - predicted["match"])) ) -0.9954 Используя такое сопоставление, мы видим, что эффект лекарства больше не является положительным. Это означает, что с учетом переменных Х лекарство сокращает время, проведенное в больнице, в среднем примерно на 1 день. Это уже огромное улучшение по сравнению со смещенной оценкой, которая предсказывала увеличение времени пребывания в больнице на 16.9 дня. Однако мы по-прежнему можем повысить надежность оценки. Смещенность матчинг-оценки Оказывается, матчинг-оценка, которую мы вычислили выше, является смещенной. Чтобы убедиться в этом, давайте рассмотрим оценку ATET вместо ATE просто потому, что его проще посчитать. Ход рассуждений применим и к ATE: где N1 – это количество объектов тестовой группы и Yj (i ) – это объект j из контрольной группы для объекта i из тестовой группы. Для проверки наличия смещения мы надеемся, что сможем применить центральную предельную теорему, чтобы написанное ниже сходилось к нормальному распределению со средним значением, равным нулю. N1(AT̂ET – ATET ). Однако это происходит не всегда. Если мы определим среднее значение зависимой переменной для объекта контрольной группы при данном X, μ0(x) = E[Y | X = x, T = 0], мы получим следующее уравнение (кстати, я опустил доказательство, потому что оно здесь немного выходит за рамки книги): E [ N1(AT̂ET – ATET )] = E [ N1(μ0(Xi) – μ0(Xj(i )))]. Понять смысл разности μ0(Xi) – μ0(Xj(i )) не так-то просто, поэтому давайте посмотрим на это уравнение повнимательнее. μ0(Xi) – это значение зависимой переменной для объекта i, подвергнутого воздействию, если бы он не подвергался воздействию. Проще говоря, речь идет о значении зависимой переменной для объекта i из тестовой группы, как если бы он попал в конт
Смещенность матчинг-оценки  175 рольную группу. Таким образом, это контрфактический результат Y0 для объекта i. μ0(Xj(i )) – это значение зависимой переменной для объекта j, не подвергнутого воздействию, который совпал с объектом i, подвергнутым воздействию. Опять же, проще говоря, речь о значении зависимой переменной для объекта j из контрольной группы, который совпал с объектом i из тестовой группы. Таким образом, это тоже Y0, но теперь уже для объекта j. Только на этот раз перед нами – фактический результат, потому что j находится в контрольной группе (группе объектов, не подвергнутых воздействию). Теперь, поскольку j и i похожи, но не одинаковы, математическое ожидание, скорее всего, не будет равно нулю. Другими словами, Xi ≈ Xj. Таким образом, Y0i ≈ Y0j. По мере увеличения размера выборки будет больше объектов для сопоставления, поэтому разница между объектом i и совпадающим с ним объектом j также будет уменьшаться. Но эта разница медленно стремится к нулю. Как результат E [ N1(μ0(Xi) – μ0(Xj(i )))] не может сойтись к нулю, поскольку N1 растет быстрее, чем уменьшается (μ0(Xi) – μ0(Xj(i ))). Смещение возникает, когда расхождения в сопоставлении становятся огромными. К счастью, мы знаем, как это исправить. Каждое наблюдение вносит свой вклад (μ0(Xi) – μ0(Xj(i ))) в смещение, поэтому все, что нам нужно сделать, – вычесть эту величину из каждого совпавшего сравнения при вычислении матчинг-оценки. С этой целью мы можем заменить μ0(Xj(i )) некоторой оценкой этой величины μ̂0(Xj (i )), которую можно получить с помощью таких моделей, как линейная регрессия. Оценка ATET получит теперь немного другую формулу μ̂0(x ) – это некоторая оценка E [Y | X, T = 0] типа линейной регрессии, которая обучена на выборке, состоящей из объектов контрольной группы. from sklearn.linear_model import LinearRegression # обучаем модель линейной регрессии, чтобы оценить mu_0(x) ols0 = LinearRegression().fit(untreated[X], untreated[y]) ols1 = LinearRegression().fit(treated[X], treated[y]) # находим объекты, которые совпадают # с объектами тестовой группы treated_match_index = mt0.kneighbors( treated[X], n_neighbors=1)[1].ravel() # находим объекты, которые совпадают # с объектами контрольной группы untreated_match_index = mt1.kneighbors( untreated[X], n_neighbors=1)[1].ravel() predicted = pd.concat([ (treated
176  Матчинг (сопоставление объектов тестовой и контрольной групп) # находим совпадение Y в другой группе .assign(match=mt0.predict(treated[X])) # вводим член корректировки смещения .assign(bias_correct=ols0.predict(treated[X]) ols0.predict(untreated.iloc[treated_match_index][X]))), (untreated .assign(match=mt1.predict(untreated[X])) .assign(bias_correct=ols1.predict(untreated[X]) ols1.predict(treated.iloc[untreated_match_index][X]))) ]) predicted.head() Сразу возникает вопрос: не противоречит ли это принципу сопоставления? Если мне все равно придется обучить линейную регрессию, почему бы мне не использовать только ее вместо этой сложной модели? Это справедливое замечание, поэтому мне потребуется некоторое время, чтобы ответить на него. Куда бы я ни пошел… …я вижу его лицо
­ Смещенность матчинг-оценки  177 Прежде всего результаты линейной регрессии, которую мы обучаем, нельзя экстраполировать на воздействие, чтобы оценить эффект воздействия. Цель линейной регрессии – просто исправить смещение. Линейная регрессия здесь является локальной в том смысле, что она не пытается увидеть, каким был бы объект из тестовой выборки, если бы он выглядел как объект из контрольной выборки. Она не выполняет никакой экстраполяции. Это уже относится к процедуре сопоставления. В основе матчинг-оценки попрежнему лежит процедура сопоставления. Я хочу подчеркнуть здесь, что в нашей матчинг-оценке регрессия является здесь вторичной по отношению к процедуре сопоставления. Второй момент заключается в том, что сопоставление – это непарамет рическая оценка. Оно не предполагает линейности или какой-либо параметрической модели. По существу, процедура сопоставления более гибкая, чем линейная регрессия, и может работать в ситуациях, где линейная регрессия не работает, а именно в тех, где нелинейность является сильно выраженной. Означает ли это, что вам следует использовать только сопоставление? Ну, это сложный вопрос. Альберто Абади утверждает, что да, вам следует это сделать. Процедура сопоставления является более гибкой, и если у вас есть программный код, его остается только запустить. Я в этом совете не совсем уверен. Например, Абади потратил много времени на изучение и разработку матчинг-оценки (да, он один из ученых, которые вносят вклад в теорию матчинга), поэтому он, очевидно, лично заинтересован в этом методе. Вовторых, в простоте линейной регрессии есть что-то такое, чего вы не увидите в сопоставлении. Математику частных производных «когда все остальные признаки оказываются фиксированными» гораздо легче понять с помощью линейной регрессии, чем с помощью сопоставления. Но это всего лишь мое мнение. Честно говоря, однозначного ответа на этот вопрос нет. В любом случае, вернемся к нашему примеру. Используя формулу коррекции смещения, я получаю следующую оценку ATE. # скорректированная c помощью линейной # регрессии матчинг-оценка ATE np.mean( (2 * predicted["medication"] - 1) * ((predicted["recovery"] - predicted["match"]) predicted["bias_correct"]) ) -7.36266090614141 Конечно, нам еще нужно вычислить доверительный интервал для нашей оценки, но хватит математической теории. На практике мы можем просто воспользоваться чужим кодом и просто импортировать соответствующую модель. Ниже приведен пример использования такой модели из библиотеки causalinference https://github.com/laurencium/causalinference.
178  Матчинг (сопоставление объектов тестовой и контрольной групп) from causalinference import CausalModel cm = CausalModel( Y=med["recovery"].values, D=med["medication"].values, X=med[["severity", "age", "sex"]].values ) cm.est_via_matching(matches=1, bias_adj=True) print(cm.estimates) Наконец, можно с уверенностью сказать, что наш препарат действительно сокращает время пребывания человека в больнице. Оценка ATE лишь немного ниже моей из-за того, что в реализации knn от sklearn и библиотеке causalinference по-разному решается проблема наличия совпадающих объектов в группе. Прежде чем мы закроем эту тему, я просто хотел немного подробнее остановиться на причине смещения, возникающего при сопоставлении. Мы видели, что сопоставление является смещенным, когда объект и соответствующий ему объект в другой группе не очень похожи. Но чем продиктованы эти различия? Проклятие размерности Как оказалось, ответ довольно прост и интуитивно понятен. Легко найти людей, которые совпадают по нескольким характеристикам, например по полу. Но если мы добавим больше характеристик, таких как возраст, доход, город рождения и т. д., находить совпадения станет все труднее и труднее. Говоря более широко, чем больше у нас признаков, тем выше будет расстояние между объектом и его «эквивалентом» в другой группе. И это вредит не только матчинг-оценке. С субклассификационной оценкой, рассмотренной ранее, те же проблемы. В том выдуманном примере с лекарством, где были лишь мужчины и женщины, было довольно легко получить субклассификационную оценку. Это потому, что у нас было только две категории: мужской пол и женский пол. Но что бы произошло, если бы у нас было больше признаков? Допустим, у нас есть два непрерывных признака, таких как возраст и доход, и нам удалось разбить каждый на 5 катего-
­ Проклятие размерности  179 рий. Это даст нам 25 категорий, или 25. А что, если бы у нас было 10 ковариат по 3 категории в каждой? Кажется, это не так уж и много, правда? Что ж, это даст нам 59 049 категорий, или 310. Легко понять, как наша процедура сопоставления может очень быстро выйти из-под контроля. Это явление, широко распространенное во всей науке о данных, называется «проклятие размерности»!!! Источник изображения: https://deepai.org/machine-learning-glossary-and-terms/curse-of-dimensionality Несмотря на свое пугающее и претенциозное название, проклятие размерности означает лишь то, что количество точек данных, необходимых для заполнения пространства признаков, растет экспоненциально с увеличением количества признаков или размерностей. Таким образом, если для заполнения пространства из трех признаков требуется X точек данных, то для заполнения пространства из четырех признаков потребуется экспоненциально большее количество точек. В контексте субклассификационной оценки проклятие размерности означает, что она пострадает, если у нас будет много признаков. Множество признаков подразумевают наличие нескольких категорий в X. Если категорий несколько, в некоторых из них может быть очень мало данных. Некоторые из них могут быть только в тесте или контроле, поэтому для них невозможно будет оценить ATE, и это повредит нашей оценке. В контексте сопоставления это означает, что пространство признаков будет очень разреженным, а объекты будут находиться очень далеко друг от друга. Это увеличит расстояние между объектами и их эквивалентами и вызовет смещение. Что касается линейной регрессии, то она неплохо справляется с этой проб лемой. Она проецирует все характеристики X в одну размерность Y. Затем на основе этой проекции проводится сравнение теста и контроля. Таким образом, линейная регрессия выполняет своего рода уменьшение размерности для оценки ATE. Это выглядит довольно элегантно. Большинство моделей причинно-следственных связей также каким-то образом справляются с проклятием размерности. Я не буду повторяться, но вам следует иметь это в виду, анализируя их. Например, когда мы столкнемся с оценкой склонности (propensity score) в следующей главе, попытайтесь разобраться, как эти модели решают проблему проклятия размерности.
­ 180  Матчинг (сопоставление объектов тестовой и контрольной групп) Ключевые идеи Мы начали эту главу с объяснения работы линейной регрессии и способов, с помощью которых линейная регрессия помогает нам выявить причинноследственные связи. А именно мы поняли, что регрессию можно рассмат ривать как разбиение набора данных на группы (категории), вычисление оценки ATE в каждой группе и потом объединение оценок ATE, вычисленных в группах, в единую оценку ATE для всего набора данных. Затем мы получили очень общую причинно-следственную модель на основе субклассификации. Мы увидели, что эта модель не очень полезна на практике, но она дала нам некоторые интересные идеи по поводу того, как решить проблему оценки причинного-следственного эффекта. Это дало нам возможность поговорить о матчинг-оценке (оценке на основе сопоставления). Сопоставление контрольных переменных для устранения спутывающих факторов осуществляется следующим образом: мы смотрим каждый объект тестовой группы и ищем схожий с ним объект в контрольной группе, затем смотрим каждый объект контрольной группы и ищем схожий с ним объект в тестовой группе. Мы увидели, как реализовать этот метод с использованием алгоритма KNN, а также как снизить его смещение с помощью линейной регрессии. Наконец, мы обсудили разницу между процедурой сопоставления и линейной регрессией. Мы увидели, что сопоставление – это непараметрическая оценка, которая не опирается на линейность, как это делает линейная регрессия. Наконец, мы углубились в проблему высокоразмерных наборов данных и увидели, как от этого могут пострадать методы анализа причинно-следственных связей.
Глава 11 Оценка склонности (Propensity Score) Психология роста Позитивная психология задается вопросом, какое поведение человека ведет к счастливой жизни. Мы можем представить ее как область пересечения книг по самопомощи и статистики с ее академической строгостью. Одним из известных открытий позитивной психологии является мышление с установкой на рост (Growth Mindset). Идея состоит в том, что у людей может быть два типа ментальной модели: фиксированное мышление (fixed mindset) и мышление с установкой на рост (growth mindset). Люди с фиксированным мышлением полагают, что способности даны с рождения или в раннем детстве. Таким образом, интеллект зафиксирован и не может меняться на протяжении всей жизни. Если у вас еще нет требуемых навыков, вам их не приобрести. Следствием этой мысли является то, что вам не следует тратить время на области, в которых вы не преуспеваете, поскольку вы никогда их не освоите. С другой стороны, если у вас – мышление с установкой на рост, вы верите, что интеллект можно развивать. Прямым следствием этого является то, что вы воспринимаете неудачу не как недостаток упорства, а как часть процесса обучения. Я не хочу пускаться в рассуждения, какой из этих подходов является правильным (хотя он, вероятно, находится где-то посередине). Для нашей задачи это не имеет большого значения. Что действительно важно, так это то, что психологи обнаружили: люди с установкой на рост, как правило, добиваются большего в жизни. У них больше шансов достичь желаемого. Несмотря на то что мы хорошо разбираемся в причинно-следственных выводах, мы научились относиться к этим утверждениям со скептицизмом. Неужели мышление с установкой на рост заставляет людей достигать большего? Или это просто тот случай, когда люди, добивающиеся большего, склонны
    ­ 182  Оценка склонности (Propensity Score) развивать мышление с установкой на рост в результате своего успеха? Что появилось раньше: курица или яйцо? Что касается обозначения потенциального результата, у нас есть основания полагать, что в этих утверждениях есть смещение. Y0 | T = 1, вероятно, больше, чем Y0 | T = 0. Это означает, что люди, обладающие мышлением с установкой на рост, добились бы большего, даже если бы у них было фиксированное мышление. Чтобы уладить ситуацию, исследователи разработали «National Study of Learning Mindsets» («Национальное исследование ментальных установок»): https://studentexperiencenetwork.org/national-mindset-study/. Это рандомизированное исследование, проведенное в государственных средних школах США (9–12 классы), цель которого заключалась в том, чтобы выявить влияние мышления с установкой на рост. Эксперимент был организован следующим образом: учащиеся проходят в школе семинар, призванный привить им мышление с установкой на рост. Затем организаторы наблюдают за учащимися, чтобы оценить их успеваемость. Измерения складываются в балл достижений и стандартизированы. Реальные данные об этом исследовании являются закрытыми в целях сохранения конфиденциальности учащихся. Однако у нас есть синтетический набор данных с такими же статистическими свойствами, который использовали в своей работе Athey and Wager https:// arxiv.org/pdf/1902.07409.pdf, поэтому мы воспользуемся им. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from matplotlib import style from matplotlib import pyplot as plt import seaborn as sns import statsmodels.formula.api as smf from causalinference import CausalModel import graphviz as gr %matplotlib inline style.use("fivethirtyeight") pd.set_option("display.max_columns", 6) Помимо переменных воздействия и результата, в исследовании также бы ли использованы некоторые другие переменные:  schoolid: идентификатор школы учащегося;  success_expect: самооценка с точки зрения ожиданий успеха в будущем, прокси-показатель предыдущих достижений, измеренный до случайного назначения;  ethnicity: категориальная переменная для расы / этнической принадлежности учащихся;  gender: категориальная переменная для пола учащегося;
       Психология роста  183  frst_in_family: категориальная переменная для обозначения статуса студента первого поколения, т. е. первого в семье, поступившего в колледж;  school_urbanicity: категориальная переменная для обозначения типа школы;  school_mindset: среднее значение фиксированного мышления учащихся на уровне школы, указанное до случайного назначения, стандартизировано;  school_achievement: уровень успеваемости в школе, измеряемый по результатам тестов и подготовки к поступлению в колледж, стандартизировано;  school_ethnic_minority: состав по расовым/этническим меньшинствам, т. е. процентная доля чернокожих, латиноамериканцев или коренных американцев, стандартизировано;  school_poverty: процент учащихся из семей, доходы которых находятся ниже федеральной черты бедности, стандартизировано;  school_size: стандартизированное общее количество учащихся во всех четырех классах школы. data = pd.read_csv("data/learning_mindset.csv") data.sample(10, random_state=5)
184  Оценка склонности (Propensity Score) Хотя исследование было рандомизированным, похоже, что данные содержат спутывающие факторы. Если мы посмотрим на дополнительные характеристики, мы заметим, что они систематически меняются в зависимости от теста и контроля. Одна из возможных причин этой изменчивости заключается в том, что переменная воздействия сводится к посещению студентом семинара. Таким образом, хотя возможность участия была случайной, само участие таковым не является. Здесь мы имеем дело с несоблюдением требований. Одним из свидетельств этого является то, как ожидания успеха у учащегося коррелируют с участием в семинаре. Учащиеся с более высокими самооценками успешности с большей вероятностью присоединятся к семинару «Мышление с установкой на рост». # посмотрим среднее значение воздействия по самооценкам успешности data.groupby("success_expect")["intervention"].mean() success_expect 1 0.271739 2 0.265957 3 0.294118 4 0.271617 5 0.311070 6 0.354287 7 0.362319 Name: intervention, dtype: float64 Давайте посмотрим, чему равна разность средних E[Y | T = 1] – E[Y | T = 0]. Это будет полезная информация для сравнения. smf.ols("achievement_score ~ intervention", data=data).fit().summary().tables[1] Просто сравнивая тех, кто испытал воздействие, и тех, кто не испытал воздействия, мы видим, что у объектов из тестовой группы оценка успеваемости (achievement_score) в среднем на 0.3185 (0.4723 – 0.1538) выше средней оценки (которая равна нулю, поскольку оценка стандартизирована). Но большое это значение или маленькое? Я знаю, что интерпретация стандартизированных результатов может быть сложной задачей, однако не торопитесь. Я думаю, что через это стоит пройти, потому что вы еще не раз столкнетесь со стандартизированными оценками. Стандартизация переменной результата означает, что она измеряется в стандартных отклонениях. Таким образом, у объектов тестовой группы оценка успеваемости на 0.3185 отклонения выше, чем у объектов контроль-
Психология роста  185 ной группы. Вот что это значит. Что касается того, много это или мало, давайте вспомним кое-что о нормальном распределении. Мы знаем, что 95 % его массы находится в пределах 2 стандартных отклонений, 2.5 % приходится на левый хвост и еще 2.5 % – на правый хвост. Это означает, что если ваш рост на 2 стандартных отклонения превышает средний рост, 97.5 % (95 % плюс 2.5 % из левого хвоста) всех людей будут ниже вас. Глядя на функцию нормального распределения, мы также знаем, что около 85 % его массы находится ниже 1 стандартного отклонения, а 70 % его массы – ниже 0.5 стандартного отклонения. Поскольку у объектов тестовой группы средняя стандартизированная оценка успеваемости находится в районе 0.5, это означает, что с точки зрения индивидуальных достижений они опережают 70 % людей. Или, другими словами, они входят в число 30 % лучших, которые добиваются бóльших успехов. Вот как это выглядит на картинке. plt.hist(data["achievement_score"], bins=20, alpha=0.3, label="Все") plt.hist(data.query("intervention==0")["achievement_score"], bins=20, alpha=0.3, color="C2") plt.hist(data.query("intervention==1")["achievement_score"], bins=20, alpha=0.3, color="C3") plt.vlines(-0.1538, 0, 300, label="Контрольная группа", color="C2") plt.vlines(-0.1538+0.4723, 0, 300, label="Тестовая группа", color="C3") plt.legend(); Разумеется, мы по-прежнему считаем этот результат смещенным. Разница между тестовой и контрольной группами, вероятно, меньше, поскольку мы считаем, что смещение является положительным. Мы уже видели, что на семинар охотнее идут более амбициозные люди, поэтому, возможно, они добились бы большего, даже если бы не посетили его. Для контроля этого смещения мы могли бы воспользоваться регрессией или матчингом, но пришло время изучить новую технику.
186  Оценка склонности (Propensity Score) Оценка склонности Оценка склонности (propensity score) возникает в результате осознания того, что вам не нужно напрямую контролировать спутывающие факторы X для достижения условной независимости (Y1, Y0) ⊥ T | X. Вместо этого достаточно контролировать балансирующую оценку E[T | X]. Эта балансирующая оценка часто является условной вероятностью воздействия P[T | X], также называемой оценкой склонности e(x). Оценка склонности позволяет вам не ставить условием совокупность переменных X, чтобы достичь независимости потенциальных результатов воздействия. Достаточно сделать условием эту единственную переменную, которая является оценкой склонности: (Y1, Y0) ⊥ T | e(x). Существует формальное доказательство того, почему это именно так, но мы можем пока забыть о нем и подойти к вопросу более интуитивно. Оценка склонности – это условная вероятность получения воздействия, верно? Таким образом, мы можем представить эту оценку как своего рода функцию, которая преобразует X в воздействие T. Оценка склонности представляет собой золотую середину между переменной X и воздействием T. Если мы покажем ее на причинно-следственном графе, то вот как она будет выглядеть. g = gr.Digraph() g.edge("T", "Y") g.edge("X", "Y") g.edge("X", "e(x)") g.edge("e(x)", "T") g X e(x) T Y
­ Оценка склонности  187 Если я знаю, что такое e(x), то сам по себе X не поможет мне узнать, каким будет T. Это означает, что контроль e(x) эквивалентен непосредственному контролю X. Подумаем об этом в терминах нашего семинара про мышление. Объекты, подвергнутые воздействию, и объекты, не подвергнутые воздействию, изначально несопоставимы, поскольку более амбициозные люди с большей вероятностью примут воздействие (более охотно пойдут на семинар) и добьются большего в жизни. Однако если я возьму двух человек, одного из тестовой группы и одного из контрольной группы, но с одинаковой вероятностью подвергнуться воздействию, они будут сопоставимы. Если у них одинаковая вероятность подвергнуться воздействию, единственная причина, по которой один из них испытал воздействие, а другой нет, – это чистая случайность. Фиксированность оценки склонности приводит к тому, что данные выглядят как случайные. Теперь, когда у нас есть интуитивно понятное объяснение, давайте посмот рим на математическое доказательство. Мы хотим показать, что (Y1, Y0) ⊥ T | e(x) эквивалентно E[T | e(x), X] = E[T | e(x)]. Получается, что как только я ставлю условием e(x), X не может дать мне никакой дополнительной информации о T. Доказательство вышесказанного выглядит довольно странно. Мы покажем, что вышеприведенное уравнение верно, превратив его в тривиальное утверждение. Сначала взгляните на левую часть E[T | e(x), X]: E[T | e(x), X] = E[T | X] = e(x). Мы используем тот факт, что e(x) – это просто функция от X, поэтому ее обусловливание не даст нам никакой дополнительной информации, после того как мы обусловим сам X. Затем мы используем определение оценки склонности E[T | X]. В правой части воспользуемся законом повторных ожиданий E[X] = E[E[X | Y ]]. Ожидаемое значение условного ожидаемого значения X при заданном Y такое же, как ожидаемое значение X. E[T | e(x)] = E[E[T | e(x), X] | e(x) = E[e(x) | e(x)] = e(x). E[T | e(x)] = e(x) вытекает из закона повторных ожиданий. E[T | e(x), X] = e(x) вытекает из того, что мы выяснили, когда рассматривали левую часть уравнения E [T | e(x), X] = E [T | e(x)]. Поскольку левая и правая части уравнения E[T | e(x), X] = E[T | e(x)] равны e(x), это уравнение тривиально верно.
188  Оценка склонности (Propensity Score) Взвешивание по склонности Идеально сбалансировано… …как и должно быть Хорошо, мы получили оценку склонности. Что теперь? Как я уже сказал, все, что нам нужно сделать, – это поставить ее условием. Например, мы могли бы обучить линейную регрессию, поставив условием оценку склонности, а не все X’ы. А пока давайте рассмотрим метод, который использует только оценку склонности и ничего больше. Идея состоит в том, чтобы записать условную разницу средних, используя оценку склонности: Мы могли бы еще упростить это выражение, но давайте посмотрим на него в приведенном виде, потому что оно хорошо иллюстрирует, что именно делает оценка склонности. Первый член оценивает Y1. Он берет все объекты тестовой группы и масштабирует их по обратной вероятности воздействия. Это приводит к тому, что люди с очень низкой вероятностью воздействия получают высокий вес. Это логично, не так ли? Если у кого-то низкая вероятность подвергнуться воздействию, этот человек выглядит как не подвергавшийся воздействию. Однако этот человек подвергался воздействию. Это уже интересно. У нас есть объект из тестовой группы, который выглядит как объект из контрольной группы, поэтому мы присвоим этому объекту высокий вес. Мы получаем выборку того же размера, что и исходная, но в которой все объекты взвешены. По тем же соображениям другой член рассматривает объекты контрольной группы и придает большое значение объектам, которые
Взвешивание по склонности  189 выглядят как объекты из тестовой группы (т. е. имеют высокую вероятность воздействия). Эта оценка называется взвешиванием по обратной вероятности воздействия (Inverse Probability of Treatment Weighting – IPTW), поскольку она масштабирует каждый объект по обратной вероятности воздействия. На картинке показано, что делает взвешивание. На верхнем левом графике показаны исходные данные. Синие точки – объекты контрольной группы (объекты, не подвергнутые воздействию), красные – объекты тестовой группы (объекты, подвергнутые воздействию). Нижний график показывает оценку склонности e(x). Обратите внимание, что она находится в диапазоне между 0 и 1 и растет по мере увеличения X. Наконец, верхний правый график показывает данные после взвешивания. Обратите внимание, что красные точки (объекты тестовой группы), расположенные левее (с более низкой оценкой склонности), имеют более высокий вес. Аналогично синие точки правее (с более высокой оценкой склонности) также имеют более высокий вес. Теперь, когда у нас есть интуитивное объяснение, мы можем упростить вышеприведенное выражение
190  Оценка склонности (Propensity Score) которое, если мы проинтегрируем по X, станет нашей оценкой, взвешенной по склонности Обратите внимание, эта оценка требует, чтобы e(x) и 1 – e(x) были больше нуля. На словах это означает, что у каждого объекта должен быть хоть какой-то шанс испытать воздействие и не испытать его. Еще один способ обозначить это условие заключается в том, что распределения объектов, испытавших воздействие, и объектов, не испытавших воздействия, должны перекрываться. Это предположение о позитивности причинно-следственного вывода (positivity assumption). Оно тоже имеет интуитивный смысл. Если объекты тестовой группы и объекты контрольной группы не пересекаются, это означает, что они очень разные, и я не смогу экстраполировать эффект одной группы на другую. Это не значит, что экстраполяцию невозможно выполнить (ее может выполнить регрессия), но она очень опасна. Это похоже на тестирование нового лекарства в эксперименте, в котором лечение получают только мужчины, а затем мы делаем вывод, что женщины будут реагировать на препарат так же хорошо, как мужчины. Прогнозирование оценки склонности В идеальном мире у нас была бы истинная оценка склонности e(x). Однако на практике механизм назначения воздействия неизвестен, и нам необходимо заменить истинную оценку склонности ее спрогнозированной оценкой ê(x). Один из распространенных способов получить такую оценку – использование логистической регрессии, но можно использовать и другие методы машинного обучения, например градиентный бустинг (хотя он требует некоторых дополнительных процедур, чтобы избежать переобучения). Здесь я воспользуюсь логистической регрессией. Это означает, что мне придется преобразовать категориальные признаки набора данных в даммипеременные. categ = ["ethnicity", "gender", "school_urbanicity"] cont = ["school_mindset", "school_achievement", "school_ethnic_minority", "school_poverty", "school_size"] data_with_categ = pd.concat([ # набор без категориальных признаков
Прогнозирование оценки склонности  191 data.drop(columns=categ), # категориальные признаки, превращенные # в дамми-переменные pd.get_dummies(data[categ], columns=categ, drop_first=False) ], axis=1) print(data_with_categ.shape) (10391, 32) Итак, получим оценку склонности с помощью логистической регрессии. from sklearn.linear_model import LogisticRegression T = 'intervention' Y = 'achievement_score' X = data_with_categ.columns.drop(['schoolid', T, Y]) ps_model = LogisticRegression(C=1e6).fit( data_with_categ[X], data_with_categ[T]) data_ps = data.assign( propensity_score=ps_model.predict_proba(data_with_categ[X])[:, 1] ) data_ps[["intervention", "achievement_score", "propensity_score"]].head() Во-первых, мы можем убедиться, что вес оценки склонности действительно реконструирует выборку, в которой все подвергаются воздействию. С помощью весов 1/e(x) мы создаем выборку, в которой все подвергаются воздействию, а с помощью весов 1/(1 – e(x)) мы создаем выборку, в которой никто не подвергается воздействию. weight_t = 1/data_ps.query("intervention==1")["propensity_score"] weight_nt = 1/(1-data_ps.query("intervention==0")["propensity_score"]) print("Исходный размер выборки", data.shape[0]) print(f"Размер выборки, в которой все получили\n" f"воздействие {sum(weight_t)}") print("Размер выборки, в которой никто не получил\n" f"воздействие {sum(weight_nt)}") Исходный размер выборки 10391
­ 192  Оценка склонности (Propensity Score) Размер выборки, в которой все получили воздействие 10388.443755049593 Размер выборки, в которой никто не получил воздействия 10391.482074447029 Кроме того, мы можем использовать оценку склонности, чтобы найти доказательства спутывания. Если один сегмент выборки имеет более высокую оценку склонности, чем другой, это означает, что воздействие вызвано каким-то неслучайным фактором. Если этот неслучайный фактор еще и влияет на результат, имеет место спутывание. В нашем случае мы видим, что учащиеся, которые считают себя более амбициозными (более высокое значение переменной success_expect), также имеют более высокую вероятность посетить семинар, посвященный мышлению с установкой на рост (более высокое значение переменной propensity_score). sns.boxplot(x="success_expect", y="propensity_score", data=data_ps) plt.title("Доказательство наличия спутывания"); Кроме того, мы должны проверить, накладываются ли группа объектов, подвергнутых воздействию, и группа объектов, не подвергнутых воздействию. С этой целью мы можем посмотреть эмпирическое распределение оценки склонности для группы объектов, подвергнутых воздействию, и группы объектов, не подвергнутых воздействию. Глядя на рисунок ниже, мы видим, что ни у кого нет нулевой оценки склонности и низкие оценки склонности мы можем найти в обеих группах. В таким случаях мы говорим, что группа объектов, подвергнутых.., и группа объектов, не подвергнутых воздействию, хорошо сбалансированы. sns.distplot(data_ps.query("intervention==0")["propensity_score"], kde=False, label="Не подвергнутые\nвоздействию")
Прогнозирование оценки склонности  193 sns.distplot(data_ps.query("intervention==1")["propensity_score"], kde=False, label="Подвергнутые\nвоздействию") plt.title("Проверка позитивности") plt.legend(); Наконец, мы можем воспользоваться оценкой, взвешенной по склонности, для вычисления среднего эффекта воздействия. weight = ((data_ps["intervention"] - data_ps["propensity_score"]) / (data_ps["propensity_score"] * (1 - data_ps["propensity_score"]))) y1 = sum( data_ps.query("intervention==1")["achievement_score"] * weight_t ) / len(data) y0 = sum( data_ps.query("intervention==0")["achievement_score"] * weight_nt ) / len(data) ate = np.mean(weight * data_ps["achievement_score"]) print("Y1:", y1) print("Y0:", y0) print("ATE", ate) Y1: 0.25957089560337376 Y0: -0.12891241487879396 ATE 0.38848331048216755 Воспользовавшись взвешиванием по склонности, мы можем сказать, что учащиеся, подвергнувшиеся воздействию, будут на 0.38 стандартного отклонения выше своих коллег, не подвергнувшихся воздействию, с точки зрения достижений. Если никто не подвергнется воздействию, следует ожидать, что
194  Оценка склонности (Propensity Score) общий уровень достижений будет на 0.12 стандартного отклонения ниже, чем сейчас. По тем же соображениям мы должны ожидать, что общий уровень достижений будет на 0.25 стандартного отклонения выше, если мы проведем семинар для всех. Сравните это с оценкой ATE 0.47, которую мы получили, просто сравнив участников, испытавших воздействие, и участников, не испытавших воздействия. Это свидетельствует о том, что наше смещение действительно положительно и контроль X дает нам более скромную оценку влияния семинара. Стандартная ошибка Чтобы вычислить стандартную ошибку для IPTW-оценки, мы можем воспользоваться формулой дисперсии взвешенного среднего: Однако мы можем воспользоваться ею только в том случае, если у нас есть истинная оценка склонности. Если мы используем спрогнозированную (расчетную) оценку склонности, нам необходимо учесть ошибки, связанные с процессом оценивания. Самый простой способ сделать это – запустить
Стандартная ошибка  195 бутстреп всей процедуры. На исходных данных мы генерируем выборку с возвращением, строим модель логистической регрессии и вычисляем ATE. Мы повторяем этот процесс много раз, чтобы получить распределение оценок ATE. # для распараллеливания from joblib import Parallel, delayed # пишем функцию, которая вычисляет IPTW-оценку def run_ps(df, X, T, y): # оцениваем оценку склонности ps = LogisticRegression(C=1e6, max_iter=300).fit( df[X], df[T] ).predict_proba(df[X])[:, 1] # задаем веса weight = (df[T] - ps) / (ps * (1-ps)) # вычисляем ATE return np.mean(weight * df[y]) np.random.seed(88) # генерируем 1000 бутстреп-выборок bootstrap_sample = 1000 ates = Parallel(n_jobs=4)(delayed(run_ps)( data_with_categ.sample(frac=1, replace=True), X, T, Y ) for _ in range(bootstrap_sample)) ates = np.array(ates) Здесь ATE является средним, полученным по значениям ATE, вычисленным для бутстреп-выборок. Чтобы получить доверительные интервалы, мы можем взять квантили бутстреп-распределения. Для 95%-ного доверительного интервала мы используем процентили 2.5 и 97.5. print(f"ATE: {ates.mean()}") print(f"95%-ный доверительный интервал:" f"{(np.percentile(ates, 2.5), np.percentile(ates, 97.5))}") ATE: 0.3877454383726752 95%-ный доверительный интервал:(0.3545138010444087, 0.4199256010209553) Кроме того, мы можем визуализировать значения ATE, полученные по бустреп-выборкам, а также границы доверительного интервала. sns.distplot(ates, kde=False) plt.vlines(np.percentile(ates, 2.5), 0, 30, linestyles="dotted") plt.vlines(np.percentile(ates, 97.5), 0, 30, linestyles="dotted", label="95% ДИ") plt.title("Бутстреп-распределение ATE") plt.legend();
196  Оценка склонности (Propensity Score) Распространенные проблемы с оценкой склонности Как специалист по data science я знаю, что может возникнуть соблазн использовать всю мощь инструментария машинного обучения, чтобы сделать оценку склонности максимально точной. Вы можете быстро увлечься оптимизацией AUC-ROC, перекрестной проверкой и байесовской оптимизацией гиперпараметров. Я не говорю, что вам не следует этого делать. На самом деле вся теория, касающаяся оценки склонности и машинного обучения, появилась совсем недавно, поэтому мы еще многого не знаем. Но сначала стоит кое-что понять. Качество прогнозирования оценки склонности не отражается на ее балансирующих свойствах. Если вы занимаетесь машинным обучением и только начинаете знакомиться с анализом причинно-следственных связей, самое сложное – отказаться рассматривать все как задачу прогнозирования. На самом деле максимизация предсказательной силы оценки склонности может даже навредить целям анализа причинно-следственных связей. Оценка склонности не обязательно должна очень хорошо предсказывать воздействие. Просто необходимо включить все спутывающие переменные. Если мы включим переменные, которые очень хорошо подходят для прогнозирования воздействия, но не влияют на результат, это фактически увеличит дисперсию оценки склонности. Это похоже на проблему, с которой сталкивается линейная регрессия, когда мы включаем переменные, коррелирующие с воздействием, но не с результатом.
Распространенные проблемы с оценкой склонности  197 Чтобы убедиться в этом, рассмотрим следующий пример (взятый из книги Эрнана). У вас есть 2 школы, одна из них проводит семинар «Мышление с установкой на рост» для 99 % своих учеников, а другая – для 1 %. Предположим, что школы не оказывают никакого влияния на эффект воздействия (кроме как через само воздействие), поэтому контролировать его нет необходимости. Если вы добавите переменную школа в модель, прогнозирующую оценку склонности, она будет обладать очень высокой предсказательной силой. Однако чисто случайно мы могли бы получить выборку, в которой все учащиеся в школе А подверглись воздействию, что дало бы оценку склонности для этой школы, равную 1, а это, в свою очередь, привело бы к огромной дисперсии. Это экстремальный пример, но давайте проиллюстрируем его на синтетических данных. np.random.seed(42) school_a = pd.DataFrame(dict(T=np.random.binomial(1, .99, 400), school=0, intercept=1)) school_b = pd.DataFrame(dict(T=np.random.binomial(1, .01, 400), school=1, intercept=1)) ex_data = pd.concat([school_a, school_b]).assign( y=lambda d: np.random.normal(1 + 0.1 * d["T"])) ex_data.head()
198  Оценка склонности (Propensity Score) Сгенерировав эти данные, мы дважды запускаем процедуру бутстрепа, чтобы спрогнозировать оценку склонности. В первой процедуре бутстрепа мы включаем школу как признак в нашу модель, прогнозирующую оценку склонности. Во второй процедуре бутстрепа мы не включаем в модель школу как признак. ate_w_f = np.array([run_ps(ex_data.sample(frac=1, replace=True), ["school"], "T", "y") for _ in range(500)]) ate_wo_f = np.array([run_ps(ex_data.sample(frac=1, replace=True), ["intercept"], "T", "y") for _ in range(500)]) sns.distplot( ate_w_f, kde=False, label="PS школа как\nпризнак включена" ) sns.distplot( ate_wo_f, kde=False, label="PS школа как\nпризнак не включена" ) plt.legend(); Как видим, прогнозная модель оценки склонности, в которую добавлена школа в качестве признака, демонстрирует огромную дисперсию, в то время как модель, в которой школа как признак отсутствует, ведет себя гораздо лучше. Кроме того, поскольку школа не является спутывающим фактором, модель без этого признака не является смещенной. Как я уже сказал, речь идет не просто о прогнозировании воздействия. На самом деле нам нужно построить прогноз таким образом, чтобы контролировать спутывание, а не прогнозировать воздействие. Это приводит к еще одной проблеме, часто встречающейся в методах прогнозирования оценки склонности. В нашем случае с семинаром данные
Распространенные проблемы с оценкой склонности  199 оказались очень сбалансированными. Но это не всегда так. В некоторых ситуациях у объектов тестовой группы вероятность воздействия может быть гораздо выше, чем у объектов контрольной группы, и распределения оценок склонности не будут сильно перекрываться. sns.distplot(np.random.beta(4,1,500), kde=False, label="Подвергнутые\nвоздействию") sns.distplot(np.random.beta(1,3,500), kde=False, label="Не подвергнутые\nвоздействию") plt.title("Проверка позитивности") plt.legend(); Если такое происходит, значит, позитивность не очень сильно выражена. Если у объекта из тестовой группы (объекта, испытавшего воздействие) оценка склонности равна, скажем, 0.9, а максимальная оценка склонности у объекта из контрольной группы (объекта, не испытавшего воздействия) составляет 0.7, у нас не будет ни одного объекта из контрольной группы, который мы могли бы сравнить с объектом из тестовой группы, получившим оценку 0.9. Отсутствие сбалансированности может привести к некоторому смещению, поскольку нам придется экстраполировать эффект воздействия на неизвестные области. Мало того, объекты с очень высокими или очень низкими оценками склонности имеют довольно высокий вес, что увеличивает дисперсию. Как правило, у вас будут проблемы, если какой-либо вес превышает 20 (такое наблюдается, если у вас есть объект, не испытавший воздействие, с оценкой склонности 0.95 или объект, испытавший воздействие, с оценкой склонности 0.05). Альтернативой является ограничение веса до максимального размера 20. Это уменьшит дисперсию, но на самом деле приведет к еще большему сме-
­ 200  Оценка склонности (Propensity Score) щению. Честно говоря, хоть это и обычная практика уменьшения дисперсии, мне она не очень нравится. Вы никогда не узнаете, насколько велико смещение, которое вы вызываете, применив клиппинг. Кроме того, если распределения не перекрываются, ваших данных, вероятно, в любом случае недостаточно, чтобы сделать причинно-следственный вывод. Чтобы получить более полное представление об этом, мы можем рассмотреть метод, который сочетает в себе оценку склонности и сопоставление. Сопоставление по оценке склонности (propensity score matching) Как я уже говорил ранее, вам не нужно контролировать X, если у вас есть оценка склонности. Ее будет достаточно. Таким образом, вы можете рассмат ривать оценку склонности как своего рода способ уменьшения размерности применительно к пространству признаков. Он объединяет все признаки X в одну размерность – назначение воздействия. По этой причине мы можем рассматривать оценку склонности как входной признак для других моделей. Возьмем, к примеру, модель регрессии. smf.ols( "achievement_score ~ intervention + propensity_score", data=data_ps ).fit().summary().tables[1] Если мы учитываем оценку склонности (т. е. иcпользуем ее в качестве признака), то получаем оценку ATE, равную 0.39, что ниже значения 0.47, которое мы получили ранее с помощью регрессионной модели, не учитывавшей оценку склонности. Кроме того, мы можем использовать сопоставление (матчинг) по оценке склонности. На этот раз, вместо того чтобы пытаться искать совпадения по схожести значений всех признаков X, мы можем искать совпадения по оценке склонности. Это большой шаг вперед по сравнению с матчинг-оценкой, поскольку он устраняет проклятие размерности. Кроме того, если признак не важен для назначения воздействия, модель, прогнозирующая оценку склонности, запомнит это и придаст ему низкую важность в ходе моделирования механизма воздействия. С другой стороны, сопоставление по значениям признаков все равно будет пытаться найти совпадения, в которых люди будут схожи по этому неважному признаку.
Ключевые идеи  201 cm = CausalModel( Y=data_ps["achievement_score"].values, D=data_ps["intervention"].values, X=data_ps[["propensity_score"]].values ) cm.est_via_matching(matches=1, bias_adj=True) print(cm.estimates) Как видим, мы получаем ATE 0.38, что больше соответствует результату, который мы получили при взвешивании по оценке склонности. Кроме того, сопоставление по оценке склонности дает нам некоторое представление о том, почему опасно иметь небольшое перекрытие распределений оценок склонности для объектов тестовой и контрольной групп. Если это произойдет, расхождение в оценках склонности будет большим, что приведет к смещению, как мы и видели в главе, посвященной сопоставлению. Последнее предостережение здесь заключается в том, что вышеприведенные стандартные ошибки неверны, поскольку они не учитывают неопределенность оценки показателя склонности. К сожалению, бутстреп не работает с сопоставлением https://economics.mit.edu/sites/default/files/publications/ ON%20THE%20FAILURE%20OF%20THE%20BOOTSTRAP%20FOR.pdf. Кроме того, вышеприведенная теория настолько свежа, что не существует Python’овских реализаций методов оценки склонности с правильно вычисленными стандартными ошибками. По этой причине мы не видим широкого применения сопоставления по оценке склонности в Python. Ключевые идеи В этой главе мы узнали, что вероятность испытать воздействие называется оценкой склонности и мы можем использовать ее в качестве балансирующего значения. Это означает, что если у нас есть оценка склонности, нам не нужно напрямую контролировать спутывающие факторы. Достаточно контролировать оценку склонности, чтобы выявить причинно-следственный эффект. Мы увидели, как оценка склонности решает задачу уменьшения размерности в пространстве спутывающих факторов. Эти свойства позволили нам получить взвешенную оценку. Мало того, мы увидели, как оценку склонности можно использовать наряду с другими методами для контроля смещения, вызванного спутывающими факторами.
202  Оценка склонности (Propensity Score) Затем мы рассмотрели некоторые распространенные проблемы, которые могут возникнуть при работе с оценкой склонности и в ходе анализа причинно-следственных связей в целом. Первая проблема возникает, когда мы увлекаемся моделированием механизма лечения. Мы увидели, что повышение качества прогнозирования оценки склонности не приводит к более точной оценке причинно-следственного эффекта, поскольку может увеличить дисперсию. Наконец, мы рассмотрели некоторые проблемы экстраполяции, с которыми мы можем столкнуться, если не сможем обеспечить хорошее перекрытие распределений оценок склонности для объектов тестовой и контрольной групп. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Глава 12 Получение оценок с двойной робастностью Не кладите все яйца в одну корзину Мы научились использовать линейную регрессию и взвешивание по оценке склонности, чтобы оценить E[Y | T = 1] – E[Y | T = 0] | X. Но какой из способов нам следует использовать и когда? Если сомневаетесь, просто используйте оба! Двойная робастная оценка – это способ объединить оценку склонности и линейную регрессию таким образом, чтобы вам не приходилось полагаться на какой-то один из этих методов. Чтобы увидеть, как это работает, давайте рассмотрим эксперимент с мышлением. Он представляет собой рандомизированное исследование, проведенное в государственных средних школах США (9–12 классы), цель которого заключалась в том, чтобы выявить влияние мышления с установкой на рост. Эксперимент выглядит следующим образом: учащиеся проходят в школе семинар, призванный привить им мышление с установкой на рост. Затем организаторы наблюдают за учащимися, чтобы оценить их успеваемость. Измерения складываются в балл достижений и стандартизированы. Реальные данные об этом исследовании не являются общедоступными в целях сохранения конфиденциальности учащихся. Однако у нас есть синтетический набор данных с такими же статистическими свойствами, который использовали в своей работе Сьюзан Эти и Стефан Вагер (Athey and Wager) https:// arxiv.org/pdf/1902.07409.pdf, поэтому мы воспользуемся им. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from matplotlib import style
204  Получение оценок с двойной робастностью from matplotlib import pyplot as plt import seaborn as sns from sklearn.linear_model import (LogisticRegression, LinearRegression) %matplotlib inline style.use("fivethirtyeight") pd.set_option("display.max_columns", 30) data = pd.read_csv("data/learning_mindset.csv") data.sample(5, random_state=5) Хотя исследование было рандомизированным, похоже, что данные содержат спутывающие факторы. Если мы посмотрим на дополнительные характеристики, то заметим, что они систематически меняются в зависимости от теста и контроля. Одна из возможных причин этой вариабельности заключается в том, что переменная воздействия сводится к посещению студен-
­ Не кладите все яйца в одну корзину  205 том семинара. Таким образом, хотя возможность участия была случайной, само участие таковым не является. Здесь мы имеем дело с несоблюдением требований. Одним из свидетельств этого является то, как ожидания успеха у учащегося коррелируют с участием в семинаре. Учащиеся с более высокими самооценками успешности с большей вероятностью присоединятся к семинару «Мышление с установкой на рост». # посмотрим среднее значение воздействия по самооценкам успешности data.groupby("success_expect")["intervention"].mean() success_expect 1 0.271739 2 0.265957 3 0.294118 4 0.271617 5 0.311070 6 0.354287 7 0.362319 Name: intervention, dtype: float64 Как мы уже знаем, мы могли бы скорректировать это, используя линейную регрессию или спрогнозировав оценки склонности с помощью логистической регрессии. Однако прежде чем мы это сделаем, нам нужно преобразовать категориальные переменные в дамми-переменные. categ = ["ethnicity", "gender", "school_urbanicity"] cont = ["school_mindset", "school_achievement", "school_ethnic_minority", "school_poverty", "school_size"] data_with_categ = pd.concat([ # набор без категориальных признаков data.drop(columns=categ), # категориальные признаки, превращенные # в дамми-переменные pd.get_dummies(data[categ], columns=categ, drop_first=False) ], axis=1) print(data_with_categ.shape) (10391, 32) Теперь мы можем посмотреть, как работает оценка с двойной робаст ностью.
206  Получение оценок с двойной робастностью Получение оценок с двойной робастностью Вместо того чтобы вычислить оценку, я сначала покажу ее вам и только потом расскажу, почему она является классной. где P̂(x) – это спрогнозированная оценка склонности (например, спрогнозированная с помощью логистической регрессии), μ̂1(x) – это спрогнозированное значение E [Y | X, T = 1] (например, спрогнозированное с помощью линейной регрессии) и μ̂0(x) – это спрогнозированное значение E [Y | X, T = 0]. Как вы уже могли догадаться, первая часть дважды робастной оценки оценивает E [Y1], а вторая часть оценивает E [Y0]. Давайте рассмотрим первую часть, так как вся интуиция по аналогии применима и ко второй части. Поскольку я знаю, что эта формула поначалу пугает (но не волнуйтесь, вы увидите, что она очень проста), сначала я покажу, как программно реализовать эту оценку. У меня такое ощущение, что некоторых людей код пугает меньше, чем формулы. Посмотрим, как эта оценка работает на практике.
Получение оценок с двойной робастностью  207 # пишем функцию вычисления оценки с двойной робастностью def doubly_robust(df, X, T, Y): ps = LogisticRegression(C=1e6, max_iter=1000).fit( df[X], df[T]).predict_proba(df[X])[:, 1] mu0 = LinearRegression().fit( df.query(f"{T}==0")[X], df.query(f"{T}==0")[Y]).predict(df[X]) mu1 = LinearRegression().fit( df.query(f"{T}==1")[X], df.query(f"{T}==1")[Y]).predict(df[X]) return ( np.mean(df[T]*(df[Y] - mu1)/ps + mu1) np.mean((1-df[T])*(df[Y] - mu0)/(1-ps) + mu0) ) T = 'intervention' Y = 'achievement_score' X = data_with_categ.columns.drop(['schoolid', T, Y]) doubly_robust(data_with_categ, X, T, Y) 0.38822121767832457 Оценка с двойной робастностью говорит нам следующее: мы должны ожидать, что люди, посетившие семинар, посвященный мышлению с установкой на рост, будут на 0.388 стандартного отклонения успешнее своих коллег, не испытавших воздействия (не посетивших семинар), с точки зрения достижений. И снова мы можем воспользоваться бутстрепом для построения доверительного интервала. # для распараллеливания from joblib import Parallel, delayed np.random.seed(88) # генерируем 1000 бутстреп-выборок bootstrap_sample = 1000 ates = Parallel(n_jobs=4)(delayed(doubly_robust)( data_with_categ.sample(frac=1, replace=True), X, T, Y ) for _ in range(bootstrap_sample)) ates = np.array(ates) print(f"95%-ный доверительный интервал ATE:" f"{(np.percentile(ates, 2.5), np.percentile(ates, 97.5))}") 95%-ный доверительный интервал ATE:(0.3536537980208197, 0.4197843234711139) sns.distplot(ates, kde=False) plt.vlines(np.percentile(ates, 2.5), 0, 20, linestyles="dotted") plt.vlines(np.percentile(ates, 97.5), 0, 20, linestyles="dotted", label="95% ДИ") plt.title("Бутстреп-распределение ATE") plt.legend();
­ 208  Получение оценок с двойной робастностью Теперь, когда мы познакомились с двойной робастной оценкой, давайте выясним, почему она так хороша. Во-первых, ее называют двойной робастной (или дважды робастной), поскольку для ее вычисления требуется корректная спецификация только одной из моделей: P̂(x) или μ̂(x). Чтобы убедиться в этом, возьмем первую часть, которая оценивает E [Y1], и внимательно посмотрим на нее. Предположим, что модель μ̂1(x) задана корректно. Если модель P̂(x), прог нозирующая оценку склонности, задана некорректно, нам не о чем беспокоиться. Потому что если модель μ̂1(x) задана корректно, то E[T i (Yi – μ̂1(Xi ))] = 0. Это происходит потому, что, умножая на T i, мы отбираем только объекты из тестовой группы и остатки модели μ̂1(x) для объектов тестовой группы по определению имеют нулевое среднее значение. Это приводит к тому, что все сводится к μ̂1(Xi ), которая дает корректную оценку E [Y1] согласно предположению. Итак, вы видите, что, будучи корректной, модель μ̂1(Xi ) сводит на нет релевантность модели, прогнозирующей оценку склонности. Мы можем применить те же рассуждения, чтобы понять оценку E [Y0]. Но не верьте мне на слово. Пусть программный код укажет вам путь! В следующей функции вычисления оценки с двойной робастностью я заменил логистическую регрессию, прогнозирующую оценку склонности, случайной величиной, которая равномерно распределена и принимает значения в диапазоне от 0.1 до 0.9 (я не хочу, чтобы очень маленькие веса увеличивали дисперсию моей оценки склонности). Поскольку наша оценка случайна, она ни в коем случае не может быть хорошей моделью, прогнозирующей оценку склонности, но мы увидим, что нам все же удается получить оценку, которая
­ Получение оценок с двойной робастностью  209 очень близка к той, когда оценка склонности оценивалась с помощью логис тической регрессии. # пишем функцию вычисления оценки с двойной робастностью # c плохой моделью, прогнозирующей оценку склонности ps def doubly_robust_wrong_ps(df, X, T, Y): # плохая модель, прогнозирующая оценку склонности ps np.random.seed(654) ps = np.random.uniform(0.1, 0.9, df.shape[0]) mu0 = LinearRegression().fit( df.query(f"{T}==0")[X], df.query(f"{T}==0")[Y]).predict(df[X]) mu1 = LinearRegression().fit( df.query(f"{T}==1")[X], df.query(f"{T}==1")[Y]).predict(df[X]) return ( np.mean(df[T]*(df[Y] - mu1)/ps + mu1) np.mean((1-df[T])*(df[Y] - mu0)/(1-ps) + mu0) ) doubly_robust_wrong_ps(data_with_categ, X, T, Y) 0.3797369830995927 Если мы воспользуемся бутстрепом, то увидим, что дисперсия будет немного выше дисперсии, когда оценка склонности прогнозировалась с помощью логистической регрессии. np.random.seed(88) parallel_fn = delayed(doubly_robust_wrong_ps) wrong_ps = Parallel(n_jobs=4)(parallel_fn( data_with_categ.sample(frac=1, replace=True), X, T, Y ) for _ in range(bootstrap_sample)) wrong_ps = np.array(wrong_ps) print(f"95% ДИ для исходного ATE:\n", f"{(np.percentile(ates, 2.5), np.percentile(ates, 97.5))}") print(f"95% ДИ для ATE, случайный прогноз PS:\n", f"{(np.percentile(wrong_ps, 2.5), np.percentile(wrong_ps, 97.5))}") 95% ДИ для исходного ATE: (0.3536537980208197, 0.4197843234711139) 95% ДИ для ATE, случайный прогноз PS: (0.3386340898920719, 0.4330454015305949) Как мы видим, случайный прогноз оценки склонности дает немного другое значение ATE, но не намного отличающееся от исходного значения ATE. Мы описали случай, когда модель, прогнозирующая оценку склонности, задана некорректно, однако результирующая модель задана корректно. А как насчет обратной ситуации? Давайте еще раз внимательно посмотрим на первую часть двойной робастной оценки, но при этом переставим местами некоторые члены.
210  Получение оценок с двойной робастностью Теперь предположим, что модель P̂(Xi), прогнозирующая оценку склонности, задана корректно. В этом случае E[T i – P̂(Xi )] = 0, что удаляет часть оценки, зависящую от μ̂1(Xi ). Это приводит к тому, что двойная робастная оценка сводится к процедуре взвешивания по оценке склонности , ко- торая корректна согласно предположению. Таким образом, даже если модель μ̂1(Xi ) задана некорректно, двойная робастная оценка все равно будет корректной, при условии что модель, прогнозирующая оценку склонности, задана корректно. Еще раз: если вы больше верите в код, чем в формулы, вот вам проверка на практике. В нижеприведенном коде я заменил обе модели линейной регрессии случайной нормально распределенной переменной. Без сомнения, модель μ̂1(Xi ) задана некорректно. Однако мы увидим, что оценка с двойной робастностью по-прежнему позволяет восстановить то же самое значение AT̂E в районе 0.38, что мы видели раньше. # пишем функцию вычисления оценки с двойной робастностью # c плохой результирующей моделью mu(x) def doubly_robust_wrong_model(df, X, T, Y): np.random.seed(654) ps = LogisticRegression(C=1e6, max_iter=1000).fit( df[X], df[T]).predict_proba(df[X])[:, 1] # плохая результирующая модель mu(x) mu0 = np.random.normal(0, 1, df.shape[0]) mu1 = np.random.normal(0, 1, df.shape[0]) return ( np.mean(df[T]*(df[Y] - mu1)/ps + mu1) -
Получение оценок с двойной робастностью  211 np.mean((1-df[T])*(df[Y] - mu0)/(1-ps) + mu0) ) doubly_robust_wrong_model(data_with_categ, X, T, Y) 0.39811864040982625 Опять же мы можем воспользоваться бутстрепом и увидеть, что дисперсия стала только чуть выше. np.random.seed(88) parallel_fn = delayed(doubly_robust_wrong_model) wrong_mux = Parallel(n_jobs=4)(parallel_fn( data_with_categ.sample(frac=1, replace=True), X, T, Y ) for _ in range(bootstrap_sample)) wrong_mux = np.array(wrong_mux) print(f"95% ДИ для исходного ATE:\n", f"{(np.percentile(ates, 2.5), np.percentile(ates, 97.5))}") print(f"95% ДИ для ATE, случайный прогноз Mu:\n", f"{(np.percentile(wrong_mux, 2.5), np.percentile(wrong_mux, 97.5))}") 95% ДИ для исходного ATE: (0.3536537980208197, 0.4197843234711139) 95% ДИ для ATE, случайный прогноз Mu: (0.33870864261850014, 0.43320014848137417) Опять же, если испортить модель условного среднего (результирующую модель), то ATE будет лишь немного отличаться. Надеюсь, мне удалось убедить вас в силе двойной робастной оценки. Это волшебство происходит потому, что при причинно-следственном выводе есть два способа устранить смещение наших причинно-следственных оценок: вы либо моделируете механизм лечения, либо механизм результата. Если какая-либо из этих моделей корректна, все будет хорошо. Единственное предостережение заключается в том, что на практике очень сложно точно смоделировать любой из этих двух механизмов. Чаще всего в конечном итоге ни модель оценки склонности, ни результирующая модель не являются корректными на 100 %. Они обе являются некорректными, но по-разному. Когда это происходит, еще не совсем ясно1, что лучше использовать – одну модель или двойную робастную оценку. Что касается меня, то мне все еще нравится их использовать, потому что это дает мне по крайней мере две возможности подтвердить свою правоту. 1 Joseph D. Y. Kang and Joseph L. Schafer. Demystifying Double Robustness: A Comparison of Alternative Strategies for Estimating a Population Mean from Incomplete Data. Statistical science, 2007. Vol. 22 (4), p. 523–539. https://www.stat.cmu.edu/~ryantibs/ journalclub/kang_2007.pdf.
212  Получение оценок с двойной робастностью Ключевые идеи В этой главе мы увидели простой способ объединения линейной регрессии с моделью, прогнозирующей оценку склонности, для получения двойной робастной оценки. Эта оценка носит такое название, потому что для нее требуется корректность только одной из двух моделей. Если модель, прог­ нозирующая оценку склонности, задана корректно, мы сможем определить причинно-следственный эффект, даже если результирующая модель задана некорректно. С другой стороны, если результирующая модель задана корректно, мы сможем определить причинно-следственный эффект, даже если модель, прогнозирующая оценку склонности, является некорректной.
Глава 13 Метод разности разностей Три рекламных щита на юге Бразилии Помню, когда я работал в сфере маркетинга, отличным маркетинговым каналом была интернет-реклама. Не потому, что она была очень эффективна (хотя это так), а потому, что было очень легко выяснить, сработала она или нет. С помощью онлайн-маркетинга у вас есть возможность узнать, какие клиенты видели рекламу, и вы можете отслеживать их с помощью файлов cookie, чтобы узнать, оказались ли они на главной странице вашего сайта или нажали кнопку Download. А еще вы можете воспользоваться машинным обучением для поиска потенциальных клиентов, которые очень похожи на ваших текущих клиентов, и показывать рекламу только им. В этом смысле онлайн-маркетинг очень точен: вы ориентируетесь лишь на посетителей, которые вам нужны, и можно увидеть, реагируют ли они так, как вам хотелось бы. Но не все поддаются воздействию онлайн-маркетинга. Иногда приходится прибегать к менее точным методам, например к телевизионной рекламе или размещению рекламного щита на улице. Обычно отделы маркетинга стремятся к разнообразию маркетинговых каналов. Но если онлайн-маркетинг – это профессиональная удочка для ловли определенного вида тунца, то рекламные щиты и телевидение – это гигантские сети, которые вы набрасываете на косяк рыбы и надеетесь поймать хотя бы несколько крупных рыб. Еще одна проблема с рекламными щитами и телевизионной рекламой заключается в том, что гораздо сложнее определить степень их эффективности. Конечно, вы можете измерить объем покупок или что-то еще, что вы хотите повысить, до и после размещения рекламного щита в каком-то месте. Если есть увеличение нужных показателей, есть некоторые доказательства того, что маркетинг эффективен. Но как узнать, не является ли этот
­ 214  Метод разности разностей рост просто какой-то естественной тенденцией, связанной с узнаваемостью вашего продукта? Другими словами, как бы вы узнали контрфактический результат Y0? Как бы узнали о том, что произошло бы, если бы вы изначально не установили рекламные щиты? Когда вы не знаете, что бы произошло, поэтому не можете оценить контрфактические результаты Ну что ж, храните свои секреты Одним из способов ответить на подобные вопросы является метод «разность разностей» (Difference-in-Difference или diff-in-diff ). Разность разностей обычно используется для оценки влияния макроинтервенций, сюда можно отнести влияние иммиграции на безработицу, влияние изменений закона об оружии на уровень преступности или просто влияние маркетинговой кампании на вовлеченность пользователей. Во всех этих случаях у вас есть период до и после вмешательства, и вы хотите отделить влияние вмешательства от общего тренда. В качестве мотивирующего примера давайте рассмотрим вопрос, похожий на тот, с которым я когда-то столкнулся. Чтобы выяснить, насколько эффективны рекламные щиты в качестве маркетингового канала, мы разместили три рекламных щита в городе ПортуАлегри, столице штата Риу-Гранди-ду-Сул. Мы хотели посмотреть, увеличит ли это сумму депозитов в банке. На заметку для тех, кто не очень знаком с географией Бразилии: Риу-Гранди-ду-Сул находится на юге страны, в одном из самых развитых регионов. Помня об этом, мы решили еще взглянуть на данные по другой столице юга, Флорианополису, столице штата Санта-Катарина. Идея состояла в том, что если данные по Порту-Алегри – это тестовая группа, то Флорианополис будет контрольной группой для оценки контрфактического результата (кстати, это был не настоящий эксперимент, информация по настоящему эксперименту конфиденциальна, но идея очень похожа). Мы разместили рекламные щиты в Порту-Алегри на весь июнь. Данные, которые у нас есть, выглядят следующим образом.
­ Метод разности разностей (difference in differences – DiD)  215 import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from matplotlib import style from matplotlib import pyplot as plt import seaborn as sns import statsmodels.formula.api as smf %matplotlib inline style.use("fivethirtyeight") data = pd.read_csv("data/billboard_impact.csv") data.head() Помните, что переменная deposits – это наша переменная результата (сумма депозита по клиенту), которую мы хотим увеличить с помощью рекламных щитов. Переменная poa – это дамми-переменная для города Порту-Алег ри. Если она равна 1, наблюдения взяты из Порту-Алегри, если она равна 0, наблюдения взяты из Флорианополиса. Переменная jul – это дамми-переменная июля-месяца или периода после интервенции. Если она равна 1, наблюдения относятся к июлю, периоду после интервенции, если она равна 0, наблюдения относятся к маю, периоду до интервенции. Метод разности разностей (difference in differences – DiD) Чтобы избежать путаницы со временем (time) и воздействием (treatment), с этого момента я буду использовать D для обозначения воздействия и T для обозначения времени. Пусть YD(T ) – потенциальный результат воздействия D в период T. В идеальном мире, в котором у нас есть возможность наблюдать контрфактический результат, мы бы оценили воздействующий эффект интервенции следующим образом: AT̂ET = E [Y1(1) – Y0(1) | D = 1].
216  Метод разности разностей Другими словами, причинно-следственный эффект – это результат, зафиксированный в период после интервенции в случае воздействия, минус результат, тоже зафиксированный в период после интервенции, но только в случае отсутствия воздействия. Конечно, мы не можем его измерить, потому что Y0(1) является контрфактическим результатом. Один из способов решения этой проблемы – сравнение «до» и «после»: AT̂ET = E [Y(1) | D = 1] – E [Y(0) | D = 1]. В нашем примере мы сравним среднюю сумму депозитов в Порту-Алегри до и после размещения рекламных щитов. poa_before = data.query("poa==1 & jul==0")["deposits"].mean() poa_after = data.query("poa==1 & jul==1")["deposits"].mean() poa_after - poa_before 41.04775 Эта оценка говорит нам, что после интервенции мы должны ожидать увеличения средней суммы депозитов на 41.04 бразильского реала. Но можем ли мы ей доверять? Обратите внимание, что E[Y(0) | D = 1] = E[Y0(0) | D = 1], то есть наблюдаемый результат для объекта тестовой группы до интервенции равен контрфактическому результату для объекта тестовой группы тоже до интервенции. Поскольку мы используем это выражение для оценки контрфактического результата после интервенции E [Y0(1) | D = 1], приведенная выше формула предполагает, что E [Y0(1) | D = 1] = E [Y0(0) | D = 1]. Речь идет о том, что в случае отсутствия интервенции результат в последнем периоде был бы таким же, как результат в начальном периоде. Очевидно, что это было бы неверно, если бы ваша результирующая переменная подчинялась какому-либо тренду. Например, если средняя сумма депозитов в Порту-Алегри растет, то E [Y0(1) | D = 1] > E [Y0(0) | D = 1] , т. е. результат в последнем периоде был бы больше, чем результат в начальном периоде, даже при отсутствии интервенции. Аналогично, если средняя сумма депозитов снижается, то E [Y0(1) | D = 1] < E [Y0(0) | D = 1], т. е. результат в последнем периоде был бы меньше, чем результат в начальном периоде, даже при отсутствии интервенции. Таким образом, мы продемонстрировали, что анализ «до» и «после» не является хорошей моделью. Другая идея состоит в том, чтобы сравнить тестовую группу с контрольной группой в период после интервенции: AT̂ET = E [Y(1) | D = 1] – E [Y(1) | D = 0]. Применительно к нашему примеру мы могли бы сравнить среднюю сумму депозитов в Порту-Алегри со средней суммой депозитов во Флорианополисе в период после интервенции.
Метод разности разностей (difference in differences – DiD)  217 fl_after = data.query("poa==0 & jul==1")["deposits"].mean() poa_after - fl_after -119.10175000000001 Эта оценка говорит нам, что кампания вредна и клиенты уменьшат среднюю сумму депозитов на 119.10 бразильского реала. Обратите внимание, что E[Y(1) | D = 0] = E[Y0(1) | D = 0]. И поскольку мы используем E[Y(1) | D = 0], чтобы оценить контрфактический результат для тестовой группы после интервенции, мы предполагаем, что можем заменить недостающий контрфактический результат следующим образом: E[Y0(1) | D = 0] = E [Y0(1) | D = 1]. Но обратите внимание, что это будет верно только в том случае, если обе группы сопоставимы по средней сумме депозитов в мае, до интервенции (сопоставимы по базовому уровню). Например, если во Флорианополисе средняя сумма депозитов гораздо больше, чем в Порту-Алегри, это выражение было бы неверно, потому что E [Y0(1) | D = 0] > E [Y0(1) | D = 1]. С другой стороны, если во Флорианополисе средняя сумма депозитов гораздо ниже, чем в Порту-Алегри, то у нас будет E [Y0(1) | D = 0] < E [Y0(1) | D = 1]. Опять же, это не лучшая идея. Чтобы решить эту проблему, мы можем использовать сравнение на основе пространства и времени. В этом и заключается идея подхода «разность разностей». Он работает путем замены отсутствующего контрфактического результата следующим образом: E [Y0(1) | D = 1] = E [Y0(0) | D = 1] + (E [Y0(1) | D = 0] – E [Y0(0) | D = 0]). Мы берем результат для объекта тестовой группы до интервенции и добавляем к результату компоненту тренда, которая оценивается с помощью контрольной группы. Другими словами, это означает, что объект тестовой группы после интервенции, если бы он не подвергнулся воздействию, выглядел бы так же, как объект тестовой группы до воздействия, плюс фактор роста, аналогичный фактору роста в контрольной группе. Важно отметить, что это утверждение предполагает, что тренды в тестовой группе и контрольной группе одинаковы: E [Y0(1) – Y0(0) | D = 1] = E [Y0(1) – Y0(0) | D = 0], где левая часть представляет собой контрфактический тренд. Теперь мы можем заменить расчетный контрфактический результат в формуле эффекта воздействия: E [Y1(1) | D = 1] – E [Y0(1) | D = 1], AT̂ET = E [Y(1) | D = 1] – (E [Y(0) | D = 1] + E [Y(1) | D = 0] – E [Y(0) | D = 0]) = или AT̂ET = E [Y(1) | D = 1] – (E [Y(0) | D = 1] – E [Y(1) | D = 0] + E [Y(0) | D = 0]).
218  Метод разности разностей Если мы переставим члены, получим классическую оценку по методу разности разностей. AT̂ET = (E [Y(1) | D = 1] – E [Y(1) | D = 0]) + (E [Y(0) | D = 1] – E [Y(0) | D = 0]). Можно записать нагляднее: Среднее значение результирующей переменной в тестовой группе после осуществления воздействия Среднее значение результирующей переменной в тестовой группе до осуществления воздействия Среднее Среднее значение значение результирующей результирующей переменной переменной в контрольной в контрольной группе после группе до осуществления осуществления воздействия воздействия в тестовой в тестовой – – – – AT̂ET = (Ytreatment, after – Ytreatment, before ) – (Ycontrol, after – Ycontrol, before ). Средняя сумма депозитов в Порту-Алегри после рекламной кампании Средняя сумма депозитов в Порту-Алегри до рекламной кампании Средняя сумма Средняя сумма депозитов во депозитов во Флорианополисе Флорианополисе после рекламной до рекламной кампании кампании в Порту-Алегри в Порту-Алегри Метод получил такое название, потому что из разницы средних значений результирующей переменной в тестовой группе после и до осуществления воздействия мы вычитаем разницу средних значений результирующей переменной в контрольной группе после и до осуществления воздействия. Вот как это выглядит в программном коде. fl_before = data.query("poa==0 & jul==0")["deposits"].mean() diff_in_diff = (poa_after-poa_before)-(fl_after-fl_before) diff_in_diff 6.524557692307688 Метод разности разностей сообщает нам, что мы должны ожидать увеличения средней суммы депозитов на 6.52 бразильского реала. Обратите внимание: предположение, лежащее в основе метода разности разностей, гораздо более правдоподобно, чем предположение двух предыдущих оценок. Оно просто исходит из того, что модель роста для двух городов одинакова. Но для этого не требуется, чтобы оба города были сопоставимы по изначальной средней сумме депозитов, а также не требуется, чтобы тренд был равен нулю. Чтобы визуализировать суть метода разности разностей, мы можем спрое­ цировать тренд, полученный для объектов контрольной группы, на объекты тестовой группы, чтобы увидеть контрфактический результат, то есть увидеть среднюю сумму депозитов, которую мы могли бы ожидать, если бы не было интервенции. plt.figure(figsize=(10,5)) plt.plot(["May", "Jul"], [fl_before, fl_after], label="Флорианополис", lw=2)
Метод разности разностей (difference in differences – DiD)  219 plt.plot(["May", "Jul"], [poa_before, poa_after], label="Порту-Алегри", lw=2) plt.plot(["May", "Jul"], [poa_before, poa_before+(fl_after-fl_before)], label="Контрфактический", lw=2, color="C2", ls="-.") plt.legend(); Видите эту небольшую разницу между красной и желтой пунктирными линиями? Если вы действительно сосредоточитесь, вы увидите небольшой эффект воздействия на сумму депозитов в Порту-Алегри. Теперь вы можете сказать себе: «Насколько я могу доверять этой оценке? Я имею право знать ее стандартную ошибку!» В этом есть смысл, поскольку без них полученные оценки выглядят глупо. Для этого мы воспользуемся изящным трюком, использующим регрессию. В частности, мы оценим следующую линейную модель:
220  Метод разности разностей Yi = β0 + β1POAi + β2 Juli + β3POAi ∗ Juli + ei. Обратите внимание, что β0 является базовым уровнем для контрольной группы. В нашем случае речь идет о средней сумме депозитов во Флорианополисе в мае, т. е. до интервенции. Если мы включим дамми-переменную города, ставшего тестовой группой или подвергнутого воздействию (дамми-переменную POA), то получим β1. В нашем случае тестовой группой является Порту-Алегри. Таким образом, β0 + β1 – это базовый уровень Порту-Алегри, средняя сумма депозитов в Порту-Алегри в мае, т. е. до интервенции, а β1 – это увеличение базового уровня Порту-Алегри по сравнению с Флорианополисом. Если мы выключим дамми-переменную POA и включим дамми-переменную Jul, то получим β0 + β2, что соответствует средней сумме депозитов во Флорианополисе в июле, т. е. после интервенции. β2 – это тренд контрольной группы, который мы прибавляем к базовому уровню, чтобы получить среднюю сумму депозитов для контрольной группы после интервенции. Таким образом, β1 – это приращение, которое мы получаем, переходя от контрольной группы к тестовой, β2 – это приращение, которое мы получаем, переходя от периода до интервенции к периоду после нее. Наконец, если мы включим обе дамми-переменные, то получим β3. β0 + β1 + β2 + β3 – это средняя сумма депозитов в Порту-Алегри после интервенции. Таким образом, β3 – это инкрементальное воздействие, когда мы из мая попадаем в июль и из Флорианополиса попадаем в Порту-Алегри. Другими словами, это оценка разности разностей. Если вы мне не верите, проверьте сами. Вы должны получить то же самое число, которое мы получили выше. А также обратите внимание, что мы получили столь желанные стандартные ошибки. smf.ols('deposits ~ poa*jul', data=data).fit().summary().tables[1] # средняя сумма депозитов во Флорианополисе до интервенции fl_before = data.query("poa==0 & jul==0")["deposits"].mean() diff_poa_vs_fl_before = (data.query("poa==1 & jul==0")["deposits"].mean() data.query("poa==0 & jul==0")["deposits"].mean()) fl_after = data.query("poa==0 & jul==1")["deposits"].mean() # разница между средними суммами депозитов во Флорианополисе # после интервенции и до интервенции diff_fl_after_vs_before = fl_after - fl_before # средняя сумма депозитов в Порту-Алегри после интервенции poa_after = data.query("poa==1 & jul==1")["deposits"].mean()
Непараллельные тренды  221 # разность разностей did = poa_after - (fl_before + diff_poa_vs_fl_before + diff_fl_after_vs_before) print("intercept:", round(fl_before, 4)) print("poa:", round(diff_poa_vs_fl_before, 4)) print("jul:", round(diff_fl_after_vs_before, 4)) print("poa:jul:", round(did, 4)) intercept: 171.6423 poa: -125.6263 jul: 34.5232 poa:jul: 6.5246 Непараллельные тренды Одной из очевидных проблем метода разности разностей является его неспособность удовлетворить условие о параллельности трендов. Если тренд в тестовой группе отличается от тренда в контрольной группе, разность разностей будет смещенной. Это распространенная проблема, возникающая при работе с неслучайными данными, когда решение о выборе региона/области в качестве тестовой группы опирается на его потенциальную способность хорошо реагировать на воздействие или когда воздействие нацелено на регионы, в которых продажи идут не очень хорошо. Возьмем наш маркетинговый пример. Мы решили протестировать рекламные щиты в Порту-Алегри не для того, чтобы проверить эффект от рекламных щитов в целом. Причина заключается в том, что там просто плохо идут продажи. Возможно, там не работает интернет-маркетинг. В этом случае вполне возможно, что рост, который мы увидели бы в Порту-Алегри, не разместив рекламных щитов, был бы ниже того роста, который мы могли бы наблюдать в других городах. Это привело бы к недооценке эффекта от рекламы на щитах. Один из способов проверить это – построить график трендов, используя периоды в прошлом. Например, предположим, что Порту-Алегри демонстрировал небольшой нисходящий тренд, но Флорианополис демонстрировал крутой восходящий тренд. В этом случае визуализация прошедших периодов выявила бы эти тренды, и мы бы убедились, что разность разностей не является надежной оценкой. plt.figure(figsize=(10,5)) x = ["Jan", "Mar", "May", "Jul"] plt.plot(x, [120, 150, fl_before, fl_after], label="Флорианополис", lw=2) plt.plot(x, [60, 50, poa_before, poa_after], label="Порту-Алегри", lw=2) plt.plot(["May", "Jul"], [poa_before, poa_before+(fl_after-fl_before)], label="Контрфактический", lw=2, color="C2", ls="-.") plt.legend();
222  Метод разности разностей В дальнейшем мы увидим, как решить эту проблему с помощью синтетического контроля. Мы воспользуемся данными по нескольким городам, чтобы создать синтетический город, который будет точно соответствовать трендам интересующего города. Ну а пока помните, что вам всегда нужно проверять, наблюдаются ли у вас параллельные тренды при применении разности разностей. И еще одна проблема, о которой стоит упомянуть: вы не сможете вычислить доверительный интервал для оценки, полученной по методу разности разностей, если у вас есть только агрегированные данные. Например, у вас нет данных по каждому клиенту из Флорианополиса или Порту-Алегри. Вмес­то этого у вас есть только средние суммы депозитов до и после интервенции для обоих городов. В этом случае вы все равно сможете оценить
Ключевые идеи  223 причинно-следственный эффект по методу разности разностей, но не будете знать его дисперсию. Это обусловлено тем, что изменчивость ваших данных стирается при агрегировании. Ключевые идеи Мы исследовали метод, широко применяемый для оценки причинно-следственных связей применительно к различным макрообъектам (школам, городам, штатам, странам). В методе разности разностей мы берем результат в тестовой группе до и после воздействия и сравниваем получившийся тренд результирующей переменной с трендом результирующей переменной, вычисленным для контрольной группы. В этой главе мы увидели, как можно применить метод разности разностей для оценки эффекта маркетинговой кампании в конкретном городе. Наконец, мы выяснили, почему метод разности разностей дает сбой, если тренды для тестовой и контрольной групп не одинаковы. Мы также выяснили, почему сравнение различий будет проблематичным, если у нас есть только агрегированные данные.
Глава 14 Панельные данные и фиксированные эффекты В предыдущей главе мы познакомились с очень простым методом разности разностей, в котором у нас были тестовая группа и контрольная группа (город Порту-Аллегри, сокращенно POA, и Флорианополис, сокращенно FLN, соответственно) и только два периода: период до интервенции и период после интервенции. Но что бы произошло, если бы у нас было больше перио­ дов? Или больше групп? Оказывается, такая постановка вопроса настолько часто встречается в анализе причинно-следственных связей, что получила собственное название: панельные данные. Панель – это когда мы повторно наблюдаем за одним и тем же объектом в течение нескольких периодов времени. Ее часто применяют для оценки государственной политики, когда мы можем отслеживать данные по нескольким городам или штатам в течение нескольких лет. Кроме того, она невероятно широко распространена в индустрии, когда компании отслеживают пользовательские данные в течение нескольких недель и месяцев. Чтобы понять, как мы можем использовать такую структуру данных, давайте сначала вернемся к примеру использования метода разности разностей, в котором мы хотели оценить влияние размещения рекламного щита (воздействия) в городе Порту-Алегри (POA). Мы хотим посмотреть, сможет ли такая стратегия офлайн-маркетинга повысить использование наших инвес­ тиционных продуктов. В частности, мы хотим знать, насколько увеличится средняя сумма депозитов, если мы разместим рекламные щиты. В предыдущей главе мы рассматривали метод DiD как стратегию, позволяющую узнать, что произошло бы с суммой депозитов в Порту-Алегри, если бы мы не разместили там рекламные щиты. Контрфактический результат Y0 для Порту-Алегри после интервенции (размещения рекламного щита) можно ввести как сумму депозитов в Порту-Алегри до вмешательства плюс фак-
­ Параллельные тренды  225 тор роста. Этот фактор роста был оценен с помощью контрольного города Флорианополиса (FLN). Вот как мы можем оценить этот контрфактический результат, вспомнив некоторые ранее принятые обозначения: Депозиты Депозиты в Порту-Алегри в Порту-Алегри после интервенции, до если бы интервенция интервенции не произошла Депозиты во Депозиты во Флорианополисе Флорианополисе после интервенции до интервенции в Порту-Алегри в Порту-Алегри где t обозначает время, D – воздействие (поскольку t уже занято), YD(t) – потенциальный результат воздействия D в период t (например, Y0(1) – это результат для контрольной группы в период 1). Теперь если мы применим этот предполагаемый потенциальный результат, то можем восстановить эффект воздействия для Порту-Алегри (ATT) cледующим образом: Депозиты в Порту-Алегри после интервенции Другими словами, эффект от размещения рекламных щитов в Порту-Алег ри – это результат, который мы увидели в Порту-Алегри после размещения рекламных щитов, за вычетом результата, который мы получили бы, не разместив рекламу на щитах. Также помните, что сила метода DiD заключается в том, что для оценки упомянутого контрфактического результата требуется только, чтобы рост сумм депозитов в Порту-Алегри соответствовал росту сумм депозитов во Флорианополисе. Это ключевое предположение о параллельных трендах. Нам определенно следует уделить ему некоторое внимание, потому что в дальнейшем он сыграет очень важную роль. Параллельные тренды Один из способов понять предположение о параллельных (или общих) трендах – это вернуться к предположению о независимости. Если мы вспомним самые ранние главы, предположение о независимости требует, чтобы назначение воздействия не зависело от потенциальных результатов: Yd ⊥ D. Это означает, что мы не должны уделять большее внимание тем объектам, которые демонстрируют более высокий результат (что может вызвать положительное смещение в оценке эффекта) или более низкий результат (что приведет к отрицательному смещению). От абстрактных терминов перейдем
226  Панельные данные и фиксированные эффекты к нашему примеру. Допустим, ваш менеджер по маркетингу решает добавить рекламные щиты только в те города, в которых у клиентов уже очень высокие суммы депозитов. Таким образом, позже он сможет похвастаться тем, что города с рекламными щитами генерируют более высокие суммы депозитов, поэтому, конечно, маркетинговая кампания увенчалась успехом. Оставляя в стороне моральную дискуссию, я думаю, вы уже видите, что данное заявление нарушает предположение о независимости: мы осуществляем воздействие в городах с высокими значениями Y0. Кроме того, помните, что естественным расширением этого предположения является предположение об условной независимости, которое допускает, что потенциальный результат сначала зависит от воздействия, но становится независимым от него, как только мы в качестве условий ставим спутывающие переменные X: Yd ⊥ D | X. Вы уже все это знаете. Но как именно это связано с DiD и предположением о параллельных трендах? Если традиционное предположение о независимости утверждает, что назначение воздействия не может быть связано с уровнями потенциальных результатов, то предположение о параллельных трендах утверждает, что назначение воздействия не может быть связано с ростом потенциальных результатов с течением времени. Фактически один из способов записать предположение о параллельных тенденциях выглядит следующим образом: (Yd(t) – Yd(t – 1)) ⊥ D. Если перевести с математического языка на простой, это предположение означает, что вполне нормально, что мы подвергаем воздействию объекты, которые демонстрируют более высокий или более низкий уровень результирующей переменной. А вот подвергнуть объекты воздействию в зависимости от того, какой темп роста результатов они демонстрируют, мы не можем. В нашем примере с рекламными щитами это означает, что вполне нормально разместить рекламные щиты только в городах с изначально высокими суммами депозитов. Но мы не можем размещать рекламные щиты только в тех городах, где суммы депозитов растут больше всего. Это имеет большой смысл, если мы вспомним, что DiD вводит контрфактический рост в тестовой группе наряду с ростом в контрольной группе. Если тренд, демонстрируемый объектом тестовой группы под контролем, отличается от тренда, демонстрируемого объектом контрольной группы, то у нас проблемы. Контролируйте то, что вы не видите Такие методы, как оценка склонности, линейная регрессия и сопоставление, очень хороши для контроля спутывающих факторов в неслучайных данных,
Контролируйте то, что вы не видите  227 но они полагаются на ключевое предположение: условное отсутствие неизмеряемых спутывающих факторов (conditional unconfoundedness)1: (Y0, Y1) ⊥ T | X. Проще говоря, наши методы требуют, чтобы все спутывающие факторы были известны и измеряемы, таким образом, мы можем учесть их и сделать воздействие в максимальной степени случайным. Одна из основных проблем, связанных с этим, заключается в том, что иногда мы просто не можем измерить спутывающий фактор. Например, возьмем классическую задачу экономики труда: выяснить влияние брака на заработные платы мужчин. В экономике хорошо известен тот факт, что женатые мужчины зарабатывают больше, чем одинокие мужчины. Однако не ясно, является эта связь причинно-следственной или нет. Возможно, более образованные мужчины с большей вероятностью вступят в брак и с большей вероятностью получат высокооплачиваемую работу. Это означает, что образование выступает спутывающим фактором, когда мы оцениваем влияние брака на заработок. Для учета этого спутывающего фактора мы могли бы измерить образование человека, участвующего в исследовании, и обучить модель регрессии, включив образование в качестве предиктора (т. е. проконтролировав образование). Но еще одним спутывающим фактором может быть привлекательность. Возможно, более привлекательные мужчины с большей вероятностью женятся и получат высокооплачиваемую работу. К сожалению, привлекательность, как и интеллект, является плохо измеряемой характеристикой. Это ставит нас в трудную ситуацию, потому что если у нас есть неизмеряемые спутывающие факторы, мы получаем смещение. Один из способов справиться с этой проблемой – использовать инструментальные переменные, как мы уже видели ранее. Но придумать хорошие инструментальные переменные – задача непростая и требует большого творчества. Вместо этого давайте воспользуемся преимуществами наших панельных данных. 1 Ignorability/exchangeability/unconfoundedness (игнорирование / взаимозаменяемость / отсутствие спутывающих факторов) является важнейшим предположением в анализе причинно-следственных связей, поэтому раскроем его чуть подробнее. В контексте выявления причинности, когда мы решаем задачу раскрытия взаимосвязей между переменными в наборе данных, не обязательно делая какиелибо предположения о направлении или силе причинно-следственных связей, ignorability/exchangeability/unconfoundedness – это предположение о том, что не существует неизмеряемых факторов, влияющих как на воздействие, так и на переменную результата, что позволяет выявить причинно-следственные связи, не делая никаких предположений о лежащей в их основе причинно-следственной структуре. В контексте оценки причинности, когда нас интересует количественная оценка причинно-следственного эффекта вмешательства на результат, ignorability/ exchangeability/unconfoundedness – это предположение о том, что вменение воздействия не зависит от потенциальных результатов с учетом наблюдаемых ковариат, и это позволит оценить причинно-следственный эффект, вызванный воздействием, путем «контроля» потенциальных спутывающих факторов. – Прим. перев.
228  Панельные данные и фиксированные эффекты Мы уже видели, как панельные данные позволяют нам заменить предположение об отсутствии спутывающих факторов предположением о параллельных трендах. Но как именно оно помогает бороться с неизмеряемыми спутывающими факторами? Во-первых, давайте посмотрим на причинно-следственный граф, который представляет собой схему, в которой мы повторяем наблюдения во времени. Здесь мы отслеживаем одно и то же наблюдение в течение 4 периодов времени. Брак (воздействие) и доход (результат) со временем меняются. В частности, брак включается (изменяется от 0 до 1) в периодах 3 и 4, и доход увеличивается в эти же периоды. Привлекательность, неизмеряемый спутывающий фактор, одинаков во все периоды (смелое заявление, но разумное, если время наблюдения составляет всего несколько лет). Итак, как мы можем узнать, что причина увеличения дохода связана с браком, а не просто со сбивающей с толку привлекательностью? И что еще более важно, как мы можем проконтролировать этот спутывающий фактор, который нельзя измерить? Привлека­ тельность Брак Брак Брак Брак Доход Доход Доход Доход Т=0 Т=1 Т=2 Т=3 Хитрость заключается в следующем: более пристально рассматривая объект и отслеживая его развитие с течением времени, мы уже контролируем все, что зафиксировано с точки зрения времени (т. е. все, что не зависит от времени). Сюда можно отнести любые фиксированные во времени неизмеряемые спутывающие факторы. Например, на графе выше мы уже знаем, что увеличение дохода с течением времени не может быть связано с увеличением привлекательности просто в силу того, что привлекательность остается прежней (в конце концов, время наблюдения за объектом у нас зафиксировано). Суть в том, что хотя мы не можем проконтролировать привлекательность, поскольку не можем ее измерить, мы все равно можем использовать
­ Контролируйте то, что вы не видите  229 панельную структуру, так что неизмеряемые спутывающие факторы не являются больше проблемой. Еще один способ понять это – подумать об этих фиксированных во времени спутывающих факторах как об атрибутах, специфичных для каждого объекта. Это было бы эквивалентно добавлению промежуточного единичного узла в наш причинно-следственный граф. Теперь обратите внимание, как контроль над самим объектом уже блокирует обходной путь между результатом и любыми ненаблюдаемыми, но фиксированными во времени спутывающими факторами. Привлека тельность ДНК Интеллект Объект Брак Брак Брак Брак Доход Доход Доход Доход Т=0 Т=1 Т=2 Т=3 Подумаем об этом. Мы не можем измерить такие качества, как красота и интеллект, но мы знаем, что эти характеристики не изменяются с течением времени (по крайней мере в течение ограниченного периода наблюдения). Механизм реализации этого контроля очень прост. Все, что нам нужно сделать, – это создать дамми-переменную для каждого объекта и добавить эти дамми-переменные в линейную модель. Именно это мы имеем в виду, когда говорим, что можем проконтролировать самого человека: мы добавляем переменную (в данном случае дамми-переменную), которая обозначает этого конкретного человека. Дамми-переменная описывает индивидуальные эффекты для каждого описываемого объекта, ее значение равно 1 для этого объекта и 0 во всех остальных случаях. Когда мы оцениваем влияние брака на доход, используя дамми-переменную объекта в нашей модели, регрессия
230  Панельные данные и фиксированные эффекты оценивает влияние брака, сохраняя при этом переменную объекта фиксированной. Таким образом, добавление подобных дамми-переменных для объектов приводит нас к построению модели с фиксированными эффектами. Фиксированные эффекты Давайте взглянем на имеющиеся у нас данные. Попробуем оценить влияние брака на доход. Наши данные содержат две переменные married and lwage по нескольким объектам (nr) за несколько лет (year). Обратите внимание, что заработная плата прологарифмирована. Кроме того, у нас есть дополнительные контрольные переменные: количество рабочих часов в этом году, количество лет, потраченных на образование, и т. д. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from matplotlib import style from matplotlib import pyplot as plt import statsmodels.formula.api as smf import graphviz as gr from linearmodels.datasets import wage_panel %matplotlib inline style.use("fivethirtyeight") data = wage_panel.load() data.head() Модель с фиксированными эффектами можно обобщить следующим образом: yit = βXit + γUi + eit, где yit – результат объекта i в момент времени t. Xit – вектор переменных для объекта i в момент времени t. Ui – это набор ненаблюдаемых факторов для
Фиксированные эффекты  231 объекта i. Обратите внимание, что эти ненаблюдаемые факторы не меняются с течением времени, отсюда и отсутствие индекса времени t. Наконец, eit – член ошибки. Применительно к нашему примеру yit – прологарифмированная зарплата, Xit – наблюдаемые переменные, которые изменяются с течением времени, например брак и опыт, и Ui – переменные, которые мы не можем наблюдать (измерить), но они постоянны для каждого человека, например привлекательность и интеллект. Теперь вспомните, я говорил, что воспользоваться панельными данными и моделью с фиксированными эффектами так же просто, как добавить дамми-переменные для объектов. Это правда, но на практике мы так не делаем. Представьте себе набор данных, в котором у нас – миллион клиентов. Если мы добавим по одному бинарному столбцу для каждого клиента, то получим миллион столбцов, что, вероятно, не очень хорошая идея. Вместо этого мы используем прием разбиения линейной регрессии на две отдельные модели. Мы уже видели это раньше, но сейчас самое время подвести итоги. Предположим, у вас есть модель линейной регрессии с набором признаков X1 и с другим набором признаков X2: Ŷ = β̂ 1 X1 + β̂ 2 X2, где X1 и X2 являются матрицами признаков (одна строка на признак и один столбец на наблюдение) и β̂ 1 и β̂ 2 – векторы параметров. Вы можете получить тот же самый вектор параметров β̂ 1, выполнив следующие шаги: 1) регрессируем результирующую переменную y на второй набор признаков ŷ * = γ̂1 X2; 2) регрессируем первый набор признаков на второй набор признаков X̂ 1 = γ̂2 X2; 3) получаем остатки X̃ 1 = X1 – X̂ 1 и ỹ 1 = y1 – ŷ *; 4) регрессируем остатки результирующей переменной на остатки, полученные в результате регрессирования признаков ŷ = β̂ 1 X̃ 1. Параметры этой последней регрессии будут точно такими же, как при обучении модели регрессии со всеми признаками. Но как именно это нам поможет? Что ж, мы можем разбить оценку модели с дамми-переменными для объектов на две части. Во-первых, мы используем дамми-переменные для прогнозирования результирующей переменной и признака. Это шаги 1 и 2 выше. Вспомним, что обучить регрессию по дамми-переменной так же просто, как оценить среднее значение этой дамми-переменной. Если нет, давайте воспользуемся нашими данными, чтобы показать, насколько это верно. Давайте обучим модель, в которой мы спрогнозируем заработную плату как функцию от переменной year. mod = smf.ols("lwage ~ C(year)", data=data).fit() mod.summary().tables[1]
232  Панельные данные и фиксированные эффекты Обратите внимание, что эта модель предсказывает, что средний доход в 1980 году составит 1.3935, в 1981 году – 1.5129 (1.3935 + 0.1194) и т. д. Теперь, если мы посчитаем среднее значение по годам, мы получим точно такой же результат. Вспомним, что базовое значение переменной year, 1980, является константой. Поэтому вам нужно добавить константу к параметрам, вычисленным для остальных годов, чтобы получить среднюю заработную плату за год. data.groupby("year")["lwage"].mean() year 1980 1.393477 1981 1.512867 1982 1.571667 1983 1.619263 1984 1.690295 1985 1.739410 1986 1.799719 1987 1.866479 Name: lwage, dtype: float64 Это означает, что если мы получим среднее значение для каждого человека в нашей панели, мы, по сути, регрессируем дамми-переменную для конкретного человека по остальным переменным. Это порождает следующую процедуру оценки: 1. Cоздаем переменные, зависящие от времени, вычитая среднее значение для конкретного человека: – Yit = Yit – Yi, – Xit = Xit – Xi. 2. Регрессируем Yit на Xit.
Фиксированные эффекты  233 Обратите внимание, когда мы так делаем, ненаблюдаемые факторы Ui исчезают. Поскольку Ui не изменяются с течением времени, мы получаем, что – Ui = Ui. Если у нас – следующая система двух уравнений: Yit = βXit + γUi + eit – – – Yi = βXit + γUi + e–it – и мы вычитаем одно уравнение из другого, мы получаем уравнение – – – (Yit – Yi ) = (βXit – βXit ) + (γUi – γUi ) + (eit – e–it), – – (Yit – Yi ) = β(Xit – Xit ) + (eit – e–it), Yit = βXit + eit, которое удаляет все ненаблюдаемые факторы, не изменяющиеся с течением времени (т. е. являющиеся константами с точки зрения времени). Честно говоря, исчезают не только ненаблюдаемые переменные. Это происходит со всеми переменными, которые не меняются с течением времени. По этой причине вы не можете включать переменные, константные с точки зрения времени, поскольку они будут представлять собой линейную комбинацию дамми-переменных, и модель не будет работать. Xit СМОТРИ НА МЕНЯ Xit Я СЕЙЧАС ТВОИ ДАННЫЕ Чтобы выяснить, какие переменные не изменяются со временем, мы можем сгруппировать наши переменные по объектам и вычислить сумму стандартных отклонений. Если переменная равна нулю, это означает, что переменная не меняется со временем ни для одного из объектов. data.groupby("nr").std().sum() year black exper 1334.971910 0.000000 1334.971910
234  Панельные данные и фиксированные эффекты hisp 0.000000 hours 203098.215649 married 140.372801 educ 0.000000 union 106.512445 lwage 173.929670 expersq 17608.242825 occupation 739.222281 dtype: float64 Нам необходимо удалить дамми-переменные этнической принадлежности black и hisp, поскольку они являются константами. Также нам необходимо убрать образование (educ). Кроме того, мы не будем использовать профессию (occupation), поскольку она, вероятно, опосредует влияние брака на заработную плату (возможно, одинокие мужчины предпочитают профессии, требующие больше времени). Выбрав нужные признаки, мы переходим к построению модели. Чтобы обучить нашу модель с фиксированными эффектами, сначала получим усредненные данные. Для этого сгруппируем все нужные нам переменные по отдельным объектам и вычислим средние значения переменных по каждому объекту. Y = "lwage" T = "married" X = [T, "expersq", "union", "hours"] mean_data = data.groupby("nr")[X+[Y]].mean() mean_data.head() Чтобы выполнить операцию вычитания, индексом датафрейма исходных данных мы назначаем идентификатор объекта nr. Затем мы можем просто вычесть из набора исходных данных набор усредненных данных. demeaned_data = (data .set_index("nr") # индекс – идентификатор объекта [X+[Y]] - mean_data) # вычитаем усредненные данные demeaned_data.head()
Фиксированные эффекты  235 Наконец, мы можем обучить нашу модель с фиксированными эффектами на признаках, зависящих от времени. mod = smf.ols(f"{Y} ~ {'+'.join(X)}", data=demeaned_data).fit() mod.summary().tables[1] Если считать, что модель с фиксированными эффектами устраняет смещение всех опущенных переменных, то можно сказать, что брак увеличивает логарифм заработной платы мужчин на 11 %. Этот результат является высокозначимым. Тонкий момент здесь заключается в том, что для моделей с фиксированными эффектами стандартные ошибки необходимо кластеризовать. Таким образом, вместо того чтобы вычислять все оценки вручную (что хорошо только по педагогическим соображениям), мы можем воспользоваться библиотекой linearmodels и установить для параметра cluster_entity значение True. from linearmodels.panel import PanelOLS mod = PanelOLS.from_formula( "lwage ~ expersq+union+married+hours+EntityEffects", data=data.set_index(["nr", "year"]) ) result = mod.fit(cov_type='clustered', cluster_entity=True) result.summary.tables[1]
236  Панельные данные и фиксированные эффекты Обратите внимание, что оценки параметров идентичны тем, которые мы получили, обучив регрессию на признаках, зависящих от времени. Единственное отличие состоит в том, что стандартные ошибки стали немного больше. Теперь сравним результаты с простой моделью OLS, которая не учитывает временную структуру данных. Для этой модели мы добавляем признаки, не зависящие от времени. mod = smf.ols( "lwage ~ expersq+union+married+hours+black+hisp+educ", data=data).fit() mod.summary().tables[1] Эта модель утверждает, что брак увеличивает заработную плату мужчины на 14 %. Эффект стал немного больше, чем тот, который мы обнаружили в модели с фиксированными эффектами. Это предполагает некоторое смещение, вызванное опущенными переменными, поскольку в модель не были добавлены фиксированные индивидуальные факторы, такие как интеллект и привлекательность. Визуализация фиксированных эффектов Чтобы расширить наше представление о том, как работают модели с фиксированными эффектами, давайте переключимся на другой пример. Предпо-
Визуализация фиксированных эффектов  237 ложим, вы работаете в крупной технологической компании и хотите оценить влияние рекламы на щитах на покупки в приложении. Если вы посмотрите на данные прошлых лет, вы увидите, что отдел маркетинга, как правило, тратит больше средств на размещение рекламных щитов в городах, где уровень покупок ниже. Это вполне логично, верно? Маркетологам не понадобилось бы много тратить на рекламу, если бы продажи стремительно росли. Если вы обучите регрессионную модель на этих данных, то окажется, что более высокие затраты на рекламу приводят к меньшим суммам покупок в приложении, но это обусловлено только тем, что инвестиции в рекламу смещены в пользу регионов с низкими суммами покупок. toy_panel = pd.DataFrame({ "mkt_costs":[5,4,3.5,3, 10,9.5,9,8, 4,3,2,1, 8,7,6,4], "purchase":[12,9,7.5,7, 9,7,6.5,5, 15,14.5,14,13, 11,9.5,8,5], "city":["C0","C0","C0","C0", "C2","C2","C2","C2", "C1","C1","C1","C1", "C3","C3","C3","C3"] }) m = smf.ols("purchase ~ mkt_costs", data=toy_panel).fit() plt.scatter(toy_panel.mkt_costs, toy_panel.purchase) plt.plot(toy_panel.mkt_costs, m.fittedvalues, c="C5", label="Линия регрессии") plt.xlabel("Затраты на маркетинг (в 1000)") plt.ylabel("Сумма покупок в приложении (в 1000)") plt.title("Простая OLS-модель") plt.legend();
238  Панельные данные и фиксированные эффекты Уже зная многое об анализе причинно-следственных связей, вы решаете обучить модель с фиксированными эффектами, добавив в свою модель по дамми-переменной для каждого города. Модель с фиксированными эффектами контролирует индивидуальные характеристики города, которые не меняются с течением времени, поэтому если город менее лоялен к вашему продукту, она это учитывает. Когда вы обучите эту модель, вы наконец увидите, что увеличение затрат на рекламу приводит к увеличению сумм покупок в приложении. fe = smf.ols("purchase ~ mkt_costs + C(city)", data=toy_panel).fit() fe_toy = toy_panel.assign(y_hat=fe.fittedvalues) plt.scatter(toy_panel.mkt_costs, toy_panel.purchase, c=toy_panel.city) for city in fe_toy["city"].unique(): plot_df = fe_toy.query(f"city=='{city}'") plt.plot(plot_df.mkt_costs, plot_df.y_hat, c="C5") plt.title("Модель с фиксированными эффектами") plt.xlabel("Затраты на маркетинг (в 1000)") plt.ylabel("Сумма покупок в приложении (в 1000)"); Рисунок выше иллюстрирует работу модели с фиксированными эффектами. Обратите внимание, что фиксированный эффект подгоняет одну линию регрессии для каждого города. Также обратите внимание, что линии параллельны. Наклон линии – это влияние маркетинговых затрат на покупки в приложении. Таким образом, фиксированный эффект предполагает, что причинно-следственный эффект является постоянным
Фиксированные эффекты для периодов времени  239 для всех объектов, которыми в данном случае являются города. Это может быть недостатком или преимуществом в зависимости от того, как вы на это смотрите. Это недостаток, если вы заинтересованы в поиске причинноследственного эффекта для каждого города. Поскольку модель фиксированных эффектов предполагает, что эффект постоянен для всех объектов, вы не обнаружите никакой разницы с точки зрения причинно-следственного эффекта. Однако если вы хотите оценить общее влияние маркетинга на покупки в приложении, панельная структура данных является очень полезным инструментом, с помощью которого можно исследовать фиксированные эффекты. Фиксированные эффекты для периодов времени Подобно тому, как мы ввели фиксированный эффект для объекта, мы могли бы ввести фиксированный эффект для периода времени. Если добавление дамми-переменной для каждого отдельного объекта контролирует фиксированные характеристики объекта, добавление дамми-переменной для периода времени будет контролировать переменные, которые фиксированы для каждого периода времени, но могут меняться со временем. Одним из примеров такой переменной является инфляция. Цены и зарплаты имеют тенденцию расти со временем, но инфляция в каждый период времени одинакова для всех предприятий. Чтобы привести более конкретный пример, предположим, что количество браков со временем увеличивается. Если бы соотношение уровня зарплаты и количества браков тоже менялось со временем, время стало бы спутывающим фактором. Поскольку инфляция тоже со временем приводит к увеличению зарплаты, некоторая положительная корреляция, которую мы видим между браком и зарплатой, объясняется просто тем, что обе эти переменные увеличиваются со временем. Чтобы исправить это, мы можем добавить по дамми-переменной для каждого периода времени. В библиотеке linearmodels нам нужно добавить TimeEffects в нашу формулу и установить для параметра cluster_time значение True. mod = PanelOLS.from_formula( "lwage ~ expersq+union+married+hours+EntityEffects+TimeEffects", data=data.set_index(["nr", "year"]) ) result = mod.fit(cov_type='clustered', cluster_entity=True, cluster_time=True) result.summary.tables[1]
240  Панельные данные и фиксированные эффекты Когда панельные данные вам не помогут Использование панельных данных и моделей с фиксированными эффектами – чрезвычайно мощный инструмент для анализа причинно-следственных связей. Когда у вас нет случайных данных и хороших инструментальных переменных, фиксированный эффект лучше всего подходит для получения выводов о причинно-следственных связях на основе неэкспериментальных данных. Однако стоит отметить, что это не панацея. Бывают ситуации, когда даже панельные данные вам не помогут. Наиболее очевидная ситуация – наличие спутывающих факторов, которые меняются со временем. Фиксированные эффекты могут устранить только смещение, вызванное факторами, которые остаются постоянными для каждого человека. Например, предположим, вы можете повысить уровень своего интеллекта, читая книги и употребляя много полезных жиров. Это позволит вам получить более высокооплачиваемую работу и жену. Фиксированный эффект не сможет устранить смещение, возникающее в силу спутывающего фактора – неизмеряемого влияния интеллекта, поскольку в этом примере интеллект меняется с течением времени. Я, запускающий модель фиксированных эффектов, чтобы устранить смещение из-за спутывающих факторов, фиксированных во времени Спутывающие факторы: В «Delorean»!
Ключевые идеи  241 Другой, менее очевидный случай, когда фиксированный эффект не срабатывает, – это обратная причинно-следственная связь (reversed causality). Например, предположим, что не брак заставляет вас зарабатывать больше. Вы зарабатываете больше, и это увеличивает ваши шансы вступить в брак. В данном случае окажется, что зарплата и брак имеют положительную корреляцию, но доход стоит на первом месте. Они будут меняться с течением времени и в одном и том же направлении, поэтому фиксированные эффекты не смогут это проконтролировать. Ключевые идеи В этой главе мы увидели, как использовать панельные данные – данные, в которых у нас есть несколько измерений характеристик одних и тех же людей за несколько периодов времени. В этом случае мы можем использовать модель с фиксированными эффектами, которая контролирует объекты, сохраняя фиксированными все отдельные факторы, которые не изменяются с течением времени. Это мощный и очень убедительный способ контроля спутывающих факторов, и он настолько хорош, насколько это возможно при работе с неслучайными данными. Наконец, мы увидели, что фиксированные эффекты – не панацея. Мы видели две ситуации, когда они не работают: когда у нас есть обратная причинно-следственная связь и когда неизмеряемое влияние спутывающего фактора меняется с течением времени.
­  Глава 15 Синтетический контроль Один удивительный математический трюк, позволяющий узнать то, что невозможно узнать Когда мы разбирали метод разности разностей, у нас были данные о клиентах из двух разных городов: Порту-Алегри и Флорианополиса. Данные охватывают два разных периода времени: до и после маркетингового вмешательства в Порту-Алегри с целью увеличения сумм депозитов клиентов. Чтобы оценить эффект воздействия, мы обучили регрессию, которая дала нам оценку по методу разности разностей и ее стандартную ошибку. В той ситуции у нас было много наблюдений, поскольку данные были дез агрегированы. Но что, если все, что у нас есть, – это данные, агрегированные на уровне города? Например, давайте представим: все, что у нас есть, – это средняя сумма депозитов в обоих городах до и после вмешательства. Город Флорианополис Порту-Алегри До 171.64 46.01 После 206.16 87.06 Мы все равно сможем вычислить оценку по методу разности разностей. (E [Y(1) | D = 1] – E [Y(1) | D = 0]) – (E [Y(0) | D = 1] – E [Y(0) | D = 0]) = = (87.06 – 206.16) – (46.01 – 171.64) = 6.53. Однако обратите внимание, что размер выборки здесь равен 4, что также соответствует количеству параметров в нашей модели разности разностей. В этом случае стандартная ошибка не имеет четкого определения, так что же нам делать? Еще одна проблема заключается в том, что Флорианополис может быть не так похож на Порту-Алегри, как нам хотелось бы. Например, Фло-
­ Один удивительный математический трюк, позволяющий узнать то, что невозможно  243 рианополис известен своими прекрасными пляжами и спокойными людьми, а Порту-Алегри более известен своим барбекю и прериями. Проблема здесь заключается в том, что вы никогда не сможете знать наверняка, используете ли вы подходящую контрольную группу. Чтобы обойти эту проблему, мы воспользуемся так называемым «самым важным нововведением в литературе по оценке политики за последние несколько лет» (https://www.aeaweb.org/articles?id=10.1257/jep.31.2.3) – синтетическим контролем (синтетической контрольной группой). В его основе лежит простая, но мощная идея. Нам не нужно искать в тестовой группе какой-либо объект, очень похожий на объект в контрольной группе. Вместо этого мы можем создать свою собственную комбинацию нескольких объектов конт рольной группы, создав, по сути, синтетический контроль. Синтетический контроль настолько эффективен и настолько интуитивен, что о нем даже была опубликована статья, но не в научном журнале, а в Washington Post (https://www.washingtonpost.com/news/ wonk/wp/2015/10/30/how-to-measurethings-in-a-world-of-competing-claims/). import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from matplotlib import style from matplotlib import pyplot as plt import seaborn as sns import statsmodels.formula.api as smf %matplotlib inline pd.set_option("display.max_columns", 6) style.use("fivethirtyeight") Чтобы увидеть его в действии, рассмотрим такую проблему, как оценка влияния налогообложения сигарет на их потребление. Чтобы дать немного контекста, скажу, что этот вопрос долгое время обсуждался в сфере экономики. Одни утверждали, что налоги повысят стоимость сигар и это снизит спрос на них. Другие утверждали, что поскольку сигареты вызывают зависимость, изменение их цены не сильно изменит их спрос. С экономической точки зрения мы бы сказали, что спрос на сигареты неэластичен по цене, а повышение налогов – это всего лишь способ увеличить государственные доходы за счет курильщиков. Чтобы уладить ситуацию, мы рассмотрим некоторые данные по этому вопросу в США. В 1988 году Калифорния приняла знаменитый Закон о налоге на табак и охране здоровья, который стал известен как Положение 99: https://en.wikipedia. org/wiki/1988_California_Proposition_99. «Его основной эффект заключался во введении государственного акцизного налога в размере 25 процентов за пачку на продажу табачных сигарет в пределах Калифорнии, при этом примерно эквивалентные акцизы аналогичным образом взимаются с розничной про-
244  Синтетический контроль дажи других коммерческих табачных изделий, таких как сигары и жевательный табак. Дополнительные ограничения, налагаемые на продажу табака, включают запрет на установку автоматов по продаже сигарет в общественных местах, доступных несовершеннолетним, и запрет на индивидуальную продажу отдельных сигарет. Доходы, полученные в результате принятия закона, были направлены на различные программы охраны окружающей среды и здравоохранения, а также на антитабачную рекламу». Чтобы оценить его эффект, мы можем собрать данные о продажах сигарет в нескольких штатах за несколько лет. В нашем случае мы получили данные с 1970 по 2000 год по 39 штатам. Другие штаты развернули аналогичные программы по борьбе против табака и были исключены из анализа. Вот как выглядят наши данные. cigar = (pd.read_csv("data/smoking.csv") .drop(columns=["lnincome", "beer", "age15to24"])) cigar.query("california").head() У нас есть state в качестве индекса штата, где Калифорния – это номер 3. Наши ковариаты – это retprice, розничная цена сигарет, и cigsale, продажи сигарет в пачках на душу населения. Наша результирующая переменная – это cigsale. Кроме того, у нас есть булевы переменные, california указывает, относится ли наблюдение к штату Калифорния, а after_treatment указывает, относится ли наблюдение к периоду после вмешательства. Давайте построим график продаж сигарет в Калифорнии и других штатах по годам. ax = plt.subplot(1, 1, 1) (cigar .assign(california = np.where(cigar["california"], "Калифорния", "Прочие штаты")) .groupby(["year", "california"]) ["cigsale"] .mean() .reset_index() .pivot("year", "california", "cigsale") .plot(ax=ax, figsize=(10,5)))
У нас есть время  245 plt.vlines(x=1988, ymin=40, ymax=140, linestyle=":", lw=2, label="Положение 99") plt.ylabel("Тренд продаж сигарет") plt.title("Разрыв в продажах сигарет на душу населения (в пачках)") plt.legend(); За период, который охватывают наши данные, жители Калифорнии, очевидно, покупали меньше сигарет, чем в среднем по стране. Кроме того, после 80-х годов наблюдается снижение потребления сигарет. Похоже, что после Положения 99 тенденция к снижению в Калифорнии ускорилась по сравнению с другими штатами, но мы не можем сказать этого наверняка. Это всего лишь предположение, которое мы можем выдвинуть, изучив график. Чтобы ответить на вопрос, оказало ли Предложение 99 влияние на потребление сигарет, мы будем использовать период до вмешательства для создания синтетического контроля. Мы объединим остальные штаты, чтобы создать фейковый штат, который будет очень похож на Калифорнию. Затем мы посмотрим, как поведет себя этот синтетический контроль после вмешательства. У нас есть время Чтобы сделать ситуацию немного более формальной, предположим, что у нас есть J + 1 объектов. Без потери общности предположим, что объект 1 – это объект, на который влияет вмешательство. Объекты J = 2, …, J + 1 представляют собой совокупность объектов, не подвергнутых воздействию, которую мы будем называть «пулом доноров». Кроме того, предположим, что имеющиеся у нас данные охватывают T периодов времени, включая T0 периодов до вмешательства. Для каждого объекта j и периода t мы наблюдаем результат Yjt. Для каждого объекта j и периода t мы определяем YjtN как потенциальный результат без вмешательства и YjtI как потенциальный результат с вмеша-
246  Синтетический контроль тельством. Тогда эффект для объекта, подвергнутого воздействию (запишем объект как j = 1), в момент t для t > T0 определяется как τ1t = Y1tI – Y1tN. Поскольку объект j = 1 – это объект, подвергнутый воздействию, то Y1tI – это фактический результат, а Y1tN – это контрфактический результат. Тогда задача состоит в том, как оценить Y1tN. Обратите внимание, что эффект воздействия задан для каждого периода, а значит, он может меняться с течением времени. Он вовсе не обязательно должен быть мгновенным. Он может накапливаться или рассеиваться. Проще говоря, чтобы оценить эффект воздействия, нам нужно выяснить, что произошло бы с результатом объекта j = 1, если бы этот объект не подвергся воздействию. Y Вмешательство τ1t YjtN YjtI T Чтобы оценить Y1tN, вспомним, что комбинация объектов «донорского пула», возможно, гораздо лучше аппроксимирует характеристики объекта, подвергнутого воздействию, чем любой объект, не подвергнутый воздействию, по отдельности. Таким образом, синтетический контроль определяется как средневзвешенное значение объектов в контрольном пуле. Учитывая веса W = (w2, …, wJ+1), оценка Y1tN, полученная с помощью синтетического контроля, вычисляется по формуле Если от всей этой математики у вас болит голова, вы не одиноки. Но не волнуйтесь, у нас есть множество примеров, которые сделают синтетический контроль более интуитивно понятным. На этот раз представьте, что синтетический контроль – это транспонированная регрессия. Как мы уже знаем, линейная регрессия – это тоже способ получить прогноз в виде средневзвешенного значения переменных. Теперь подумайте о таких регрессиях, как в примере с методом разности разностей, в котором каждая переменная
У нас есть время  247 является дамми-переменной со значениями 0/1 для определенного перио­ да времени. В этом случае регрессию можно представить как следующее матричное умножение: W Y Объекты Время В случае с синтетическим контролем у нас не так много объектов, но зато много периодов времени. Итак, все, что нам нужно сделать, – это транспонировать матрицу входов. Тогда объекты становятся «переменными», и мы представляем результат как средневзвешенное значение объектов, как в следующем матричном умножении. Объекты W Y = Объект 1 t1 Время t2 t3 t4 t5 t6 Если у нас больше одного признака, мы можем сгруппировать их, как на рисунке ниже. Важно, чтобы регрессия попыталась «спрогнозировать» объект 1, подвергнутый воздействию, используя другие объекты. Таким образом, мы можем подобрать веса оптимальным способом для достижения желаемого сходства. Мы даже можем по-разному масштабировать признаки, чтобы придать им разную важность.
248  Синтетический контроль Объекты W Y = Объект 1 Х1 t1 t2 t3 Х2 t4 t5 t6 Итак, если синтетический контроль можно рассматривать как линейную регрессию, это же означает, что мы можем оценить ее веса с помощью МНК, верно? Ага! Собственно, давайте это и сделаем сейчас. Синтетический контроль в виде линейной регрессии Всегда так было Подождите, это все OLS? Чтобы оценить эффект воздействия с помощью синтетического контроля, мы попытаемся создать «фейковый объект», напоминающий объект, подвергнутый воздействию в период до вмешательства. Потом посмотрим, как поведет себя этот «фейковый объект» после вмешательства. Разница между синтетическим контролем и объектом, который этот контроль имитирует, и составляет эффект воздействия. Чтобы сделать это с помощью линейной регрессии, мы найдем веса с помощью МНК. Мы минимизируем квадратичное расстояние между средне-
Синтетический контроль в виде линейной регрессии  249 взвешенным значением объектов донорского пула и объектом, который подвергся воздействию до вмешательства. Первое, что нам нужно, – это преобразовать объекты (в нашем случае объектами являются штаты) в столбцы, а время – в строки. Поскольку у нас – два признака: cigsale и retprice, мы поместим их друг на друга, как на рисунке выше. Мы создадим синтетическую контрольную группу, которая будет очень похожа на Калифорнию в период до вмешательства, и посмотрим, как она будет вести себя в период после вмешательства. По этой причине важно выбирать только период до вмешательства. Здесь признаки, похоже, имеют одинаковый масштаб, поэтому мы не будем с ними ничего делать. Если объекты имеют разные масштабы, один измеряется в тысячах, а другой – в десятичных дробях, то объект большего масштаба будет наиболее важным при минимизации разницы. Чтобы этого избежать, важно сначала привести их к одному масштабу (стандартизировать, или, как еще говорят, масштабировать). features = ["cigsale", "retprice"] inverted = (cigar.query("~after_treatment") # берем период до вмешательства # берем по столбцу для года и по строке для штата .pivot(index='state', columns="year")[features] .T) # транспонируем и получаем по столбцу для штата inverted.head() Теперь нашей переменной Y станет штат Калифорния, а X будут остальные штаты. y = inverted[3].values # штат Калифорния X = inverted.drop(columns=3).values # прочие штаты Затем мы обучаем регрессию. Наличие константы эквивалентно добавлению еще одного штата, в котором каждая строка равна 1. Я не буду включать константу. В итоге мы получим набор весов, которые минимизируют квадратичную разность между объектом, испытавшим воздействие, и объектами, находящимися в пуле доноров.
250  Синтетический контроль from sklearn.linear_model import LinearRegression weights_lr = LinearRegression(fit_intercept=False).fit(X, y).coef_ weights_lr.round(3) array([-0.436, -0.295, -0.25 , 0.243, 0.805, -1.038, 0.052, -0.667, -0.171, -0.318, 0.679, -0.529, -0.106, -0.02 , -1.246, 0.078, 1.235, -0.145, 0.14 , 0.773, 0.339, -0.549, 0.109, -0.811, -0.055, 1.213, 0.143, 0.437, -0.023, 0.242, -0.328, 0.362, 0.519, -0.032]) 0.555, -0.266, 0.594, -0.304, Эти веса показывают нам, как создать синтетическую контрольную группу. В траспонированной таблице мы умножим значения штата 1 на –0.436, значения штата 2 на –1.038, значения штата 4 на 0.679 и т. д. Мы можем добиться этого с помощью скалярного произведения матрицы штатов, находящихся в пуле, и весов. calif_synth_lr = (cigar.query("~california") .pivot(index='year', columns="state")["cigsale"] .values.dot(weights_lr)) Теперь, когда у нас есть синтетическая контрольная группа, мы можем визуализировать ее с помощью значений результирующей переменной для штата Калифорния. plt.figure(figsize=(10,6)) plt.plot(cigar.query("california")["year"], cigar.query("california")["cigsale"], label="Калифорния") plt.plot(cigar.query("california")["year"], calif_synth_lr, label="Синтетический контроль") plt.vlines(x=1988, ymin=40, ymax=140, linestyle=":", lw=2, label="Положение 99") plt.ylabel("Разрыв в продажах сигарет\nна душу населения (в пачках)") plt.legend();
­ Не экстраполируйте  251 Кажется, что-то пошло не так. Что привлекает ваше внимание на этой картинке? Во-первых, после вмешательства в синтетической контрольной группе продается больше сигарет, чем в Калифорнии. Это свидетельствует о том, что вмешательство помогло снизить спрос на сигареты. Во-вторых, обратите внимание, насколько идеально смоделирован период до вмешательства. Синтетическая контрольная группа в точности соответствует штату Калифорния. Это признак того, что наша модель синтетической контрольной группы, вероятно, переобучена. Другим признаком является огромная дисперсия результирующей переменной для синтетической контрольной группы после вмешательства. Обратите внимание, что значения переменной не меняются плавным образом. Вместо этого они идут то вверх, то вниз, то вверх, то вниз. После вмешательства До вмешательства Если задуматься, почему это происходит, вспомним, что в нашем пуле доноров находится 38 штатов. Таким образом, у нашей линейной регрессии – 38 параметров, с которыми можно экспериментировать, чтобы пул до воздействия максимально соответствовал тестовой группе. Это тот случай, когда T (количество строк – периодов) и N (количество столбцов – объектов) тоже велико, что придает нашей модели линейной регрессии слишком большую сложность и соответственно гибкость. Если вы знакомы с регуляризованными моделями, вы можете воспользоваться гребневой регрессией или LASSO-регрессией, чтобы исправить это. Здесь мы рассмотрим еще один, более традиционный способ избежать переобучения. Не экстраполируйте Предположим, у вас есть данные, аналогичные приведенным в таблице ниже, и вас просят построить синтетическую контрольную группу, чтобы воспроизвести объект, подвергнутый воздействию (тестовый объект), используя любую линейную комбинацию объектов, не подвергнутых воздействию (конт рольных объектов).
252  Синтетический контроль Объект Объем продаж Цена Поскольку необходимо сопоставить 3 объекта и только 2 атрибута, существует несколько точных решений этой проблемы, но хорошее решение состоит в том, чтобы умножить атрибуты первого контрольного объекта на 2.25, умножить атрибуты второго контрольного объекта на –2 и сложить оба. Обратите внимание, как для контрольного объекта 2 операция умножения создает фейковый объект с объемом продаж –16 (8 × –2) и ценой –8 (4 × –2). Это умножение экстраполирует контрольный объект 2 на область данных, которая не имеет особого смысла, поскольку отрицательные объем продаж и цена практически невозможны. Операция умножения для контрольного объекта 1 тоже является экстраполяцией, поскольку она переносит объект в область, в которой продажи и цена равны 18 (8 × 2.25). Эти цифры намного превышают все имеющиеся у нас данные, отсюда и экстраполяция. Именно это и делает регрессия, когда мы просим ее создать синтетическую контрольную группу. Технически экстраполяция не является ошибкой, но на практике она опасна. Мы выдвигаем предположение о том, что данные, которые никогда не видели, ведут себя так же, как и имеющиеся у нас данные. Один из способов начать более безопасную игру – ограничить наш синтетический контроль только интерполяцией. Для этого мы ограничим веса положительными значениями, и в сумме они должны давать единицу. Теперь синтетическим контролем будет выпуклая комбинация объектов донорского пула. При выполнении интерполяции мы спроецируем объект, подвергнутый воздействию, на выпуклую оболочку, определяемую объектом, который не подвергался воздействию, как на рисунке ниже. Экстраполяция Control 1 Control 2 cigsale cigsale Control 2 Интерполяция Control 3 Control 1 Control 3 Treated retprice Treated retprice
Не экстраполируйте  253 Обратите внимание на две вещи. Во-первых, в этом случае интерполяция не сможет обеспечить идеальное совпадение с объектом, подвергнутым воздействию. Это связано с тем, что объект, подвергнутый воздействию, – это объект с наименьшим объемом продаж и самой высокой ценой. Выпуклые комбинации могут в точности воспроизводить только те признаки, которые находятся между контрольными объектами. Еще одна вещь, на которую следует обратить внимание, – разреженность интерполяции. Мы проецируем объект, подвергнутый воздействию, на поверхность выпуклой оболочки, которая задана всего несколькими объектами. По этой причине интерполяция присвоит нулевой вес многим объектам. Такова общая идея, а теперь давайте ее немного формализуем. Синтетический контроль по-прежнему определяется как но теперь мы воспользуемся весами W = (w2, …, wJ+1), которые минимизируют с учетом ограничения, заключающегося в том, что w2, …, wJ+1 положительны и в сумме равны единице. Обратите внимание, что v отражает важность каждой переменной при минимизации разницы между объектом, подвергнутым воздействию, и синтетической контрольной группой. Различные важности v приведут к разным оптимальным весам. Один из способов выбрать V – сделать так, чтобы каждая переменная имела нулевое среднее значение и единичную дисперсию. Более сложный способ – выбрать V таким образом, чтобы переменные, помогающие лучше предсказать Y, получили более высокую важность. Поскольку мы хотим сохранить простоту программного кода, мы просто будем считать, что все переменные имеют одинаковую важность. Чтобы реализовать нашу идею, сначала напишем вышеприведенную функцию потерь. from typing import List from operator import add from toolz import reduce, partial def loss_w(W, X, y) -> float: return np.sqrt(np.mean((y - X.dot(W))**2)) Поскольку мы используем одинаковую важность для каждого признака, нам не нужно беспокоиться о v. Теперь, чтобы получить оптимальные веса, мы выполним оптимизацию на основе квадратичного программирования, воспользовавшись библиотекой SciPy. Мы ограничим сумму весов единицей с помощью lambda x: np.sum(x) – 1.
254  Синтетический контроль Кроме того, ограничим значения переменных, участвующих в оптимизации, диапазоном от 0 до 1. from scipy.optimize import fmin_slsqp def get_w(X, y): w_start = [1/X.shape[1]] * X.shape[1] weights = fmin_slsqp(partial(loss_w, X=X, y=y), np.array(w_start), f_eqcons=lambda x: np.sum(x) - 1, bounds=[(0.0, 1.0)] * len(w_start), disp=False) return weights Реализовав функцию вычисления весов, давайте получим веса, которые задают синтетическую контрольную группу. calif_weights = get_w(X, y) print("Сумма:", calif_weights.sum()) np.round(calif_weights, 4) Сумма: 1.000000000000424 array([0. , 0. , 0. , 0. , 0.2401, 0. 0. 0. 0. 0. , , , , , 0. 0. 0. 0. 0. , , , , , 0.0852, 0. , 0.113 , 0. , 0. , 0. , 0. , 0.1051, 0. , 0. , 0. , 0. 0. , 0. 0.4566, 0. 0. , 0. 0. ]) , , , , 0. 0. 0. 0. , , , , Используя полученные веса, мы умножаем штаты 1, 2 и 3 на ноль, штат 4 на 0.0852 и т. д. Обратите внимание на разреженность весов, что мы и предсказывали. Кроме того, сумма всех весов равна единице и находится в диапазоне от 0 до 1, что удовлетворяет нашему ограничению на выпуклую комбинацию. Теперь, чтобы получить синтетическую контрольную группу, мы можем умножить эти веса на штаты точно так же, как мы делали это раньше с весами регрессии. calif_synth = cigar.query("~california").pivot( index='year', columns="state")["cigsale"].values.dot(calif_weights) Если мы сейчас построим график результатов для синтетической конт­ рольной группы, то получим гораздо более плавный тренд. Кроме того, обратите внимание, что в период до вмешательства синтетическая контрольная группа больше не воспроизводит в точности объект, подвергнутый воздействию. Это хороший знак, поскольку он указывает на то, что наша модель не переобучена.
Не экстраполируйте  255 plt.figure(figsize=(10,6)) plt.plot(cigar.query("california")["year"], cigar.query("california")["cigsale"], label="Калифорния") plt.plot(cigar.query("california")["year"], calif_synth, label="Синтетический контроль") plt.vlines(x=1988, ymin=40, ymax=140, linestyle=":", lw=2, label="Положение 99") plt.ylabel("Продажи сигарет\nна душу населения (в пачках)") plt.legend(); Имея под рукой синтетическую контрольную группу, мы можем оценить эффект воздействия как разницу между результатом объекта, подвергнутого воздействию (тестовой группы), и результатом синтетической контрольной группы: τ1t = YjtI – YjtN. В нашем случае эффект со временем становится все больше и больше. plt.figure(figsize=(10,6)) plt.plot(cigar.query("california")["year"], cigar.query("california")["cigsale"] - calif_synth, label="Эффект Калифорнии") plt.vlines(x=1988, ymin=-30, ymax=7, linestyle=":", lw=2, label="Положение 99") plt.hlines(y=0, xmin=1970, xmax=2000, lw=2) plt.title("State - synthetic, по годам") plt.ylabel("Разрыв в продажах сигарет\nна душу населения (в пачках)") plt.legend();
256  Синтетический контроль Похоже, что к 2000 году Положение 99 сократило продажи сигарет на 25 пачек (в расчете на душу населения). Это очень круто и все такое, но вы, возможно, задаете себе вопрос: как я могу узнать, является ли полученная оценка статистически значимой? Делаем вывод Поскольку размер нашей выборки является очень маленьким (39), нам надо чуть поумнеть, чтобы выяснить, является ли наш результат статистически значимым, а не просто является удачной случайностью. Здесь мы воспользуемся идеей точного теста Фишера, его суть очень проста. Мы переставляем местами объект, подвергнутый воздействию, и объекты контрольной группы. В итоге для каждого штата мы получим свою синтетическую контрольную группу и оценку эффекта. Итак, мы делаем вид, что воздействие на самом деле осуществлялось в другом штате, а не в Калифорнии, и смотрим, каков
Делаем вывод  257 был бы эффект от этого воздействия, которого не было. Затем мы посмотрим, является ли воздействие в Калифорнии достаточно большим по сравнению с остальными фейковыми воздействиями. Идея состоит в том, что если мы применительно к штатам, которые на самом деле не подвергались воздействию (т. е. в них не было принято Положение 99), притворимся, что они якобы испытали воздействие, мы не сможем обнаружить какого-либо значительного эффекта воздействия. Чтобы реализовать вышесказанное, я написал функцию, которая принимает на вход штат и создает синтетическую контрольную группу для этого штата. Эта функция возвращает датафрейм с 5 столбцами. Этими столбцами являются штат (state), год (year), объем продаж сигарет, т. е. фактическая результирующая переменная для данного штата (cigsale), индикатор до/после вмешательства (after_treatment), синтетическая результирующая переменная для данного штата (syntetic). def synthetic_control(state: int, data: pd.DataFrame) -> np.array: features = ["cigsale", "retprice"] inverted = (data.query("~after_treatment") .pivot(index='state', columns="year")[features] .T) # y # X объект, подвергнутый воздействию = inverted[state].values пул доноров = inverted.drop(columns=state).values weights = get_w(X, y) synthetic = (data.query(f"~(state=={state})") .pivot(index='year', columns="state")["cigsale"] .values.dot(weights)) return (data .query(f"state=={state}")[ ["state", "year", "cigsale", "after_treatment"] ].assign(synthetic=synthetic)) Посмотрим результаты для штата 1. synthetic_control(1, cigar).head()
258  Синтетический контроль Чтобы получить результаты по всем штатам, мы распараллеливаем вычисления по 8 ядрам. Если ваш компьютер имеет большее или меньшее число ядер, вы можете задать другое число. Следующий программный код вернет список датафреймов, аналогичных вышеприведенному. from joblib import Parallel, delayed control_pool = cigar["state"].unique() parallel_fn = delayed(partial(synthetic_control, data=cigar)) synthetic_states = Parallel(n_jobs=8)( parallel_fn(state) for state in control_pool ) synthetic_states[0].head() Используя синтетическую контрольную группу для всех штатов, мы можем оценить по каждому штату гэп между синтетической результирующей переменной и фактической результирующей переменной. Для Калифорнии этот гэп и будет эффектом воздействия. Для других штатов он похож на эффект плацебо, когда мы оцениваем эффект воздействия в штате, в котором воздействия на самом деле не было. Если мы построим график всех эффектов плацебо вместе с эффектом воздействия в Калифорнии, то получим следующий график. plt.figure(figsize=(12,7)) for state in synthetic_states: plt.plot(state["year"], state["cigsale"] - state["synthetic"], color="C5",alpha=0.4) plt.plot(cigar.query("california")["year"], cigar.query("california")["cigsale"] - calif_synth, label="Калифорния"); plt.vlines(x=1988, ymin=-50, ymax=120, linestyle=":", lw=2, label="Положение 99") plt.hlines(y=0, xmin=1970, xmax=2000, lw=3) plt.ylabel("Разрыв в продажах сигарет\nна душу населения (в пачках)") plt.title("State - synthetic, по годам") plt.legend();
Делаем вывод  259 В глаза бросаются два аспекта этого графика. Во-первых, мы видим, что дисперсия после вмешательства выше, чем дисперсия до вмешательства. Это ожидаемо, поскольку синтетический контроль предназначен для минимизации разницы в периоде до вмешательства. Еще один интересный аспект заключается в том, что есть некоторые объекты, которые мы не можем хорошо смоделировать даже для периода до вмешательства. Этого тоже следовало ожидать. Например, если в каком-то штате наблюдается очень высокий уровень потребления сигарет, никакая выпуклая комбинация других штатов никогда не сможет смоделировать его. Поскольку такие объекты плохо смоделированы, их рекомендуется исключить из анализа. Один из способов сделать это объективно – установить порог допустимой ошибки для периода до вмешательства и удалить объекты с высокой ошибкой. Если мы поступим таким образом и построим тот же самый график, то получим следующую картину. def pre_treatment_error(state): pre_treat_error = ( state.query("~after_treatment")["cigsale"] - state.query("~after_treatment")["synthetic"] ) ** 2 return pre_treat_error.mean() plt.figure(figsize=(12,7)) for state in synthetic_states: # удаляем объекты с MSE выше 80 if pre_treatment_error(state) < 80: plt.plot(state["year"], state["cigsale"] - state["synthetic"], color="C5",alpha=0.4)
260  Синтетический контроль plt.plot(cigar.query("california")["year"], cigar.query("california")["cigsale"] - calif_synth, label="Калифорния"); plt.vlines(x=1988, ymin=-50, ymax=120, linestyle=":", lw=2, label="Положение 99") plt.hlines(y=0, xmin=1970, xmax=2000, lw=3) plt.ylabel("Разрыв в продажах сигарет\nна душу населения (в пачках)") plt.title("State - synthetic, по годам\n(штаты с большими ошибками удалены)") plt.legend(); Удалив шум, мы можем увидеть, насколько экстремальным является размер эффекта в штате Калифорния. Этот рисунок показывает нам, что если бы мы притворились, что воздействие имело место в любом другом штате, мы почти никогда не получили бы столь экстремального эффекта, как тот, который получили в Калифорнии. Сам по себе этот график является формой вывода, но мы еще можем на основе данных результатов вывести p-значение. Все, что нам нужно сделать, – это посмотреть, насколько полученные нами эффекты ниже эффекта Калифорнии. effects = [state.query("year==2000").iloc[0]["cigsale"] - state.query("year==2000").iloc[0]["synthetic"] for state in synthetic_states if pre_treatment_error(state) < 80] # удаляем шум calif_effect = cigar.query( "california & year==2000").iloc[0]["cigsale"] - calif_synth[-1] print("Эффект воздействия для Калифорнии на 2000 год:", calif_effect) np.array(effects) Эффект воздействия для Калифорнии на 2000 год: -24.83015975607075
Делаем вывод  261 array([ 5.79715888, -10.92204862, -18.4579509 , 10.49627373, 14.02322406, -2.12402707, 4.25211766, 12.6495598 , 24.69067376, 0.89458999, 37.1164056 , 21.13366447, -11.67012367, 8.25002715, -7.42865061, -17.75844576, -17.47677515, 10.36299574, -24.83015976, -7.16628124, -15.0697171 , -0.49805136, 12.57782738, -1.47547826, 4.2985083 , 8.04811402, 0.32576355, -8.40826877, 2.96157562, 24.10478092, 7.93334017, 2.81640133, -25.1604094 , -12.26469132, -8.59880332]) Если мы хотим проверить одностороннюю гипотезу о том, что эффект в Калифорнии ниже нуля, мы можем оценить p-значение как долю случаев, когда эффект в Калифорнии превышает все оцененные эффекты. Как оказалось, эффект воздействия для Калифорнии в 2000 году составил –24.8, а это означает, что вмешательство снизило потребление сигарет почти на 25 пачек. Из всех остальных 34 оцененных нами эффектов плацебо только один превосходит эффект, обнаруженный нами в Калифорнии. Таким образом, p-значение будет равно 1/35. np.mean(np.array(effects) < calif_effect) 0.02857142857142857 Наконец, мы можем показать распределение эффектов, просто чтобы понять, насколько эффект в Калифорнии на самом деле превышает остальные эффекты. _, bins, _ = plt.hist(effects, bins=20, color="C5", alpha=0.5); plt.hist([calif_effect], bins=bins, color="C0", label="Калифорния") plt.ylabel("Частота") plt.title("Распределение эффектов") plt.legend();
262  Синтетический контроль Ключевые идеи Мы выяснили, что если в нашем распоряжении есть только агрегированные данные о таких объектах, как города или штаты, метод разности разностей не позволит нам сделать вывод. Кроме того, у этого метода есть и некоторые другие ограничения, поскольку он требует задать контрольный объект (объект, не подвергнутый воздействию), а один контрольный объект может не очень адекватно отражать контрфактический результат для объекта, подвергнутого воздействию (тестового объекта). Для устранения этих ограничений мы можем создать синтетическую конт­ рольную группу, состоящую из нескольких контрольных объектов и по характеристикам похожую на тестовый объект. С помощью этой синтетической контрольной группы мы выясним, что произошло бы с нашим объектом, подвергнутым воздействию, в случае отсутствия воздействия. Наконец, мы увидели, как можно использовать точные критерии Фишера для получения вывода в случае использования синтетического контроля. А именно мы сделали вид, что контрольные объекты (объекты, не подвергнутые воздействию) на самом деле были объектом, подвергнутым воздействию, и вычислили эффект. Это были эффекты плацебо – эффекты, которые мы наблюдаем даже без воздействия. Мы используем их для проверки статистической значимости оцененного эффекта воздействия.
Глава 16 Разрывной регрессионный дизайн Мы не задумываемся об этом, но плавность природы впечатляет. Вы не можете вырастить дерево, если еще не распустились почки, вы не можете телепортироваться из одного места в другое, ране требуется время, чтобы зажить. Даже в социальной сфере плавность кажется нормой. Вы не можете создать бизнес за один день, для достижения богатства необходимы последовательность и упорный труд, и потребуются годы, прежде чем вы поймете, как работает линейная регрессия. В обычных обстоятельствах природа является очень сплоченной и нечасто прыгает с места на место. Когда разумная и животная души объединены одним объятием, их можно удержать от разлучения. – «Дао Дэ Цзин», Лао-цзы Это означает, что когда мы видим скачки и всплески, они, скорее всего, являются искусственными и часто рукотворными ситуациями. Эти события обычно сопровождаются событиями, противоречащими обычному порядку вещей: если происходит что-то странное, это дает нам некоторое представление о том, что произошло бы, если бы природа действовала подругому. Исследование этих искусственных скачков лежит в основе разрывного регрессионного дизайна (regression discontinuity design – RDD). Природа: «Посмотри на всю симметрию и плавность, которые я создала». Человечество:
264  Разрывной регрессионный дизайн Представьте, что у вас есть переменная воздействия T и потенциальные результаты Y0 и Y1. Воздействие T является разрывной функцией наблюдаемой переменной отбора1 R такой, что D i = 1{Ri > c}. Другими словами, это означает, что воздействие равно нулю, когда R ниже порога c, и единице в противном случае. Это означает, что мы можем наблюдать Y1, когда R > c, и Y0, когда R < c. Чтобы понять суть вышесказанного, подумайте о потенциальных результатах как о двух функциях, которые мы не можем наблюдать полностью. Обе функции Y0(R) и Y1(R) существуют, мы просто их не видим. Порог работает как переключатель, который позволяет нам увидеть только одну из этих функций, но никогда не позволит нам увидеть обе функции одновременно, как на рисунке ниже: Рис. 1. Вероятности назначения (SRD) Рис. 2. Регрессия потенциальных и наблюдаемых результатов Идея разрывной регрессии состоит в том, чтобы сравнить результат чуть выше и чуть ниже порога, дабы определить эффект воздействия на уровне порогового значения. Это называется четким (sharp) разрывным дизайном, поскольку вероятность воздействия меняется с 0 на 1 в пороговой точке, но мы могли бы также подумать о нечетком (fuzzy) разрывном дизайне, в котором вероятность изменения воздействия в пороговой точке всегда меньше 1. Алкоголь убивает вас? Очень актуальным вопросом государственной политики является вопрос о том, каким должен быть минимальный возраст употребления алкоголя. 1 Переменную отбора (selection variable) в англоязычной литературе еще называют «running variable» и «forcing variable». – Прим. перев.
Алкоголь убивает вас  265 В большинстве стран, включая Бразилию, этот возраст установлен на уровне 18 лет, но в США (в большинстве штатов) в настоящее время он составляет 21 год. Итак, значит ли это, что США слишком осторожны и им следует снизить минимальный возраст употребления алкоголя? Или другим странам следует повысить возраст, начиная с которого разрешено употреблять алкоголь? Один из способов взглянуть на этот вопрос – посмотреть на проблему с точки зрения уровня смертности (Carpenter and Dobkin, 2009) https://www. aeaweb.org/articles?id=10.1257/app.1.1.164. В свете государственной политики можно утверждать, что мы должны максимально снизить уровень смертности. Если употребление алкоголя значительно увеличивает уровень смертности, мы не должны снижать минимальный возраст употребления алкоголя. Это соответствовало бы цели снижения смертности, вызванной употреблением алкоголя. Чтобы оценить влияние алкоголя на смертность, мы могли бы использовать тот факт, что возраст, с которого разрешено употребление алкоголя, по своей природе имеет точку разрыва. В США люди моложе 21 года не пьют (или пьют гораздо меньше), а те, кому чуть старше 21 года, пьют регулярно. Это означает, что вероятность употребления алкоголя резко возрастает в 21 год, и именно это мы можем изучить с помощью разрывного регрессионного дизайна. Кроме того, можно сказать, что достижение 21 года увеличивает вероятность употребления алкоголя, поскольку пить его можно и до этого возраста, хотя и нелегально. Технически это соответствует нечеткому разрывному регрессионному дизайну, который мы рассмотрим в этой главе чуть позже. import warnings warnings.filterwarnings('ignore') import pandas as pd import numpy as np from matplotlib import style from matplotlib import pyplot as plt import seaborn as sns import statsmodels.formula.api as smf %matplotlib inline style.use("fivethirtyeight") У нас есть некоторые данные о смертности, агрегированные по возрасту. В каждой строке указан средний возраст группы людей и средний уровень смертности от всех причин (all), от дорожно-транспортных происшествий (mva) и от самоубийств (suicide). drinking = pd.read_csv("data/drinking.csv") drinking.head()[["agecell", "all", "mva", "suicide"]]
266  Разрывной регрессионный дизайн Для лучшей наглядности (и в силу еще одной важной причины, которую мы увидим позже) мы вычтем из переменной отбора agecell пороговое значение 21. По сути, мы выполняем центрирование переменной agecell в 0, поскольку 21 является средним значением этой переменной. Если мы визуализируем несколько результирующих переменных (all, mva, suicide), отложив переменную отбора по оси X, мы получим некоторое визуальное свидетельство о своеобразном скачке смертности, по мере того как мы пересекаем возраст, разрешенный для употребления алкоголя. plt.figure(figsize=(8,8)) ax = plt.subplot(3,1,1) drinking.plot.scatter(x="agecell", y="all", ax=ax) plt.title("Причины смертности по возрасту (центрировано в 0)") ax = plt.subplot(3,1,2, sharex=ax) drinking.plot.scatter(x="agecell", y="mva", ax=ax) ax = plt.subplot(3,1,3, sharex=ax) drinking.plot.scatter(x="agecell", y="suicide", ax=ax);
Оценка RDD  267 Есть некоторые догадки, но нам нужно нечто большее. Как именно употребление алкоголя влияет на смертность в области порогового значения? И какова стандартная ошибка оценки эффекта? Оценка RDD Ключевое предположение, на которое опирается разрывной регрессионный дизайн (RDD), – это плавность изменения потенциального результата в пороговом значении. Формально это означает, что предельные значения потенциальных результатов при приближении переменной отбора к порогу с обеих сторон (слева и справа) должны быть одинаковыми: Если это так, то мы можем оценить причинно-следственный эффект для порогового значения: Это своего рода локальный средний эффект воздействия (LATE), поскольку мы можем вычислить его только для порогового значения. В этом контексте мы можем рассматривать RDD как локальное рандомизированное исследование. Для объектов в пороговом значении воздействие могло пойти в любую сторону, и случайным образом некоторые люди оказались ниже порога, а некоторые – выше. В нашем примере в один и тот же момент времени некоторым людям чуть больше 21 года, а некоторым чуть меньше 21. Ктото родился на несколько дней позже, кто-то на несколько дней раньше, что довольно случайно. По этой причине RDD предлагает очень убедительную причинно-следственную историю. Он не является «золотым стандартом», которым считаются рандомизированные контролируемые испытания или РКИ, но довольно близок к нему. Теперь, чтобы оценить эффект воздействия в пороговом значении, все, что нам нужно сделать, – это оценить оба предела в вышеприведенной формуле и сравнить их. Самый простой способ сделать это – обучить линейную регрессию.
268  Разрывной регрессионный дизайн ТЫ ОЖИДАЛ КАКУЮ-ТО ДРУГУЮ КРУТУЮ МОДЕЛЬ НО ЭТО БЫЛА Я, OLS! Чтобы линейная регрессия сработала, мы создаем взаимодействие даммипеременной (дамми-переменная – есть превышение порогового значения или нет) и переменной отбора: yi = β0 + β1ri + β21{ri > c} + β31{ri > c}ri. По сути, это то же самое, что обучить две линейные регрессии: одну – для данных выше порога и другую – для данных ниже порога. Параметр β0 – конс­ танта регрессии, обученной на данных ниже порога, а β0 + β2 – это константа регрессии, обученной на данных выше порога. Здесь в игру вступает трюк с центрированием переменной отбора в пороговом значении. После центрирования пороговое значение становится нулевым. Это приводит к тому, что константа β0 становится спрогнозированным значением для порогового значения в регрессии, обученной на данных ниже порога. Другими словами, β0 = , константа прогнозирует результат в точке порога для тех наблюдений, которые находятся ниже порога. По тем же соображениям, β0 + β2 – это предельное значение результирующей переменной в регрессии, обученной на данных выше порога. Это означает, что Ниже приведен программный код для случая, когда мы хотим оценить влия­ние потребления алкоголя на смертность от всех причин в возрасте 21 года. rdd_df = drinking.assign(threshold=(drinking["agecell"] > 0).astype(int)) model = smf.wls("all~agecell*threshold", rdd_df).fit() model.summary().tables[1]
Оценка RDD  269 Эта модель говорит нам, что смертность увеличивается на 7.6627 пункта при употреблении алкоголя. Другими словами, алкоголь увеличивает вероятность смерти от всех причин на 8 % (100*((7.6627+93.6184)/93.6184 – 1)). Обратите внимание, что модель позволяет нам вычислить стандартные ошибки для оценки причинно-следственного эффекта. В данном случае эффект статистически значим, поскольку p-значение ниже 0.01. Если мы хотим визуализировать результаты модели, мы можем показать значения, спрогнозированные на основе имеющихся у нас данных. Мы видим, что у нас – две модели регрессии: одна – для наблюдений, которые выше порога, а другая – для наблюдений ниже порога. ax = drinking.plot.scatter(x="agecell", y="all", color="C0") drinking.assign(predictions=model.fittedvalues).plot( x="agecell", y="predictions", ax=ax, color="C1" ) plt.title("Регрессия с разрывом"); Если мы построим отдельную модель для каждой результирующей переменной (вида смертности), то получим следующие результаты.
270  Разрывной регрессионный дизайн plt.figure(figsize=(8,8)) for p, cause in enumerate(["all", "mva", "suicide"], 1): ax = plt.subplot(3,1,p) drinking.plot.scatter(x="agecell", y=cause, ax=ax) m = smf.wls(f"{cause}~agecell*threshold", rdd_df).fit() ate_pct = 100*((m.params["threshold"] + m.params["Intercept"]) / m.params["Intercept"] - 1) drinking.assign(predictions=m.fittedvalues).plot( x="agecell", y="predictions", ax=ax, color="C1" ) plt.title(f"Влияние алкоголя на смертность: {np.round(ate_pct, 2)}%") plt.tight_layout() RDD сообщает нам, что алкоголь увеличивает вероятность смерти в результате самоубийства и автомобильных аварий на 15 %, что является довольно значительной цифрой. Эти результаты являются убедительными аргументами в пользу того, чтобы не снижать возраст употребления алкоголя, если мы хотим минимизировать уровень смертности.
­ Взвешивание с помощью ядерной функции  271 Взвешивание с помощью ядерной функции Регрессия с разрывом во многом зависит от экстраполяционных свойств линейной регрессии. Поскольку мы смотрим на значения в начале и конце двух линий регрессии, нам нужно правильно вычислить эти предельные значения. Возможна ситуация, когда регрессия за счет плохой подгонки в области порогового значения может слишком сильно сфокусироваться на подгонке остальных точек данных. Если это произойдет, мы можем получить неправильную оценку эффекта воздействия. Одним из способов решения этой проблемы является присвоение более высоких весов точкам, расположенным ближе к пороговому значению. Есть много способов сделать это, но наиболее популярный из них – повторное взвешивание наблюдений с помощью треугольного ядра (triangular kernel). Первая часть этого ядра является индикаторной функцией того, близко ли мы находимся от порога. R – это вектор, представляющий собой расстояния между точками данных и порогом. c – это порог. h – это параметр ширины полосы, определяющий, насколько быстро вес убывает с увеличением расстоя ния до порога. Вторая часть этого ядра представляет собой взвешивающую функцию. По мере удаления от порога веса становятся все меньше и меньше. Эти веса делятся на ширину полосы. Если ширина полосы является большой, веса уменьшаются медленнее. Если ширина полосы является маленькой, веса быстро стремятся к нулю. В итоге умножение индикатора на вес треугольного ядра создает финальный весовой вектор, где точки, близкие к порогу, получают более высокий вес, а точки, находящиеся дальше, получают меньший вес. Для лучшего понимания покажем, как выглядят веса для этого ядра применительно к нашей задаче. Здесь я установил ширину полосы, равную 1, это означает, что мы будем учитывать только данные от людей не старше 22 лет и не моложе 20 лет. def kernel(R, c, h): indicator = (np.abs(R-c) <= h).astype(float) return indicator * (1 - np.abs(R-c)/h) plt.plot(drinking["agecell"], kernel(drinking["agecell"], c=0, h=1)) plt.xlabel("agecell") plt.ylabel("Вес") plt.title("Вес ядра по возрасту");
272  Разрывной регрессионный дизайн Если мы применим эти веса к нашей исходной задаче, влияние алкоголя на смертность станет сильнее, по крайней мере на смертность от всех причин. Оно увеличивается с 7.6627 до 9.7004. Результат остается статистически значимым. Также обратите внимание, что я использую WLS вместо OLS. model = smf.wls("all~agecell*threshold", rdd_df, weights=kernel(drinking["agecell"], c=0, h=1)).fit() model.summary().tables[1] ax = drinking.plot.scatter(x="agecell", y="all", color="C0") drinking.assign(predictions=model.fittedvalues).plot( x="agecell", y="predictions", ax=ax, color="C1" ) plt.title("Регрессия с разрывом (локальная регрессия)");
Взвешивание с помощью ядерной функции  273 А теперь посмотрим на остальные виды смертности. Обратите внимание, что регрессия справа имеет более отрицательный наклон1, поскольку она не учитывает большую часть точек, расположенных справа. plt.figure(figsize=(8,8)) weights = kernel(drinking["agecell"], c=0, h=1) for p, cause in enumerate(["all", "mva", "suicide"], 1): ax = plt.subplot(3,1,p) drinking.plot.scatter(x="agecell", y=cause, ax=ax) m = smf.wls(f"{cause}~agecell*threshold", rdd_df, weights=weights).fit() ate_pct = 100*((m.params["threshold"] + m.params["Intercept"]) / m.params["Intercept"] - 1) drinking.assign(predictions=m.fittedvalues).plot( x="agecell", y="predictions", ax=ax, color="C1" ) plt.title(f"Влияние алкоголя на смертность: {np.round(ate_pct, 2)}%") plt.tight_layout() 1 Отрицательный наклон означает, что при движении слева направо прямая направлена вниз: двигаясь слева направо по оси x, значения по оси y уменьшаются. –Прим. перев.
274  Разрывной регрессионный дизайн Похоже, во всех случаях, кроме самоубийств, мы получили, что использование ядерной функции еще больше усилило негативное влияние алкоголя. Еще раз убеждаемся в том, что если мы хотим минимизировать уровень смертности, нам НЕ следует рекомендовать снижение возраста, начиная с которого разрешено употребление алкоголя, поскольку существует явное влияние алкоголя на уровень смертности. Этот простой случай показывает, что происходит, когда метод регрессии с разрывом работает идеально. Далее мы посмотрим процедуру диагностики, которую нам следует провести, чтобы проверить надежность оценки RDD, и поговорим о влиянии образования на заработок – теме, которая очень дорога нашему сердцу. Эффект овчины и нечеткий RDD Если говорить о влиянии образования на заработок, в экономике сложились два основных мнения. Первое мнение – это широко известный аргумент о том, что образование увеличивает человеческий капитал, повышая производительность и, следовательно, заработок. С этой точки зрения образование действительно меняет вас к лучшему. Другая точка зрения состоит в том, что
Эффект овчины и нечеткий RDD  275 образование – это просто сигнальный механизм. Он просто проведет вас через все эти сложные тесты и академические задачи. Если вы можете их выполнить, это сигнализирует рынку, что вы – хороший сотрудник. Таким образом, образование не сделает вас более продуктивным. Это только говорит рынку, насколько продуктивным вы всегда были. Здесь важен диплом. Если он у вас есть, вам будут платить больше. Мы называем это эффектом овчины, поскольку раньше дипломы печатались на овчине. Чтобы проверить эту гипотезу, Кларк и Марторелл (https://faculty.smu.edu/ millimet/classes/eco7321/papers/clark%20martorell%202014.pdf) использовали регрессию с разрывом для измерения влияния окончания 12-го класса на заработок. Для этого им пришлось подумать о некоторой переменной отбора, согласно которой студенты, оказавшиеся выше порогового обучения, получают диплом, а те, кто оказался ниже этого порога, не получают диплома. Такие данные они нашли в системе образования Техаса. Чтобы получить диплом в Техасе, нужно сдать экзамен. Тестирование начинается в 10-м классе, и учащиеся могут проходить его несколько раз, но в конечном итоге в конце 12-го класса им предстоит сдать последний экзамен. Идея заключалась в том, чтобы получить данные от учащихся, сдававших последний экзамен, и сравнить тех, кто его чуть-чуть завалил, с теми, кто его едва сдал. У этих студентов будет очень схожий человеческий капитал, но разные сигнальные качества. А именно те, кто едва сдал экзамен, получат диплом. sheepskin = pd.read_csv("data/sheepskin.csv")[ ["avgearnings", "minscore", "receivehsd", "n"] ] sheepskin.head() И снова данные сгруппированы по переменной отбора. Набор данных содержит не только переменную отбора (minscore, уже центрировано в нуле) и переменную результата (avgearnings), но и вероятность получения диплома (receivehsd) для значения minscore в данной группе, а также размер группы (n). Так, например, из 12 студентов в группе –30 (что ниже порогового значения, равного 0) только 5 (12*0.416) смогли получить диплом. Это означает, что в назначении воздействия есть некоторый сдвиг. Некоторым студентам, не достигшим порогового значения, все равно удалось получить диплом. Здесь регрессионный разрыв является скорее нечетким
276  Разрывной регрессионный дизайн (fuzzy), чем четким. Обратите внимание, что вероятность получения диплома не переключается с нуля на единицу в пороговом значении. Она меняется с 50 % на 90 %. sheepskin.plot.scatter(x="minscore", y="receivehsd", figsize=(10,5)) plt.xlabel("Значения теста относительно порога") plt.ylabel("Доля получивших дипломы") plt.title("Последний экзамен"); Нечеткий RD (fuzzy RD – FRD) – это своего рода несоблюдение требований (non compliance). Преодоление порога должно привести к тому, что каждый получит диплом, но некоторые студенты, «всегда отказывающиеся» (nevertakers), его не получат. Точно так же, если вы находитесь ниже порога, вы не сможете получить диплом, но некоторым студентам, «всегда принимающим» (always-takers), все равно удастся его получить. В этой ситуации, как и в случае с потенциальным результатом, у нас есть потенциальный статус воздействия. T1 – это воздействие, которое все испытали бы, если бы находились выше порога. T0 – это воздействие, которое все испытали бы, если бы находились ниже порога. Как вы могли заметить, мы можем рассматривать порог как инструментальную переменную. Как и в случае с инструментальной переменной, если мы наивно, без учета особенностей нечеткого RD, оценим эффект воздействия, он будет смещен к нулю.
Эффект овчины и нечеткий RDD  277 Рис. 3. Вероятности назначения (FRD) Рис. 4. Регрессия потенциальных и наблюдаемых результатов (FRD) Если мы находимся выше порогового значения и вероятность воздействия меньше единицы, наблюдаемый результат будет меньше, чем истинный потенциальный результат Y1. Проще говоря, не все, кто находится выше порога, фактически испытают воздействие (получат лечение). Аналогично если мы находимся ниже порогового значения, наблюдаемый результат будет выше, чем истинный потенциальный результат Y0. Из-за этого создается впечатление, что эффект воздействия на уровне порогового значения меньше, чем он есть на самом деле, и нам придется использовать метод инструментальных переменных, чтобы скорректировать это. Точно так же, как мы ранее предполагали плавность потенциального результата, теперь мы предполагаем плавность потенциального воздействия. Кроме того, как и при использовании инструментальных переменных, нам сейчас нужно выдвинуть предположение о монотонности. Напомним, в нем говорится, что T i1 > T i0 ∀i. Это означает, что пересечение порога слева направо только увеличивает ваши шансы на получение диплома (или отсутствие «бунтарей»). При соблюдении этих двух предположений мы можем воспользоваться оценкой Вальда для LATE: Обратите внимание, что эта оценка является локальной в двух смыслах. Во-первых, она является локальной, потому что дает оценку эффекта воздействия только для порогового значения c. Это локальность разрывного
278  Разрывной регрессионный дизайн регрессионного дизайна. Во-вторых, она является локальной, потому что дает оценку эффекта воздействия только для участников, соблюдающих требования. Это локальность инструментальной переменной. Чтобы получить эту оценку, мы воспользуемся двумя линейными регрессиями. Числитель можно оценить так же, как мы это делали раньше. Чтобы получить знаменатель, мы просто заменяем переменную результата Y переменной воздействия T. Но сначала давайте поговорим о проверке адекватности, которую нам нужно провести, чтобы убедиться, что мы можем доверять полученным оценкам разрывного регрессионного дизайна. Тест Маккрари Разрывной регрессионный дизайн потерпит фиаско, если люди смогут манипулировать своим положением в области порогового значения. Допустим, в примере с выдачей дипломов учащиеся, находящиеся чуть ниже порога, смогут найти способ обойти систему и немного повысят свой балл по итогам теста. Возьмем другой пример. Для получения государственного пособия у вас должен быть доход ниже определенного уровня. Некоторые семьи могут намеренно снизить свой доход, просто чтобы получить право участвовать в программе государственной помощи. В такого рода ситуациях мы склонны наблюдать явление под названием «скученность вокруг порогового значения переменной отбора» («скученность по плотности переменной»). В контексте разрывного регрессионного дизайна это означает, что существует необычно большое скопление наблюдений вокруг порогового значения. Чтобы проверить это, мы можем построить график функции плотности переменной отбора и посмотреть, есть ли какие-либо всплески вокруг порога. Применительно к нашему случаю плотность определяется столбцом n набора данных. plt.figure(figsize=(8,8)) ax = plt.subplot(2,1,1) sheepskin.plot.bar(x="minscore", y="n", ax=ax) plt.title("Тест Маккрари") plt.ylabel("Плавность в районе порога") ax = plt.subplot(2,1,2, sharex=ax) sheepskin.replace({1877:1977, 1874:2277}).plot.bar( x="minscore", y="n", ax=ax ) plt.xlabel("Оценки по итогам теста относительно порога") plt.ylabel("Всплеск в районе порога");
­ Тест Маккрари  279 Первый график показывает, как выглядит плотность нашей переменной. Как мы видим, вокруг порога нет всплесков, нет скученности. Учащиеся не манипулируют своим положением в области порогового значения. Прос то в иллюстративных целях второй график показывает, как бы выглядела скученность, если бы учащиеся могли манипулировать своим положением в области порога. Мы увидим всплеск плотности в бине чуть выше порога, поскольку в этом бине будет находиться много учащихся, едва сдающих экзамен. Теперь вернемся к оценке эффекта овчины. Как я уже говорил ранее, числитель оценки Вальда можно оценить так же, как мы это сделали для четкого разрывного регрессионного дизайна. Здесь мы в качестве веса возьмем ядро с шириной полосы 15. Поскольку у нас еще есть информация о размере группы (бина), мы умножим ядро на размер группы, чтобы получить итоговый вес для группы. sheepsking_rdd = sheepskin.assign( threshold=(sheepskin["minscore"]>0).astype(int) ) model = smf.wls( "avgearnings~minscore*threshold", sheepsking_rdd,
280  Разрывной регрессионный дизайн weights=kernel(sheepsking_rdd["minscore"], c=0, h=15) * sheepsking_rdd["n"] ).fit() model.summary().tables[1] Модель говорит нам о том, что эффект диплома составляет –97.7571, но он не является статистически значимым (p-значение 0.5). Если мы визуализируем эти результаты, то получим непрерывную линию в области порогового значения. Более образованные люди действительно зарабатывают больше денег, но скачка в момент получения ими диплома по окончании 12-го класса не происходит. Это аргумент в пользу точки зрения, согласно которой образование увеличивает заработок, делая людей более продуктивными, а не просто является сигналом для работодателя. Другими словами, эффекта овчины не существует. ax = sheepskin.plot.scatter(x="minscore", y="avgearnings", color="C0") sheepskin.assign(predictions=model.fittedvalues).plot( x="minscore", y="predictions", ax=ax, color="C1", figsize=(8,5) ) plt.xlabel("Оценки по итогам теста относительно порога") plt.ylabel("Средний заработок") plt.title("Последний экзамен");
Тест Маккрари  281 Однако поскольку мы знаем, как работает смещение, возникающее в силу несоблюдения требований, можно утверждать, что полученный результат смещен в сторону нуля. Чтобы исправить это, нам нужно поделить результат на коэффициент регрессии 1-го этапа (масштабировать результат по коэффициенту регрессии 1-го этапа) и получить оценку Вальда. К сожалению, для этой операции не существует хорошей программной реализации на Python, поэтому нам придется делать это вручную и использовать бутстреп для получения стандартных ошибок. Нижеприведенный программный код вычисляет числитель оценки Вальда, как мы делали раньше, а затем вычисляет знаменатель, заменяя переменную результата на переменную воздействия receivehsd. Последний шаг – простая операция деления числителя на знаменатель. def wald_rdd(data): weights=kernel(data["minscore"], c=0, h=15)*data["n"] numerator = smf.wls( "avgearnings~minscore*threshold", data, weights=weights ).fit() denominator = smf.wls( "receivehsd~minscore*threshold", data, weights=weights ).fit() return numerator.params["threshold"] / denominator.params["threshold"] from joblib import Parallel, delayed np.random.seed(45) bootstrap_sample = 1000 ates = Parallel(n_jobs=4)(delayed(wald_rdd)( sheepsking_rdd.sample(frac=1, replace=True) ) for _ in range(bootstrap_sample)) ates = np.array(ates) С помощью бутстреп-выборок мы можем построить график распределения ATE и увидеть, где находится 95%-ный доверительный интервал. sns.distplot(ates, kde=False) plt.vlines(np.percentile(ates, 2.5), 0, 100, linestyles="dotted") plt.vlines(np.percentile(ates, 97.5), 0, 100, linestyles="dotted", label="95% ДИ") plt.title("Бутстреп-распределение ATE") plt.xlim([-10000, 10000]) plt.legend();
282  Разрывной регрессионный дизайн Как видите, даже когда мы масштабируем эффект по коэффициенту регрессии 1-го этапа, он все равно статистически значимо не отличается от нуля. Это означает, что образование увеличивает доходы не за счет простого эффекта овчины, а, скорее, за счет повышения продуктивности. Ключевые идеи Мы научились использовать искусственные разрывы для оценки причинноследственных эффектов. Идея состоит в том, что у нас будет некий искусственный порог, который приведет к резкому скачку вероятности воздействия. Одним из примеров, который мы рассмотрели, было резкое увеличение вероятности употребления алкоголя в возрасте 21 года. Мы могли бы использовать этот факт, чтобы оценить влияние употребления алкоголя на уровень смертности. Мы используем тот факт, что в районе порогового значения мы можем провести эксперимент, очень похожий на рандомизированное испытание. Объекты, находящиеся очень близко от порогового значения, могли быть как чуть выше, так и чуть ниже порога, и причины, которые определяют их расположение относительно порога, по сути, являются случайными. Благодаря этому мы можем сравнить объекты, находящиеся чуть выше порога, с объектами, находящимися чуть ниже порога, чтобы получить эффект воздействия. Мы показали, как это можно сделать с помощью взвешенной линейной регрессии с использованием ядра и при этом получить стандартные ошибки для среднего эффекта воздействия (ATE). Затем мы рассмотрели пример нечеткого разрывного регрессионного дизайна с несоблюдением требований. Мы увидели, что в этой ситуации можно поступить так же, как и в случае с инструментальными переменными.
 ­    ­   ­ Дополнительное чтение  283 Дополнительное чтение Я рассматриваю эту книгу как дань уважения Джошуа Ангристу (Joshua Angrist), Альберто Абади (Alberto Abadie) и Кристоферу Уолтерсу (Christopher Walters) за их потрясающий курс по эконометрике. Многие идеи почерпнуты из их лекций на курсах Американской экономической ассоциации. Наб людение за их работой поддерживало меня в здравом уме в этом трудном 2020 году.  «Cross-Section Econometrics», https://www.aeaweb.org/conference/conted/2017-webcasts;  «Mastering Mostly Harmless Econometrics», https://www.aeaweb.org/confe rence/cont-ed/2020-webcasts. Я также хочу сослаться на замечательные книги Ангриста. Они показали мне, что эконометрика, или «метрика», как ее еще называют, не только чрезвычайно полезна, но и по-настоящему увлекательна.  «Mostly Harmless Econometrics», https://www.mostlyharmlesseconometrics. com/;  «Mastering ‘Metrics», https://www.masteringmetrics.com/. Еще одна важная ссылка — это книга Мигеля Эрнана (Miguel Hernan) и Джейми Робинса (Jamie Robins). Она стала для меня надежным спутником в самых сложных вопросах, касающихся причинно-следственных связей.  «Causal Inference Book», https://www.hsph.harvard.edu/miguel-hernan/cau sal-inference-book/. Наконец, я также хотел бы отметить Скотта Каннингема (Scott Cunningham) и его блестящую работу, которая объединяет причинно-следственный анализ и цитаты из рэпа.  «Causal Inference: The Mixtape», https://www.scunning.com/mixtape.html. Аналогия с пивом, приведенная в главе 1, была взята из удивительной серии Stock Series (https://jlcollinsnh.com/2012/04/15/stocks-part-1-theres-a-majormarket-crash-coming-and-dr-lo-cant-save-you/), написанной Дж. Л. Коллинзом (JL Collins). Эту книгу должен прочитать каждый, кто хочет научиться продуктивно инвестировать свои деньги. Данные, использованные в главах 2 и 3, взяты из исследования Альперта (Alpert, William T.), Кеннета А. Кауча (Kenneth A. Couch) и Оскара Р. Хармона (Oskar R. Harmon), 2016. «A Randomized Assessment of Online Learning». American Economic Review, 106 (5): 378–82, https://www.aeaweb.org/articles?id=10.1257/ aer.p20161057.
284  Разрывной регрессионный дизайн Данные, использованные в главах 11 и 12, взяты из статьи «Estimating Treatment Effects with Causal Forests: An Application» Сьюзан Эти (Susan Athey) и Стефана Вагера (Stefan Wager), https://arxiv.org/pdf/1902.07409.pdf. Чистая поэзия
ЧАСТЬ II ИНЬ
Глава 17 Курс по прогнозным моделям Мы завершаем первую часть этой книги. Первая часть была посвящена основам анализа причинно-следственных связей. Методы там очень хорошо известны и отработаны. Они выдержали испытание временем. Часть I закладывает прочный фундамент, на который мы можем положиться. Говоря более техническим языком, часть I сфокусирована на определении анализа причинно-следственных связей, видах смещения, которые могут помешать корреляции стать каузацией (причинно-следственной связью), способах корректировки этих смещений (регрессия, сопоставление и оценка склонности) и стратегии канонической идентификации (инструментальные переменные, метод разности разностей и RDD). Таким образом, часть I посвящена стандартным методам, которые мы используем для определения среднего эффекта воздействия E[Y1 – Y0]. Когда мы перейдем ко второй части, ситуация станет немного шаткой. Мы расскажем о последних достижениях, изложенных в литературе по анализу причинно-следственных связей, взаимодействии анализа причинно-следственных связей с машинным обучением и применении анализа причинноследственных связей в промышленности. В этом смысле мы жертвуем академической строгостью в пользу практической применимости и эмпиризма. Некоторые методы, представленные в части II, не имеют четкой теории, объясняющей, почему они работают. Тем не менее когда мы их применим, мы убедимся, что они являются рабочими. В этом смысле часть II более полезна для специалистов-практиков, которые хотят использовать анализ причинноследственных связей в своей повседневной работе, а не для ученых, которые хотят исследовать фундаментальные причинно-следственные связи в мире. Первые несколько глав второй части будут посвящены оценке гетерогенных эффектов воздействия. Мы перенесемся из мира, в котором нас волновал только средний эффект воздействия, в мир, в котором мы хотим знать, как разные объекты по-разному реагируют на воздействие E[Y1 – Y0 | X]. Это мир, в котором персонализация имеет первостепенное значение. Мы хотим подвергнуть воздействию прежде всего тех, для кого эффект воздействия
­ Машинное обучение в промышленности  287 будет наиболее действенным, и не хотим подвергнуть воздействию тех, кому воздействие может навредить. В каком-то смысле мы переходим от вопроса «что такое средний эффект воздействия?» к вопросу «кого нам следует подвергнуть воздействию?». Это вопрос, который задает себе большинство компаний, хотя и в несколько иных терминах: кому я должен предоставлять скидки? Какую процентную ставку следует взимать по кредиту? Какой товар я должен порекомендовать этому пользователю? Какой макет страницы я должен показать клиенту? На все эти вопросы о гетерогенности эффекта воздействия мы можем ответить с помощью инструментов, представленных в части II. Но прежде чем мы это сделаем, будет справедливо рассказать, что представляет собой машинное обучение в промышленности, поскольку оно станет главным инструментом, который мы позже будем использовать для анализа причинно-следственных связей. Машинное обучение в промышленности Цель этой главы – поговорить о том, как мы обычно используем машинное обучение в промышленности. Если вы еще незнакомы с машинным обуче нием, рассматривайте эту главу в качестве ускоренного курса. Если вы никогда раньше не работали с ML, я настоятельно рекомендую вам изучить хотя бы основы, чтобы получить максимальную отдачу от того, что вас ждет. Но это не значит, что вам следует пропустить эту главу, если вы уже разбираетесь в ML. Я по-прежнему думаю, что вам будет полезно прочитать этот материал. В отличие от других материалов по машинному обучению, в этом материале не обсуждаются все тонкости таких алгоритмов, как деревья решений или нейронные сети. Вместо этого он будет сосредоточен на том, как машинное обучение применяется в реальном мире.
288  Курс по прогнозным моделям Первое, на что я хочу обратить внимание: почему в книге, посвященной анализу причинно-следственных связей, мы говорим о машинном обучении? Короткий ответ: я считаю, что один из лучших способов понять причинность (каузальность) – это противопоставить ее прогнозному моделированию, основанному на машинном обучении. Длинный же ответ будет двояким. Во-первых, если вы дошли до этого места в нашей книге, велика вероятность, что вы уже знакомы с машинным обучением. Во-вторых, даже если это не так, учитывая нынешнюю популярность машинного обучения, вы, вероятно, уже имеете некоторое представление о том, что оно из себя представляет. Единственная проблема заключается в том, что, несмотря на всю шумиху вокруг машинного обучения, мне, возможно, придется вернуть вас на землю и объяснить, что оно на самом деле делает, с очень практической точки зрения. Наконец, в самых последних достижениях в области анализа причинно-следственных связей интенсивно используются алгоритмы машинного обучения, так что это тоже имеет значение. Будучи очень прямолинейным по своей сути, машинное обучение – это способ генерировать быстрые, автоматические и хорошие прогнозы. Этим определением машинное обучение не исчерпывается, но данная формулировка на 90 % охватывает суть. Именно в области машинного обучения с учителем было сделано большинство замечательных достижений, таких как компьютерное зрение, беспилотные автомобили, перевод с одного языка на другой и диагностика. Обратите внимание, что на первый взгляд может показаться, что эти задачи нельзя отнести к задачам прогнозирования. Каким образом перевод с одного языка на другой стал прогнозом? В этом и заключается вся прелесть машинного обучения. С помощью прогнозирования мы можем решить гораздо больше проблем, чем кажется на первый взгляд. В случае перевода с одного языка на другой вы можете сформулировать эту задачу в виде задачи прогнозирования, когда предоставляете машине одно предложение, а она должна предсказать то же самое предложение на другом языке. Обратите внимание, что я не использую слово «предсказание» в смысле прогнозирования или предвосхищения будущего. Здесь прогнозирование – это просто отображение одного определенного входа в изначально неизвестный, но столь же четко определенный и наблюдаемый выход1. 1 В машинном обучении входы представляют собой признаки или характеристики, поданные на вход модели, а выходы – это прогнозируемые значения или классы, которые модель пытается предсказать. – Прим. перев.
Машинное обучение в промышленности  289 На самом деле машинное обучение выучивает эту отображающую функцию, даже если речь идет об очень сложной отображающей функции. Суть заключается в том, что если вы можете сформулировать задачу как отображение пространства входных данных в пространство выходных данных, то машинное обучение может стать хорошим кандидатом для ее решения. Что касается беспилотных автомобилей, эту задачу можно представить не в виде одной, а в виде нескольких сложных задач прогнозирования: прогнозирование правильного угла поворота колеса на основе датчиков в передней части автомобиля, прогнозирование давления в тормозах по данным камер вокруг автомобиля, прогнозирование давления в акселераторе по данным GPS. Решение этих задач прогнозирования (и не только их) составляет суть беспилотного автомобиля. Более технический подход к машинному обучению заключается в оценке (возможно, очень сложных) функций математического ожидания: E[Y | X ], где Y – это то, что вам нужно узнать (предложение, которое нужно перевести, диагностика), и X – это то, что вы уже знаете (исходное предложение, рентгеновский снимок). Машинное обучение – это просто способ оценки функции условного математического ожидания. Хорошо… Теперь вы понимаете, что предсказания могут быть более эффективными, чем мы думали вначале. Беспилотные автомобили и перевод с одного языка на другой – это круто и все такое, но довольно абстрактно, если только вы не работаете в крупной технологической компании типа Google или Uber. Итак, чтобы сделать ситуацию более понятной, давайте поговорим о проблемах, с которыми сталкивается почти каждая компания: привлечение клиентов (то есть приобретение новых клиентов). С точки зрения привлечения клиентов вам часто нужно выяснить, кто является прибыльным клиентом. В этой задаче есть затраты на привлечение клиента (возможно, затраты на маркетинг, затраты на адаптацию, расходы на доставку…), который, как мы надеемся, принесет компании положительный денежный поток. Например, предположим, что вы интернет-провайдер или газовая компания. Денежный поток от вашего типичного клиента может выглядеть примерно так:
290  Курс по прогнозным моделям Каждый столбик представляет собой транзакцию в ваших отношениях с клиентом. Например, чтобы сразу заполучить клиента, вам нужно инвестировать в маркетинг. Затем, после того как кто-то решит вести с вами бизнес, вы можете понести какие-то затраты на адаптацию (когда вам придется объяснить клиенту, как использовать ваш продукт) или затраты на имплементацию. Только тогда клиент начинает генерировать ежемесячную прибыль. В какой-то момент клиенту может понадобиться помощь, и вам придется заплатить за обслуживание. Наконец, если клиент решит расторгнуть контракт, у вас также могут возникнуть некоторые заключительные издержки. Чтобы понять, является ли клиент прибыльным, мы можем изменить расположение столбиков на так называемом каскадном графике. Будем надеяться, что сумма транзакций окажется намного выше нулевой линии. Напротив, вполне возможно, что клиент будет генерировать гораздо больше затрат, чем доходов. Если он использует ваш продукт очень редко и предъявляет высокие требования к обслуживанию, при накоплении сумма транз­ акций может оказаться ниже нулевой линии. Конечно, этот денежный поток может быть проще или намного сложнее, в зависимости от типа бизнеса. Вы можете предложить временные скидки с процентной ставкой и сильно заморочиться с этим, но я думаю, что суть здесь ясна. Однако что можно сделать со всем этим? Что ж, если у вас есть много прибыльных и неприбыльных клиентов, вы можете натренировать модель ма-
Машинное обучение в промышленности  291 шинного обучения отличать прибыльных клиентов от неприбыльных. Таким образом, вы можете сосредоточить свои маркетинговые стратегии, ориентированные только на прибыльных клиентов. Или, если ваш контракт позволяет, вы можете прекратить отношения с клиентом до того, как он принесет вам дополнительные расходы. По сути, вы здесь формулируете бизнес-задачу как задачу прогнозирования, чтобы ее можно было решить с помощью машинного обучения: вы хотите предсказать или определить прибыльных и неприбыльных клиентов, чтобы взаимодействовать только с прибыльными. import pandas as pd import numpy as np from sklearn import ensemble from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.metrics import r2_score import seaborn as sns from matplotlib import pyplot as plt from matplotlib import style style.use("ggplot") Например, предположим, у вас есть 30 дней транзакционных данных по 10 000 клиентов. У вас также есть стоимость привлечения cacq. Это может быть бид – максимальная цена, которую вы готовы заплатить за рекламу и устанавливаете самостоятельно, если занимаетесь онлайн-маркетингом, это может быть стоимость доставки или любого обучения, которое вам нужно провести с клиентом, чтобы он мог воспользоваться вашим продуктом. Также для упрощения (ведь это экспресс-курс, а не семестровый курс, посвященный оценке клиентов) давайте притворимся, что вы сами выбираете себе клиентов для бизнеса. Другими словами, у вас есть возможность отказать клиенту, даже если он захочет вести бизнес с вами. Если это так, вам нужно заранее определить тех, кто принесет прибыль, чтобы вы могли взаимодействовать только с ними. transactions = pd.read_csv("data/customer_transactions.csv") print(transactions.shape) transactions.head() Теперь нам нужно отделить хороших клиентов от плохих на основе этих транзакционных данных. Ради простоты я просто сложу все транзакции
292  Курс по прогнозным моделям и CACQ. Имейте в виду, что это упрощение не учитывает множество нюансов, таких как различие между клиентами, которые ушли, и теми, кто просто делает перерыв между покупками. Потом я объединю эту сумму, которую назвал net_value, с характеристиками конкретного клиента. Поскольку моя цель – выяснить, будет ли клиент прибыльным, перед тем как решить, взаимодействовать с ним или нет, можно использовать только данные до периода привлечения. В нашем случае этими характеристиками будут возраст, регион и доход, все они доступны в другом CSV-файле. profitable = (transactions[["customer_id"]] .assign(net_value = transactions .drop(columns="customer_id") .sum(axis=1))) customer_features = (pd.read_csv("data/customer_features.csv") .merge(profitable, on="customer_id")) customer_features.head() Отлично! Наша задача стала менее абстрактной. Мы хотим отделить прибыльных клиентов (net_value > 0) от неприбыльных. Давайте попробуем разные варианты и посмотрим, какой из них работает лучше. Но перед этим нам нужно быстро пройтись по машинному обучению (можете пропустить, если знаете, как работает ML). Ускоренный курс по машинному обучению В зависимости от наших замыслов и целей мы можем думать о машинном обучении как о мощном способе прогнозирования. Чтобы машинное обучение сработало, вам нужны признаки и метки, т. е. фактические значения зависимой переменной – переменной, которую вы прогнозируете. Зависимую переменную еще называют целевой переменной. Затем вы обучаете модель
­ Ускоренный курс по машинному обучению  293 машинного обучения на этих данных и используете ее для прогнозирования, когда фактические значения зависимой переменной неизвестны. Рисунок ниже иллюстрирует типичный процесс машинного обучения. Обучаем ML Про гно зир уем Во-первых, вам нужны фактические значения зависимой переменной, net_value. Затем вы обучаете модель машинного обучения, которая будет использовать признаки region, income, age для прогнозирования net_value. На этапе обучения или оценивания будет создана модель машинного обуче ния, которую можно будет использовать для прогнозирования значений net_value, если они неизвестны. Это показано в левой части рисунка. У вас есть новые данные, в которых есть признаки region, income, age, но у вас нет значений net_value. Итак, вы передаете эти признаки в модель, и она генерирует спрогнозированные значения net_value. Это показано в правой части рисунка. Для тех, кто больше любит технические обозначения, еще один способ понять машинное обучение – это оценить условное ожидание E[Y | X], где Y – это зависимая переменная (отклик, переменная результата), а X – это признаки. ML – это просто мощный способ получить Ê[Y | X] с помощью оптимизации некоторой функции потерь. Одна из сложностей моделей машинного обучения заключается в том, что они могут аппроксимировать практически любую функцию. Другими словами, их можно сделать настолько мощными, чтобы они идеально соответствовали данным обучающего набора. У моделей машинного обучения, как правило, есть гиперпараметры сложности. Они определяют, насколько мощной или сложной может быть модель. На рисунке ниже вы можете увидеть примеры простой модели (слева), промежуточной модели (в центре) и сложной и мощной модели (справа). Обратите внимание, как сложная модель идеально соответствует обучающим данным.
­ 294  Курс по прогнозным моделям Здесь возникает ряд проблем. А именно как нам оценить качество модели перед ее применением в реальном мире? Один из способов – сравнить прогнозы с фактическими значениями обучающего набора. Речь идет о так называемых метриках качества подгонки типа R2. Но помните, что модель можно легко сделать очень мощной, и она будет идеально соответствовать обучающим данным. Если это произойдет, прогнозы будут идеально соответствовать фактическим значениям обучающего набора. Получается, что такая проверка вводит в заблуждение, поскольку мы можем добиться идеального качества, просто сделав модель более мощной и сложной. Кроме того, очень сложная модель не значит оптимальная модель. И здесь есть интуитивное обучение. Например, на рисунке выше какую модель вы предпочтете? Более сложную, которая дает всегда верные предсказания? Вероятно, нет. Вы, скорее всего, предпочтете средний вариант. Здесь модель является более плавной, простой и тем не менее по-прежнему дает хорошие прогнозы, даже если они не соответствуют данным идеально. Знание Опыт Переобучение Креативность Ваша интуиция права. Что произойдет, если вы построите очень мощную модель? Она не только выучит закономерности в ваших обучающих данных, но также выучит случайный шум. Поскольку шум в обучающих данных будет разительно отличаться от шума в наборе новых данных, к которому вы примените вашу обученную модель (в конце концов, наш мир случаен), ваша «идеальная» модель будет допускать ошибки. Используя термины машинного обучения, мы можем сказать, что слишком сложные модели переобуча ются и плохо обобщают на новые данные. Так что же мы можем сделать?
Перекрестная проверка  295 Мы собираемся притвориться, что у нас нет доступа к некоторым частям данных. Идея состоит в том, чтобы разделить набор данных с фактическими метками на две части. Затем мы можем использовать одну часть для обучения модели, а другую часть – для проверки прогнозов обученной модели. Данная процедура называется перекрестной проверкой. В вышеприведенном наборе данных, который модель не видела во время обучения, сложная модель не очень хорошо справляется со своей задачей. С другой стороны, модель посередине, похоже, работает лучше. Для выбора правильной сложности модели мы можем обучить несколько моделей с разной сложностью и посмотреть, как они работают на данных с фактическими метками, которые не использовались для обучения модели. Перекрестная проверка настолько важна, что нам, вероятно, следует уделить ей больше времени. Перекрестная проверка Перекрестная проверка необходима для выбора сложности модели, но она полезна и помимо этого. Мы можем использовать ее всякий раз, когда нужно попробовать много разных вариантов и оценить, как они будут работать в реальном мире. Идея перекрестной проверки состоит в том, чтобы имитировать реальный мир, где мы оцениваем модель на основе имеющихся у нас данных, но делаем прогнозы на основе новых, незнакомых для модели данных. Отложенные данные, которые у нас играют роль новых данных, служат имитацией того, с чем мы столкнемся в реальности. Давайте посмотрим, как можно применить перекрестную проверку к нашей задаче. Напомним, что нам нужно определить, какие клиенты прибыльны, а какие – нет. Вот схема того, что нам следует сделать: 1. У нас есть данные о существующих клиентах. На основе этих данных мы знаем, какие клиенты прибыльны, а какие – нет (нам известны фактические метки). Давайте назовем наши внутренние данные обучаю­ щим набором. 2. Мы будем использовать внутренние данные, чтобы выучить правила, в соответствии с которыми клиент является прибыльным (следовательно, происходит обучение).
296  Курс по прогнозным моделям 3. Мы применим правила к отложенным данным, которые не использовались для выучивания правил. Мы симулируем процесс выучивания правил в одном наборе данных и применения этих правил к другому набору – процесс, который мы повторим, когда перейдем к производству и получим действительно новые данные. Ниже приведена иллюстрация перекрестной проверки. В правой части рисунка расположены действительно новые данные (выделены розовым цветом), а есть данные, в отношении которых мы лишь притворяемся, что они являются новыми (веделены светло-коричневым цветом). Внутренние данные Обучающие данные Тестовые данные Новые данные Новые данные Итак, мы разобьем наши внутренние данные на обучающий и тестовый наборы. Мы используем обучающий набор для обучения модели или выучивания правил, которые определяют, будет клиент прибыльным или нет. На тестовом наборе мы будем проверять эти правила. Тестовый набор не используется для обучения. Заметим, что существует множество способов улучшить перекрестную проверку, кроме этого простого разделения на обучающий и тестовый наборы (например, k-кратная перекрестная проверка или перекрестная проверка для временных рядов), но для простоты изложения условимся, что описанной процедуры проверки достаточно. Помните, что суть перекрестной проверки заключается в моделировании того, что произойдет, когда мы перейдем в производственную среду. Используя перекрестную проверку, мы надеемся получить более реалистичные оценки. В нашем случае я не буду делать ничего особенного. Я просто разделю набор данных на две части. 70 % данных будет использовано для обучения модели, которая позволит нам выявить прибыльных клиентов, а 30 % – для оценки качества нашей модели. train, test = train_test_split(customer_features, test_size=0.3, random_state=13) train.shape, test.shape ((7000, 5), (3000, 5))
Политика на основе одного признака  297 Прогнозы и политики Этап 1 Создаем крутую прогнозную модель Этап 2 Этап 3 Прибыль Мы говорили о методах и подходах к выявлению прибыльных клиентов, но пришло время уточнить наши понятия. Представим вам два новых понятия. Прогноз (prediction) – это число, которое оценивает или предсказывает что-то. Это оценивание Ê [yi | Xi ]. Например, мы можем попытаться спрогнозировать прибыльность клиента, и прогноз будет примерно равен 16 BRL (бразильских реалов), то есть мы прогнозируем, что этот клиент принесет доход в размере 16 BRL. Суть в том, что прогноз – это простое число. Второе понятие – это политика (policy). Политика – это правило принятия решения, чаще всего принятие решений происходит автоматически. Прогноз – это число, а политика – это решение. Например, у нас может быть политика, согласно которой мы взаимодействуем с клиентами, имеющими доход более 1000 BRL, и не взаимодействуем с остальными. Обычно мы строим политику на основе прогнозов: взаимодействуем со всеми клиентами, у которых прогноз прибыльности превышает 10 BRL Ê[yi | Xi ] > 10, и не взаимодействуем с другими клиентами. Машинное обучение обычно интересует первое, то есть прогноз. Но заметьте, что прогнозы сами по себе бесполезны. Нам нужно какое-то решение или политика на основе прогноза. Мы можем создавать как очень простые политики и модели, так и очень сложные. И для политик, и для прогнозов нам необходимо использовать перекрестную проверку, то есть оценить политику или прогноз на одном подмножестве данных и проверить их полезность на другом подмножестве. Поскольку мы уже разделили наши данные на две части, все уже готово. Политика на основе одного признака Прежде чем мы свихнемся на машинном обучении, решая эту задачу поиска прибыльных клиентов, давайте сначала попробуем простые вещи. 20 % уси-
298  Курс по прогнозным моделям лий дают 80 % результата. Простые вещи часто творят чудеса, и, что удивительно, большинство специалистов по data science о них забывают. Итак, что самое простое мы можем сделать? Естественно, просто взаимодействовать со всеми клиентами! Вместо того чтобы делить клиентов на прибыльных и неприбыльных, давайте просто будем вести дела со всеми и надеяться, что прибыльные клиенты с лихвой компенсируют неприбыльных клиентов. Чтобы проверить, хорошая ли это идея, мы можем посмотреть среднюю чистую стоимость клиентов. Если результат окажется положительным, это означает, что в среднем мы заработаем на наших клиентах. Конечно, будут прибыльные и неприбыльные, но в среднем, если у нас будет достаточно клиентов, мы заработаем деньги. С другой стороны, если это значение будет отрицательным, мы потеряем деньги, взаимодействуя со всеми клиентами. train["net_value"].mean() -29.169428571428572 Ничего не вышло… Если мы будем взаимодействовать со всеми, мы потеряем около 30 BRL. Наша первая, очень простая вещь не сработала, и нам лучше найти что-то более перспективное, если мы не хотим потерять бизнес. Небольшое примечание: имейте в виду, что это педагогический пример. Хотя очень простая политика «относиться ко всем одинаково» здесь не сработала, в реальной жизни она часто работает. Обычно бывает так, что отправить всем маркетинговое электронное письмо лучше, чем не отправлять его, или раздать всем купоны на скидку часто лучше, чем не раздавать их. Забегая вперед, спросим себя: какую следующую простейшую модель мы можем придумать? Одна из идей – взять наши признаки и посмотреть, насколько точно они помогают нам отделить хороших клиентов от плохих. Возьмем, к примеру, income. Интуитивно понятно, что более богатые клиенты должны быть более прибыльными, не так ли? Что, если мы будем вести бизнес только с самыми богатыми клиентами? Будет ли это хорошей идеей? Чтобы выяснить это, мы можем разделить наши данные по квантилям дохода (квантиль делит данные на части одинакового размера, этим он мне нравится). Затем для каждого квантиля дохода давайте рассчитаем среднюю чистую стоимость. Есть надежда на то, что хотя средняя чистая стоимость отрицательна, E[NetValue] < 0, может существовать некоторая группа дохода, где чистая стоимость положительна, E[NetValue] > 0, вероятно, речь идет о группах с более высоким уровнем доходов. plt.figure(figsize=(12,6)) # задаем стартовое значение генератора псевдослучайных # чисел, поскольку ДИ из seaborn использует бутстреп np.random.seed(123) # функция pd.qcut() создает квантили дохода sns.barplot(
­ Политика на основе одного признака  299 data=train.assign(income_quantile=pd.qcut(train["income"], q=20)), x="income_quantile", y="net_value" ) plt.title("Прибыльность по доходу") plt.xticks(rotation=70); И к сожалению, опять нет. Вновь все группы доходов имеют отрицательную среднюю чистую стоимость. Хотя это и правда, что более богатые клиенты чуть выгоднее, чем небогатые, они все равно дают в среднем отрицательную чистую стоимость. Итак, доход здесь нам не очень помог, но как насчет других переменных, таких как регион? Если большая часть наших затрат связана, скажем, с необходимостью обслуживать клиентов в отдаленных областях, мы должны ожидать, что регион позволит отделить прибыльных клиентов от неприбыльных. Поскольку регион уже является категориальной переменной, нам не нужно использовать квантили. Давайте просто посмотрим среднюю чистую стои мость по регионам. plt.figure(figsize=(12,6)) np.random.seed(123) region_plot = sns.barplot(data=train, x="region", y="net_value") plt.title("Прибыльность по региону");
300  Курс по прогнозным моделям Бинго! Мы ясно видим, что некоторые регионы прибыльны, например регионы 2, 17, 39, а некоторые – нет, например регионы 0, 9, 29 и особенно регион 26. Это выглядит очень многообещающе! Мы можем взять эту информацию и сформулировать на ее основе политику: вести бизнес только с теми регионами, которые показали себя прибыльными на основе тех данных, которые у нас есть. Следует отметить, что мы делаем то же самое, что делает ML, но гораздо проще. А именно мы оцениваем ожидаемое значение чистой стоимости в каждом регионе: E[NetValue | Region]. Теперь нам нужно взять эту оценку и что-то с ней сделать. Чтобы построить на основе этой оценки политику, мы выполним очень простую операцию. Мы вычислим 95%-ный доверительный интервал для ожидаемой чистой стоимости региона. Если он больше нуля, мы будем вести дела с этим регионом. Следующий программный код создает словарь, в котором ключом является регион, а значением – нижняя граница 95%-ного доверительного интервала. Затем мы отбираем только те регионы, в которых ожидаемая чистая стоимость является положительной. Результатом станут регионы, с которыми мы будем вести бизнес в соответствии с нашей политикой. # извлекаем нижнюю границу 95%-ного ДИ для чистой стоимости региона regions_to_net = train.groupby('region')['net_value'].agg( ['mean', 'count', 'std'] ) regions_to_net = regions_to_net.assign( lower_bound=regions_to_net['mean'] - 1.96 * regions_to_net['std'] / (regions_to_net['count'] ** 0.5) )
Политика на основе одного признака  301 regions_to_net_lower_bound = regions_to_net['lower_bound'].to_dict() regions_to_net = regions_to_net['mean'].to_dict() # извлекаем регионы, в которых нижняя # граница чистой стоимости > 0 regions_to_invest = { region: net for region, net in regions_to_net_lower_bound.items() if net > 0} regions_to_invest {1: 2.9729729729729737, 2: 20.543302704837856, 4: 10.051075065003388, 9: 32.08862469914759, 11: 37.434210420891255, 12: 37.44213667009523, 15: 32.09847683044394, 17: 39.52753893574483, 18: 41.86162250217046, 19: 15.62406327716401, 20: 22.06654814414531, 21: 24.621030401718578, 25: 33.97022928360584, 35: 11.68776141117673, 37: 27.83183541449011, 38: 49.740709395699994, 45: 2.286387928016998, 49: 17.01853709535029} Словарь region_to_invest содержит все регионы, с которыми мы будем взаи­модействовать. Теперь давайте посмотрим, как эта политика сработает на нашем тестовом наборе. Это ключевой шаг в оценке нашей политики, поскольку вполне возможно, что по чистой случайности тот или иной регион в нашем обучающем наборе оказался прибыльным. Если это происходит только в силу случайности, маловероятно, что мы обнаружим ту же самую закономерность в тестовом наборе. Для этого мы отфильтруем наблюдения тестового набора так, чтобы в наборе остались клиенты из регионов, обозначенных как прибыльные (согласно нашему обучающему набору). Затем мы построим график распределения чистой стоимости для этих клиентов, а также покажем среднюю чистую стои­ мость в соответствии с нашей политикой. region_policy = ( test[test["region"] # оставляем только регионы из словаря regions_to_invest .isin(regions_to_invest.keys())] ) sns.histplot(data=region_policy, x="net_value")
302  Курс по прогнозным моделям # среднее значение должно быть по всем клиентам, а не только # по тем, которых мы отобрали с помощью нашей политики plt.title("Средняя чистая стоимость: %.2f" % ( region_policy["net_value"].sum() / test.shape[0])); Политика на основе модели машинного обучения Если вам нужен результат получше, можно воспользоваться возможностями машинного обучения. Имейте в виду, что это может значительно усложнить процесс оценивания и проверки и обычно приводит к небольшому улучшению. Но в зависимости от обстоятельств небольшие улучшения могут дать огромные суммы денег, и именно поэтому машинное обучение так ценно в наши дни. Здесь я воспользуюсь моделью градиентного бустинга. Эту модель непрос­ то интерпретировать, но она очень проста в использовании. Здесь нам не нужно вдаваться в детали того, как она работает. Вместо этого просто помните то, что мы выяснили в нашем кратком курсе по машинному обучению: модель машинного обучения – это сверхмощная машина для получения прог­ нозов, обладающая некоторыми параметрами сложности. Это инструмент для оценки E[Y | X]. Чем сложнее модель, тем мощнее она становится. Однако если сложность слишком высока, модель будет переобучаться, исследовать шум и плохо обобщать на новые данные. Поэтому здесь нам нужно использовать перекрестную проверку для подбора правильной сложности модели.
Политика на основе модели машинного обучения  303 Теперь нам нужно выяснить, как можно использовать хорошие прогнозы для улучшения нашей простой региональной политики по выявлению и взаи­модействию с прибыльными клиентами. Думаю, здесь можно сделать два основных улучшения. Во-первых, вам придется согласиться, что перебирать все признаки в поисках того, который будет наилучшим образом отличать хороших клиентов от плохих, – это громоздкий процесс. Здесь у нас всего 3 признака (возраст, доход и регион), поэтому эта идея выглядит неплохо, но представьте, если бы у вас было более 100 признаков. Кроме того, нужно быть осторожным из-за проблем, связанных со множественным тестированием (https://en.wikipedia.org/wiki/Multiple_comparisons_problem) и ложноположительными срабатываниями. Вторая причина заключается в том, что, вероятно, вам нужно больше одного признака для классификации клиентов на прибыльных и неприбыльных. Применительно к нашему примеру мы считаем, что в других признаках, помимо региона, содержится некоторая информация о прибыльности клиентов. Конечно, когда мы рассматривали только доход, это нам мало что дало, но как насчет дохода в тех регионах, которые лишь чуть-чуть неприбыльны? Возможно, в этих регионах, если мы сосредоточимся только на богатых клиентах, мы все равно сможем получить прибыль. Технически говоря, мы утверждаем, что E[NetValue | Region, Age] является лучшим предиктором net_value, чем E[NetValue | Region]. Это логично. Использование большего количества информации о доходе и возрасте, помимо региона, должно позволить нам лучше предсказывать чистую стоимость. Разработка более сложных политик, включающих взаимодействие нескольких признаков, может быть чрезвычайно сложной. Комбинации, которые нам нужно рассмотреть, растут экспоненциально с увеличением количества признаков, и это просто непрактично. Вместо этого мы можем подать все эти признаки в модель машинного обучения и позволить ей изучить все эти взаимодействия за нас. Именно это мы и сделаем далее. Целью этой модели будет прогнозирование net_value с помощью region, income и age. Чтобы помочь модели, мы возьмем признак region, который является категориальной переменной, и закодируем его количественными значениями. Мы заменим каждый регион средним значением чистой стои­ мости региона в обучающем наборе. Помните, что у нас эта информация сохранена в словаре regions_to_net? Для этого нужно просто вызвать метод .replace() и передать этот словарь в качестве аргумента. Я напишу функцию для выполнения этой операции, потому что мы выполним ее несколько раз. Этот процесс преобразования признаков для упрощения процесса обучения обычно называется конструированием признаков. def encode(df): return df.replace({"region": regions_to_net}) Теперь нам нужно импортировать из scikit-learn (https://scikit-learn.org/ stable/) класс GradientBoostingRegressor, в котором реализована модель градиентного бустинга. Все модели этой библиотеки имеют довольно стандартное использование. Сначала вы создаете экземпляр модели, передавая парамет­
304  Курс по прогнозным моделям ры сложности. Для этой модели мы установим количество деревьев равным 400, максимальную глубину – 4 и т. д. Чем больше деревьев и чем глубже деревья, тем мощнее будет модель. Конечно, мы не можем сделать модель слишком мощной, иначе она воспримет шум в обучающих данных за сигнал и возникнет переобучение. Опять же, вам не нужна подробная информация о работе каждого параметра. Просто имейте в виду, что это очень хорошая модель прогнозирования. Затем для обучения нашей модели мы вызовем метод .fit(), передав признаки X и переменную, которую мы хотим предсказать, или целевую переменную, net_value. model_params = {'n_estimators': 400, 'max_depth': 4, 'min_samples_split': 10, 'learning_rate': 0.01, 'loss': 'squared_error'} features = ["region", "income", "age"] target = "net_value" np.random.seed(123) reg = ensemble.GradientBoostingRegressor(**model_params) # обучаем модель на обучающем наборе encoded_train = train[features].pipe(encode) reg.fit(encoded_train, train[target]); Модель теперь обучена. Далее нам нужно проверить ее качество. Для этого мы можем посмотреть прогнозирующую способность этой модели на тес­ товом наборе. Существует множество метрик для оценки качества модели машинного обучения. Здесь я воспользуюсь R2. Здесь нам не нужно вдаваться в подробности. Достаточно сказать, что R2 используется для оценки моделей, которые прогнозируют непрерывную переменную (например, net_income). Кроме того, R2 может варьировать от минус бесконечности (значение мет­ рики будет отрицательным, если прогноз хуже среднего) до 1.0. R2 говорит нам, какая доля дисперсии зависимой переменной net_income объясняется нашей моделью. train_pred = (encoded_train .assign(predictions=reg.predict(encoded_train[features]))) print("R2 на обучающем наборе: ", r2_score(y_true=train[target], y_pred=train_pred["predictions"])) print("R2 на тестовом наборе: ", r2_score(y_true=test[target], y_pred=reg.predict(test[features].pipe(encode)))) R2 на обучающем наборе: 0.7108790300152951 R2 на тестовом наборе: 0.6938513063048141
Политика на основе модели машинного обучения  305 В нашем случае модель объясняет около 71 % дисперсии net_income в обучаю­щем наборе, но только около 69 % дисперсии net_income в тестовом наборе. Это ожидаемо. Поскольку модель имела доступ к меткам зависимой переменной в обучающем наборе, качество там часто будет завышено. Прос­ то ради развлечения (и чтобы узнать больше о переобучении) попробуйте установить для параметра max_depth модели значение 14 и посмотрите, что произойдет. Вы, вероятно, увидите, что R2 для обучающего набора взлетает до небес, а R2 для тестового набора становится ниже. Вот именно так выглядит переобучение. Далее, чтобы разработать нашу политику, мы сохраним прогнозы тестового набора в столбце prediction. Эти прогнозы представляют собой оценки E[NetValue | Age, Income, Region]. model_policy = test.assign( prediction=reg.predict(test[features].pipe(encode)) ) model_policy.head() Как и в предыдущем случае с признаком region, мы можем показать среднюю чистую стоимость на основе прогнозов нашей модели. Опять сделаем наши данные дискретными. Один из способов сделать это – воспользоваться функцией qcut() библиотеки pandas (Ей-богу! Мне нравится эта функция!), которая разбивает данные на квантили, используя прогнозы модели. Давайте возьмем 50 квантилей, потому что 50 – это количество имеющихся у нас регионов. И просто по соглашению я назову эти квантили диапазонами прог­нозов модели (model bands), т. е. та или иная группа содержит прог­ нозы, лежащие в определенном диапазоне, скажем в диапазоне от –10 до 10. plt.figure(figsize=(12,6)) n_bands = 50 bands = [f"band_{b}" for b in range(1,n_bands+1)] np.random.seed(123) model_plot = sns.barplot( data=model_policy .assign(model_band = pd.qcut(model_policy["prediction"], q=n_bands)), x="model_band", y="net_value") plt.title("Прибыльность по квантилям прогнозов модели") plt.xticks(rotation=70);
306  Курс по прогнозным моделям Здесь обратите внимание, что у нас есть диапазоны прогнозов, в которых чистая стоимость демонстрирует высокие отрицательные значения, а есть и диапазоны, где чистая стоимость демонстрирует высокие положительные значения. Кроме того, существуют диапазоны, в отношении которых мы точно не знаем, является чистая стоимость отрицательной или положительной. Наконец, обратите внимание на восходящий тренд, идущий слева направо. Поскольку мы прогнозируем непосредственно чистую стоимость, ожидается, что прогнозы будут пропорциональны значениям чистой стоимости (т. е. нам не нужно делать никаких дополнительных преобразований). Теперь, чтобы сравнить политику на основе модели машинного обучения с политикой, использующей только регионы, мы построим гистограммы распределения чистой стоимости согласно двум политикам. plt.figure(figsize=(10,6)) model_plot_df = (model_policy[model_policy["prediction"]>0]) sns.histplot(data=model_plot_df, x="net_value", color="C2", label="политика на основе модели МО") region_plot_df = ( model_policy[model_policy["region"].isin(regions_to_invest.keys())] ) sns.histplot(data=region_plot_df, x="net_value", label="политика на основе регионов")
Тонкая настройка политики  307 plt.title("Чистый доход от политики на основе модели: %.2f;\n" "Чистый доход от политики на основе регионов: %.2f" % (model_plot_df["net_value"].sum() / test.shape[0], region_plot_df["net_value"].sum() / test.shape[0])) plt.legend(); Как мы видим, политика на основе модели лучше, чем политика на основе признака region, но ненамного. Если политика на основе модели принесла бы нам около 16.6 реала с клиента на тестовом наборе, политика на основе региона принесла бы только 15.5 реала с клиента. Это небольше улучшение, но если у вас много клиентов, это уже может оправдать использование политики на основе модели машинного обучения вместо простой политики на основе одного признака. Тонкая настройка политики Итак, мы протестировали самую простую из всех политик, которая предполагает взаимодействие со всеми клиентами. Эту политику можно рассматривать как оценку предельной чистой стоимости (marginal net value), Ê[NetValue] > 0. Поскольку она не сработала (средний чистый доход на одного клиента был отрицательным), мы разработали единую политику, основанную на регионах: мы будем вести бизнес только в определенных регионах Ê[NetValue | Region] > 0. Эта политика уже дала нам очень хорошие результаты.
308  Курс по прогнозным моделям Затем мы попробовали политику на основе модели машинного обучения, использующей все признаки: Ê[NetValue | Region, Income, Age] > 0. Мы решили вести бизнес со всеми клиентами, для которых прогнозы чистой стоимости оказались выше нуля. Здесь решение, принимаемое на основе политики, очень простое: взаимодействовать с клиентом или не взаимодействовать. Политики, которые мы использовали до сих пор, касались бинарного случая. Они имели вид if prediction > 0 then do business else don't do business. У нас всегда был какой-то порог (threshold). Если прогноз превышает определенный порог (в нашем случае ноль, но это может быть любое значение, имеющее здравый смысл и бизнес-логику), мы принимаем одно решение, если нет – принимаем другое решение. Еще одним примером, как можно применить порог в реальной жизни, является обнаружение мошенничества при транзакциях: если оценка вероятности мошенничества превышает некоторый порог X, мы отклоняем транзакцию, в противном случае мы одобряем ее. Пороговое значение работает во многих реальных сценариях и особенно полезно, когда природа решения является бинарной. Однако мы можем вспомнить более сложные случаи. Например, вы можете потратить больше средств на маркетинг, чтобы привлечь внимание высокоприбыльных клиентов. Или вы можете добавить их в список привилегированных клиентов, которым вы будете уделять особое внимание, но это также потребует от вас больше средств. Обратите внимание, если мы учтем эти возможности, ваше решение изменится с бинарного (взаимодействовать или не взаимодействовать) в непрерывное: сколько вам следует инвестировать в клиента. В качестве следующего примера предположим, что вы решаете, не только с кем вести бизнес, но и сколько вам следует инвестировать в каждого клиента с точки зрения маркетинговых затрат. Представьте, что вы конкурируете с другими фирмами, и тот, кто больше инвестирует в конкретного клиента, получает его (так же, как на аукционе). В этом случае имеет смысл инвестировать больше в высокодоходных клиентов, меньше инвестировать в малоприбыльных клиентов и не инвестировать в неприбыльных клиентов. Один из способов сделать это – разделить ваши прогнозы на группы. Ранее мы делали это с целью сравнения моделей, но здесь мы сделаем это для принятия решений. Давайте создадим 20 групп. Опять это будут квантили или группы равного размера. Согласно нашим прогнозам, в первую группу попадут 5 % наименее прибыльных клиентов, а последняя, 20-я группа будет содержать наиболее прибыльных клиентов. Обратите внимание, что бины нужно получить на обучающем наборе и применить их к тестовому набору! По этой причине мы будем вычислять бины, используя pd.qcut() на обучающем наборе. Чтобы фактически выполнить биннинг, мы воспользуемся функцией np.digitize(), передавав ей бины, которые были предварительно вычислены в обучающем наборе.
Тонкая настройка политики  309 def model_binner(prediction_column, bins): # находим бины на обучающем наборе bands = pd.qcut(prediction_column, q=bins, retbins=True)[1] def binner_function(prediction_column): return np.digitize(prediction_column, bands) return binner_function # применяем функцию –- находим бины на обучающем наборе binner_fn = model_binner(train_pred["predictions"], 20) # применяем биннинг к прогнозам для тестового набора model_band = model_policy.assign( bands=binner_fn(model_policy["prediction"]) ) model_band.head() plt.figure(figsize=(10,6)) sns.barplot(data=model_band, x="bands", y="net_value") plt.title("Диапазоны прогнозов модели");
310  Курс по прогнозным моделям С помощью этих диапазонов мы можем большую часть наших маркетинговых инвестиций вложить в клиентов из групп 20 и 19. Обратите внимание, как мы перешли от бинарного решения (взаимодействовать или не взаимодействовать) к непрерывному: сколько инвестировать в каждого клиента с точки зрения маркетинга. Конечно, вы можете настроить процесс еще лучше, добавив больше диапазонов. Как вариант можно вообще не прибегать к биннингу. Вместо этого возьмите сырой прогноз модели и создайте правило принятия решений, например mkt_investments_i = model_prediction_i * 0,3 где в каждого i-го клиента вы инвестируете 30 % от чистой стоимости, предсказанной моделью для него (30 % – произвольное число, но суть вы поняли). Ключевые идеи Мы рассмотрели МНОГО вопросов за очень короткое время, поэтому я думаю, этот обзор чрезвычайно важен для нас, он позволяет взглянуть на наши достижения. Во-первых, мы узнали, что большинство задач машинного обуче­ния подразумевают не что иное, как получение точных прогнозов, при этом прогнозирование понимается как отображение пространства входных данных в пространство выходных данных. Кроме того, прогноз можно представить в виде оценки функции ожидания E[Y | X]. Но когда я говорю «не что иное», я не совсем честен. Мы также увидели, как хорошие прогнозы могут решить больше проблем, чем мы можем себе представить на первый взгляд, например перевод с одного языка на другой и беспилотные автомобили. Затем мы вернулись на землю и посмотрели, как хорошие прогнозы могут помочь нам в решении более общих задач, например классификации клиентов на прибыльных и неприбыльных. В частности, мы рассмотрели, как можно спрогнозировать прибыльность клиентов. Опираясь на этот прогноз, мы разработали политику, определяющую, с кем нам следует вести дела. Обратите внимание, что это всего лишь пример применения прогнозных моделей. Есть наверняка множество других сфер применения, таких как андеррайтинг кредитных карт, обнаружение мошенничества, диагностика рака и все остальное, где хорошие прогнозы могут быть полезны. Ключевой вывод здесь заключается в том, что если вы можете сформулировать свою бизнес-задачу как задачу прогнозирования, то машинное обучение, вероятно, станет подходящим инструментом. Была бы возможность, я бы подчеркнул этот тезис еще сильнее. Я чувствую, что из-за шумихи вокруг машинного обучения люди забывают об этом очень
Дополнительное чтение  311 важном моменте и часто создают модели, которые очень хорошо прогнозируют что-то совершенно бесполезное. Вместо того чтобы сформулировать бизнес-задачу в виде задачи прогнозирования, а затем решить ее с помощью машинного обучения, они часто создают прогнозную модель и пытаются увидеть, какая бизнес-задача может выиграть от этого прогноза. Это может сработать, но чаще всего это как выстрел в темноте, порождающий решения в поисках задачи. Дополнительное чтение Материал, который я написал в этой главе, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю данный материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. Наконец, я полагаю, что, возможно, был слишком поспешен в глазах тех, кто ожидал всестороннего и детального введения в машинное обучение. Честно говоря, я верю, что моя истинная ценность заключается в обучении причинно-следственному анализу, а не машинному обучению. Что касается последнего, есть множество потрясающих онлайн-ресурсов, намного лучше, чем я когда-либо мог бы создать. Классический пример — это курс по машинному обучению от Эндрю Ына (Andrew Ng), https://www.coursera.org/ learn/machine-learning, и я определенно рекомендую взглянуть на него, если вы – новичок в машинном обучении.
Глава 18 Гетерогенные эффекты воздействия и персонализация От прогнозов к анализу причинно-следственных связей В последней главе мы кратко рассмотрели модели машинного обучения. Модели машинного обучения – это инструменты для получения прогнозов, или, говоря более техническим языком, для получения оценки функции условного математического ожидания E[Y | X]. Другими словами, машинное обуче­ние невероятно полезно, когда вы хотите отобразить известные входные данные (например, предложение на английском, продажи в этом месяце, изображения сканирования мозга) в изначально неизвестные, но четко определенные выходные данные (например, предложение на японском, продажи в следующем месяце или диагностика рака мозга у новых пациентов). Итак, если машинное обучение занимается прогнозированием или оцениванием и должно приносить пользу, вы должны сформулировать любую задачу, которую хотите решить с помощью машинного обучения, в виде задачи прогнозирования, задачи, в которой оценка E[Y | X] будет играть ключевую роль. Мы рассмотрели такой пример в последней главе. Там нужно было спрогнозировать прибыльность клиента по его конкретным характеристикам: E[NetValue | Age, Income, Region]. Эта информация была очень полезной, поскольку позволила нам сосредоточить усилия на привлечении прибыльных клиентов, отказавшись от работы с неприбыльными клиентами. Здесь ключевое значение имеет правильное прогнозирование прибыльности. Обратите внимание, что это пассивный подход к оцениванию в том смысле, что вы исключаете себя из процесса генерации данных. В нашем примере
От прогнозов к анализу причинно-следственных связей  313 мы предположили, что задана прибыльность клиентов, NetValue. Нам оставалось только оценить ее. Другими словами, мы предполагали, что ничего не можем сделать с прибыльностью клиента, кроме как предсказать ее. Мы не могли ни увеличить ее, ни уменьшить. Но это не всегда так. На самом деле у компаний часто есть рычаги, которые они могут использовать для увеличения прибыльности клиентов. Эти рычаги могут быть разными, начиная с лучшего или более дешевого обслуживания клиентов и заканчивая скидками, изменениями цен либо маркетингом. В индустрии мы часто вмешиваемся в процесс генерации данных. Мы можем повлиять на него. Следовательно, как специалистам по data science, работающим в индустрии, нам часто приходится отвечать, как лучше всего действовать или какое вмешательство нужно предпринять, чтобы оптимизировать некоторые бизнес-метрики, обычно прибыльность, или какие-то другие промежуточные метрики, такие как конверсия, затраты либо продажи. В этом мире, где мы не являемся пассивными наблюдателями, оценивание E[Y | X] не дает полной картины. Здесь мы вступаем на территорию анализа причинно-следственных связей. Нам нужно добавить еще одну деталь к нашей функции условного ожидания. Именно эта деталь моделирует наше участие в процессе генерации данных. Она представляет собой воздействие: E[Y | X, T]. Теперь нам нужно провести различие между контекстом или экзогенными переменными X и воздействием T. И то, и другое влияет на результат Y, но хотя мы не контролируем X, мы можем решить, какое значение примет T, или по крайней мере повлиять на него. Приведем конкретный пример. Допустим, Y – объем продаж в течение дня, X – контекстные характеристики, которые вы не можете контролировать, но они дают вам информацию о продажах, например средние продажи за предыдущие дни, а T – переменная воздействия, которую вы можете изменить, чтобы увеличить продажи, это может быть цена, уровень запасов товаров или маркетинг. Анализ причинноследственных связей заключается в оценке причинно-следственной связи между T и Y в контексте X (или при имеющихся значениях характеристик X). После этого оптимизация Y – это просто вопрос назначения воздействия T оптимальным образом: В этом смысле, помимо положительного эффекта анализа причинно-следственных связей, у нас еще появляется нормативная мотивация. В части I мы попытались ответить на вопрос: в чем заключается ценность школьного образования? Могут ли изменения в законодательстве снизить уровень курения? Можем ли мы повысить успеваемость, имея позитивный настрой? Как алкоголь влияет на уровень смертности? Все эти вопросы интересны с чисто научной точки зрения для понимания того, как работает мир. Но за ними стоит еще и практическая мотивация. Если мы знаем влияние
314  Гетерогенные эффекты воздействия и персонализация школьного образования на доходы, мы можем понять, какая стоимость образования является разумной. Математически говоря, наши действия заключаются в том, чтобы оценить причинно-следственную связь между школьным образованием и доходами и оптимизировать ее: В части I мы сфокусировались на вопросе, было ли воздействие в целом положительным, сильным или нулевым. Например, мы хотели выяснить, является вложение в образование в целом хорошей идеей или нет. Кроме того, в части I роль X была двоякой. Во-первых, набор характеристик X мог содержать спутывающие факторы, и в этом случае причинно-следственный эффект можно было идентифицировать только при учете X. Или X мог влиять на уменьшение дисперсии оценки причинно-следственного эффекта. Если набор характеристик X являлся хорошим предиктором Y, мы могли использовать его для объяснения дисперсии Y, что делало причинно-следственный эффект более очевидным. Теперь все станет менее четким. Нам нужно больше, чем просто средний эффект воздействия. Мы позволим воздействию положительно влиять на некоторых людей, но не на всех. Контекстные характеристики X будут играть роль в определении различных профилей объектов, и каждый профиль может по-разному реагировать на воздействие. Теперь мы хотим персонализировать воздействие, применяя его только к тем объектам, которые наилучшим образом на него реагируют. Мы переходим из мира, где нас интересовал только средний эффект воздействия, в мир, где нам интересен гетерогенный эффект воздействия. От ATE к CATE До сих пор каждый раз, когда мы оценивали причинно-следственный эффект воздействия, речь шла о среднем эффекте воздействия (или, иногда, о локальном среднем эффекте воздействия): E[Y1 – Y 0 ] или эквиваленте непрерывного воздействия E[y′(t)], где y′(t) – производная функции отклика или результата относительно воздействия1. Мы изучили методы для выявления общей эффективности воздействия. Оценка ATE (среднего эффекта воздействия) является основой анализа причинно-следственных связей. Это чрезвычайно полезный инстру1 Здесь производная показывает, как быстро изменяется результат по сравнению с изменением воздействия. Тут нас могут интересовать изменения среднего значения результата или изменения в форме функции отклика. – Прим. перев.
От ATE к CATE  315 мент для задачи принятия решений, которую мы называем оценкой программ (program evaluation). Мы хотим знать, следует ли распространять воздействие на всю популяцию. Его можно применять и в государственной политике. Один и тот же метод можно использовать и для оценки эффективности национальной образовательной или здравоохранительной программы, и для определения влияния запуска нового продукта на финансовые показатели компании. Решение, которое мы хотим принять, заключается в том, следует нам оказывать воздействие или не следует. Теперь мы попытаемся принять другое решение: на кого мы воздействуем? Мы позволяем нашему решению меняться от одного объекта к другому. Может быть полезно воздействовать на один объект, а другой объект оставить без воздействия. Мы хотим персонализировать воздействие. Говоря более техническим языком, мы хотим оценить условный средний эффект воздействия (Conditional Average Treatment Effect – CATE): E[Y1 – Y 0 | X ] или E[y′(t) | X ]. Y (Sales, Conversion, Profit) Обусловленность по X означает, что мы теперь позволяем эффекту воздействия быть разным в зависимости от характеристик каждого объекта. Опять же, здесь мы полагаем, что не все объекты одинаково хорошо реагируют на воздействие. Мы хотим воспользоваться этой гетерогенностью. Мы хотим подвергнуть воздействию только подходящие, «правильные» объекты (в бинарном случае) или определить оптимальную величину воздействия для каждого объекта (в случае непрерывного воздействия). Например, если вы – банк, который должен решить, какой кредит может получить каждый клиент, вы уверены, что не стоит выдавать много денег всем подряд – хотя это может быть разумно для некоторых клиентов. Вам нужно просчитать ваше воздействие (сумму кредита). Возможно, отталкиваясь от кредитного рейтинга клиента (X), вы сможете определить, какая сумма кредита будет правильной. Конечно, вам не нужно быть крупным учреждением, чтобы воспользоваться персонализацией. Нет недостатка в примерах, иллюстрирующих персонализацию. В какие дни года следует проводить распродажи? Сколько стоит взимать за тот или иной продукт? Какое количество упражнений является избыточным для того или иного человека? Представьте себе следующее. У вас есть множество клиентов и воздействие (цена, скидка, кредит, ...). Вы хотите персонализировать воздействие, например предоставить различные скидки разным клиентам. T (Price, Discount, Interest Rate…)
316  Гетерогенные эффекты воздействия и персонализация Чтобы сделать это, вам нужно разделить (сегментировать) клиентов. Вы создали группы, которые по-разному реагируют на ваше воздействие. Например, вы хотите найти клиентов, которые хорошо реагируют на скидки, и клиентов, которые реагируют на них слабо. Отклик клиента на воздействие задается условным эффектом воздействия . Таким образом, мы можем каким-то образом оценить этот эффект для каждого клиента, а затем объединить вместе тех, кто отлично реагирует на воздействие (высоковыраженный эффект воздействия), и тех, кто не очень хорошо реагирует на воздействие. Если бы мы сделали это, мы разделили бы пространство клиентов примерно так, как изображено на следующем рисунке. Band 1 Y Band 2 Band 3 T Y Y Это было бы замечательно, потому что теперь мы могли бы оценить различные эффекты воздействия или чувствительности по каждой группе. И обратите внимание, что чувствительность – это просто наклон линии или функции, идущей от T к Y. Таким образом, мы можем разбить пространство клиентов на части, в которых наклон или чувствительность будут различными, это означает, что объекты в этих группах по-разному реагируют на воздействие. Band 1 = High Elasticity Band 2 = Med Elasticity T Y T Band 3 = Low Elasticity T
От ATE к CATE  317 Другими словами, вам нужно отойти от прогнозирования в его чистом виде и начать прогнозировать производную Y по T, для каждого объекта. Например, предположим, что Y – это продажи мороженого, T – это цена мороженого, а каждый объект i – это день. Давайте отложим в сторону моральные вопросы ради аргументации и притворимся, что вы можете менять цену мороженого каждый день. – производная продаж мороженого относительно цены мороженого или чувствительность к цене мороженого. Она измеряет, насколько изменятся продажи при изменении цены. Если каким-то образом вы сможете найти дни, в которые чувствительность к цене яв­ляется низкой, вы можете повысить цены, не потеряв много в количестве проданного мороженого в эти дни. Возможно, вы уже это делаете, например когда повышаете цены во время праздников или выходных. Суть в том, что полезно дифференцировать дни с точки зрения чувствительности к цене, потому что это дает вам некоторую основу для оптимального ценообразования. Хорошо, вы можете так сделать, но это довольно сложно. Как я могу спрогнозировать чувствительность , если я не могу наблюдать ее? Это очень хороший вопрос. Чувствительность, по сути, не поддается наблюдению на уровне объекта. Мало того, это вообще странное понятие. Мы гораздо больше привыкли думать в терминах исходных величин, а не в терминах скорости изменения этих самых величин. Чтобы лучше понять чувствительность, воспользуемся небольшой хитростью. Представьте, что по каждому объекту (дню) известно не только значение Y (в нашем случае – объем продаж мороженого), но и индивидуальная чувствительность (в нашем случае – чув- ствительность к цене мороженого). Напомним, чувствительность измеряет, насколько изменится Y при изменении T, поэтому у каждого объекта будет коэффициент наклона, связанный с его чувствительностью . Применитель- Y но к нашему примеру мы можем сказать, что каждый день имеет свой коэффициент наклона (свое значение производной продаж относительно цены). T
318  Гетерогенные эффекты воздействия и персонализация Конечно, мы не можем наблюдать эти индивидуальные коэффициенты наклона. Чтобы увидеть индивидуальные коэффициенты наклона, нам нужно фиксировать продажи каждый день при двух разных ценах, а затем вычислить, как меняются продажи для каждой из этих цен: Вот и снова перед нами встает фундаментальная проблема анализа причинно-следственных связей. Мы не можем наблюдать один и тот же объект в различных условиях воздействия (в нашем случае мы не можем наблюдать продажи в один и тот же день, установив две разные цены). Итак, что мы можем сделать? Прогнозирование чувствительности Мы попали в сложную ситуацию. Мы согласились, что нам нужно предсказывать индивидуальную чувствительность , которая, к сожалению, не подда- ется наблюдению. Так что нам не удастся использовать алгоритм машинного обучения и подставить чувствительность в качестве зависимой переменной. Но, возможно, нам вовсе не обязательно наблюдать индивидуальную чувствительность , чтобы предсказать ее. Есть идея. А что, если мы воспользуемся линейной регрессией? Я
Прогнозирование чувствительности  319 Допустим, вы обучаете следующую линейную модель на ваших данных: y i = β 0 + β 1t i + β 2 X i + e i. Если вы продифференцируете ее по воздействию, то получите И поскольку вы можете оценить модель, приведенную выше, чтобы получить βˆ1, вы можете предсказать чувствительность, даже если у вас нет возможности наблюдать ее. В данном случае это довольно простое предсказание, то есть мы предсказываем постоянное значение β1 для каждого объекта. Это уже что-то, но все еще не то, что нам нужно. Это ATE, а не CATE. Это не помогает нам в нашей задаче группировки объектов в зависимости от того, как они отреагировали на воздействие, потому что по всем объектам сейчас дан одинаковый прогноз чувствительности. Однако мы можем внести следующее простое изменение y i = β 0 + β 1t i + β 2 X i + β 3 t i X i + e i, что, в свою очередь, даст нам следующий прогноз чувствительности: где β3 – это вектор коэффициентов для набора признаков X. Теперь каждый объект с разными значениями признаков Xi будет иметь разный прогноз чувствительности. Другими словами, прогноз чувствительности будет меняться при изменении X. Регрессия может дать нам способ оценки CATE E[y′(t) | X ]. Мы наконец-то к чему-то пришли. Вышеприведенная модель позволяет нам предсказывать чувствительность для каждого объекта. С помощью этих предсказаний мы можем создать более полезные группы. Мы можем сгруппировать вместе объекты с высокой предсказанной чувствительностью и сделать то же самое с теми, у кого низкая предсказанная чувствительность. Наконец, с помощью наших прогнозов чувствительности мы можем проранжировать объекты по степени выраженности реакции на воздействие. Достаточно теории на данный момент. Пора приступить к примеру, который проиллюстрирует создание такой модели чувствительности. Давайте рассмотрим наш пример с мороженым. Каждый объект i – это день. По каждому дню у нас есть информация, является он будним днем или нет, сведения о затратах на производство мороженого (здесь мы будем считать затраты показателем качества мороженого) и средняя температура в этот день. Это будет нашим пространством признаков X. Помимо этого, у нас есть наше воздействие – цена – и наш результат – количество проданных единиц моро-
320  Гетерогенные эффекты воздействия и персонализация женого. Для этого примера предположим, что воздействие происходит случайным образом, поэтому нам не нужно сейчас беспокоиться о смещении. import pandas as pd import numpy as np from matplotlib import pyplot as plt %matplotlib inline import seaborn as sns import statsmodels.formula.api as smf import statsmodels.api as sm from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import train_test_split prices_rnd = pd.read_csv("data/ice_cream_sales_rnd.csv") print(prices_rnd.shape) prices_rnd.head() (5000, 5) Вспомним нашу задачу: нам необходимо решить, когда устанавливать более высокие или более низкие цены на мороженое в зависимости от конкретных особенностей дня: температуры (temp), дня недели (weekday) и затрат на производство мороженого (cost). Модель гетерогенного эффекта воздействия требует проверки качества. Мы вернемся к этому вопросу в ближайший момент (а также куда более глубоко в следующей главе), а пока давайте просто разделим данные на обучающий и тестовый наборы. np.random.seed(123) train, test = train_test_split(prices_rnd) Теперь, когда у нас есть обучающие данные, нужно создать модель, которая позволит нам отличать дни с высокой чувствительностью к цене от дней с низкой чувствительностью к цене. Наш подход заключается в том, чтобы просто прогнозировать чувствительность к цене. Как именно? Сначала давайте рассмотрим следующую линейную модель: salesi = β0 + β1pricei + β2 Xi + ei.
Прогнозирование чувствительности  321 Давайте посмотрим параметры модели. m1 = smf.ols("sales ~ price + temp+C(weekday)+cost", data=train).fit() m1.summary().tables[1] Для модели m1 спрогнозированная чувствительность к цене будет определяться значением βˆ1, которое в нашем случае равно –2.75. Это означает, что каждый дополнительный бразильский реал, который мы заложим в цену нашего мороженого, дает уменьшение продаж примерно на 3 единицы. Обратите внимание, модель m1 предсказывает одинаковую чувствительность по каждому объекту. Таким образом, это не очень хорошая модель, если мы хотим выяснить, в какие дни люди демонстрируют меньшую чувствительность к цене на мороженое. Она оценивает ATE, тогда как нам здесь нужен CATE. Помните, что наша цель – разделить объекты таким образом, чтобы мы могли персонализировать и оптимизировать наше воздействие (цену) для каждой конкретной группы. Если все прогнозы одинаковы, то разделения мы не получим. Мы не различаем объекты (дни) с высокой чувствительностью к цене от объектов (дней) с низкой чувствительностью к цене. Для исправления этого недостатка рассмотрим нашу вторую модель: salesi = β0 + β1pricei + β2 pricei ∗ tempi + β3 Xi + ei. В эту вторую модель включен член взаимодействия (interaction term) между ценой и температурой. Это означает, что модель позволяет чувствительности к цене варьировать в зависимости от различных значений температуры. Фактически мы здесь говорим, что люди более или менее чувствительны к увеличению цены в зависимости от температуры. m2 = smf.ols("sales ~ price*temp + C(weekday) + cost", data=train).fit() m2.summary().tables[1]
322  Гетерогенные эффекты воздействия и персонализация После того как мы оценили модель, чувствительность к цене можно спрогнозировать с помощью следующей формулы: Обратите внимание, что значение βˆ2 является положительным и равно 0.03, а базовая чувствительность к цене βˆ1 (чувствительность при температуре 0 °C) составляет –3.6. Это означает, что в среднем с увеличением цены продажи уменьшаются, что логично. Кроме того, это означает, что с увеличением температуры люди становятся менее чувствительными к увеличению цен на мороженое (впрочем, чувствительность к цене не уменьшается слишком сильно). Например, при 25 °C каждый дополнительный бразильский реал, который мы взимаем, уменьшает наши продажи на 2.8 единицы (–3.6 + (0.03 × 25)). А при 35 °C каждый дополнительный бразильский реал, который мы взимаем, уменьшает наши продажи всего на 2.5 единицы (–3.6 + (0.03 × 35)). Это тоже вполне логично. По мере того как дни становятся все жарче, люди готовы заплатить бóльшую сумму за мороженое. Мы можем пойти еще дальше. В следующей модели включены члены взаи­ модействия для всего пространства признаков. Это означает, что чувствительность будет изменяться в зависимости от температуры, дня недели и затрат: salesi = β0 + β1pricei + β2 Xi ∗ pricei + β3 Xi + ei. m3 = smf.ols( "sales ~ price*cost + price*C(weekday) + price*temp", data=train).fit()
Прогнозирование чувствительности  323 Согласно вышеприведенной модели чувствительность на уровне объекта или CATE определяется следующим образом: где β1 – это коэффициент цены, а β2 – это вектор коэффициентов взаимодействий. Наконец, давайте посмотрим, как нам получить прогнозы чувствительности. Один из способов – извлечь параметры модели и использовать вышеуказанную формулу. Однако мы прибегнем к более общей аппроксимации. Поскольку чувствительность представляет собой не более чем производную результата относительно воздействия, мы можем воспользоваться определением производной: где ϵ стремится к нулю. Мы можем аппроксимировать это выражение, заменив ϵ на 1: где ŷ – это прогнозы нашей модели. Проще говоря, я буду делать два прогноза с помощью модели m1: один по исходным данным, а другой по исходным данным, но с воздействием, увеличенным на единицу. Разница между этими прогнозами – это мой прогноз CATE. Ниже приведена функция для вычисления этой разницы. Поскольку мы использовали обучающий набор для оценки нашей модели, мы теперь получим прогнозы для тестового набора. Сначала давайте воспользуемся нашей первой моделью для оценки ATE m1. def pred_sensitivity(m, df, t="price"): return df.assign(**{ "pred_sens": m.predict(df.assign(**{t:df[t]+1})) - m.predict(df) }) pred_sensitivity(m1, test).head()
324  Гетерогенные эффекты воздействия и персонализация Получение прогнозов чувствительности с помощью модели m1 ничем не примечательно. Мы видим, что модель предсказывает одно и то же значение для всех дней. Это происходит потому, что в данной модели отсутствуют члены взаимодействия. Однако если мы получим прогнозы с помощью модели m3, то увидим, что модель m3 выдает разные прогнозы чувствительности. Теперь чувствительность, или эффект воздействия, зависит от характеристик конкретного дня. pred_sens3 = pred_sensitivity(m3, test) np.random.seed(1) pred_sens3.sample(5) Обратите внимание, что прогнозы представляют собой числа от –9 до 1. Это не прогнозы продаж, которые исчисляются сотнями. Это прогноз величины изменения продаж при увеличении цены на одну единицу. Сразу же мы видим некоторые странные цифры. Например, взгляните на день 4764. Для него спрогнозирована положительная чувствительность. Другими словами, мы прогнозируем, что продажи увеличатся, если мы повысим цену на мороженое. Это не соответствует здравому смыслу. Вероятно, модель выполнила какую-то странную экстраполяцию в рамках этого прогноза. К счастью, вам не нужно слишком беспокоиться об этом. Помните, что наша конечная цель – сегментировать объекты по степени их чувствительности к воздействию. Это нам нужно не для того, чтобы получить самый точный прогноз чувствительности. В рамках нашей задачи достаточно, если прогнозы позволяют упорядочить объекты в соответствии с их чувствительностью. Другими словами, даже если положительные прогнозы чувствительности, такие как 1.1 или 0.5, не имеют особого смысла, все, что нам нужно, – это правильный порядок, то есть мы хотим, чтобы на объект с прогнозом 1.1 рост цен влиял меньше, чем на объект с прогнозами 0.5. Итак, у нас есть модель чувствительности, или CATE. Но все еще остается вопрос: как она соотносится с прогнозной моделью машинного обучения? Давайте попробуем ответить на него сейчас. Мы применим алгоритм машинного обучения, который использует в качестве признаков X цену (price), температуру (temp), день недели (weekday) и затраты (cost) и пытается предсказать продажи мороженого.
Прогнозирование чувствительности  325 X = ["temp", "weekday", "cost", "price"] y = "sales" ml = GradientBoostingRegressor() ml.fit(train[X], train[y]) # оцениваем качество модели ml.score(test[X], test[y]) 0.9124088322890127 Эта модель предсказывает ежедневное количество продаж. Но подходит ли она для нашей задачи? Другими словами, способна ли эта модель отделять дни с более высокой чувствительностью к цене на мороженое от дней с более низкой чувствительностью? Может ли она помочь нам определить, какую цену на мороженое стоит устанавливать в зависимости от этой чувствительности? Чтобы понять, какая из моделей более полезна, давайте попробуем использовать их для группировки объектов. Для каждой модели мы выполним разбиение объектов (дней) на две группы. Мы предполагаем, что одна группа объектов сильно реагирует на увеличение цен, тогда как другая группа объектов реагирует не так сильно. Если это так, мы можем организовать наш бизнес, исходя из результатов этой группировки: для дней, входящих в группу с высокой реакцией на увеличение цен, лучше не устанавливать слишком высокие цены. Для дней, входящих в группу с низкой реакцией, мы можем увеличить цены без сильного риска для наших продаж. bands_df = pred_sens3.assign( # создаем 2 группы на основе прогнозов чувствительности к цене sens_band = pd.qcut(pred_sens3["pred_sens"], 2), pred_sales = ml.predict(pred_sens3[X]), # создаем 2 группы на основе прогнозов объемов продаж pred_band = pd.qcut(ml.predict(pred_sens3[X]), 2), ) bands_df.head() Далее нам нужно сравнить, какая из этих группировок является лучшей. Возможно, я забегаю вперед, поскольку следующая глава будет целиком посвящена оценке качества модели CATE. Но давайте быстро взглянем, как выглядит проверка такой модели. Один очень простой способ проверить, насколько хороши эти две группировки (под хорошими я подразумеваю по-
326  Гетерогенные эффекты воздействия и персонализация лезные), – это построить линию регрессии (зависимость продаж от цен) для каждой группировки. Мы можем легко выполнить это с помощью функции regplot() библиотеки Seaborn в сочетании с классом FacetGrid этой же биб­ лиотеки. Ниже мы видим линии регрессии для группировки на основе прогнозов чувствительности. Помните, что все это делается на тестовом наборе. g = sns.FacetGrid(bands_df, col="sens_band") g.map_dataframe(sns.regplot, x="price", y="sales") g.set_titles(col_template="Sens. Band {col_name}"); Как мы видим, данная группировка выглядит полезной. Для первой группы существует высокая чувствительность к цене. Продажи существенно падают по мере роста цен. Однако для второй группы продажи остаются примерно неизменными при росте цен. На самом деле даже кажется, что продажи рас­ тут по мере повышения цены, но это, наверное, шум. Сравним эти результаты с результатами группировки на основе прогнозов объемов продаж, полученных с помощью прогнозной модели машинного обучения: g = sns.FacetGrid(bands_df, col="pred_band") g.map_dataframe(sns.regplot, x="price", y="sales") g.set_titles(col_template="Pred. Band {col_name}");
Ключевые идеи  327 Мне очень нравятся эти графики, потому что они передают довольно важную мысль. Как видите, объекты сильно отличаются по оси Y (по количеству проданного мороженого). В дни, соответствующие первой группе, мы продаем не так много мороженого, но в дни, соответствующие второй группе, мы продаем больше. Я нахожу это удивительным, потому что прогнозная модель делает именно то, что должна делать: она прогнозирует продажи. Она может отличать дни, в которые будут низкие продажи мороженого, от дней с высокими продажами мороженого. Единственная проблема в том, что прогнозирование здесь не особенно полезно. В конечном счете мы хотим знать, когда мы можем повысить цены, а когда нет. Но как только мы посмотрим на наклоны линий в обеих группах, полученных на основе прогнозов модели, мы увидим, что эти наклоны не сильно меняются. Другими словами, как выяснила наша прогнозная модель, обе группы примерно одинаково реагируют на рост цен. Это не позволяет нам понять, в какие дни мы можем повысить цены, поскольку похоже, что цена вообще не влияет на продажи. Ключевые идеи Мы, наконец, дали формальное определение условного среднего эффекта воздействия и выяснили, каким образом он может быть полезен для персонализации. Если мы сможем понять, как каждый объект реагирует на воздействие, то есть если мы сможем оценить гетерогенность эффекта воздействия, мы сможем предложить лучшее, более адресное воздействие в зависимости от индивидуальных характеристик объекта. Мы также противопоставили эту цель цели прогнозной модели. А именно мы переосмысливаем задачу оценивания: от прогноза Y в ее исходном виде мы переходим к прогнозу изменения Y при изменении T, . К сожалению, совсем не очевидно, как строить такие модели. Поскольку мы не можем наблюдать чувствительность напрямую, сложно создать модель, которая бы предсказывала ее. Но нам на помощь пришла линейная регрессия. Используя регрессионную модель, обученную прогнозировать Y, мы также нашли способ прогнозировать . Нам еще пришлось включить интеракции воздействия и признаков. В результате наш прогноз чувствительности получился разным для каждого клиента. Другими словами, теперь мы стали оценивать E[Y′(t) | X]. Эти прогнозы чувствительности затем использовались для группировки наших объектов на более или менее чувствительные к воздействию, что в конечном итоге помогло нам определить уровень воздействия для каждой группы.
328  Гетерогенные эффекты воздействия и персонализация ВАМ НУЖНО ЧТО-ТО СМОДЕЛИРОВАТЬ? ЛИНЕЙНАЯ РЕГРЕССИЯ ПРЯМО СЕЙЧАС НИКАКИХ ДЕРЕВЬЕВ РЕШЕНИЙ, НЕЙРОННЫХ СЕТЕЙ, НИЧЕГО НЕ НАДО ЛИНЕЙНАЯ РЕГРЕССИЯ В связи со всем этим возникает естественный вопрос: можем ли мы заменить линейную регрессию общей моделью машинного обучения и использовать ее для прогнозирования чувствительности? Ответ будет положительным, но есть некоторые предостережения. В этой главе использовалась очень простая модель CATE, поскольку я думаю, что концепцию, лежащую в ее основе, легче понять с помощью линейной регрессии. Но не волнуйтесь. В следующих главах мы увидим некоторые более сложные модели. Однако перед этим мне сначала нужно затронуть очень важную тему: как сравнить две модели CATE и выбрать лучшую из них. Дополнительное чтение Материал, который я написал в этой главе, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю этот материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом.
Глава 19 Оценка качества причинно-следственных (каузальных) моделей В подавляющем большинстве материалов, посвященных причинноcти, исследователи используют синтетические данные, чтобы проверить эффективность примененных методов. Они генерируют данные как для Y0i, так и для Y1i, чтобы проверить, правильно ли их модель улавливает эффект воздействия Y1i – Y0i. Подобный сценарий подходит для академических целей, но в реальном мире у нас нет такой возможности. Применяя эти методы в промышленности, клиенты будут постоянно спрашивать нас, почему наша модель лучше, почему она должна заменить модель, используемую сейчас в производстве, или почему она не потерпела сокрушительную неудачу. Это настолько важно, что я не понимаю, почему отсутствует материал, объясняющий, как мы должны оценивать модели причинно-следственных связей на реальных данных. В результате специалистам по data science, которые хотят применить модели причинно-следственных связей, очень трудно убедить управленческий состав довериться им. Подход, который выбирают специалисты по data science, заключается в том, чтобы показать состоятельность теории и тщательную проработку процеса обучения модели. К сожалению, в мире, где нормой является парадигма разделения на обучающий и тестовый наборы, этого недостаточно. Качество вашей модели должно быть обосновано на более конкретных доказательствах, чем красивая теория. Подумайте об этом. Машинное обучение добилось своего огромного успеха только потому, что валидация прогнозных моделей очень проста. Есть что-то обнадеживающее в том, что прогнозы соответствуют реальным событиям. К сожалению, совсем не очевидно, как нам в анализе причинно-следственных связей найти что-то, аналогичное парадигме разбиения данных на обучающий и тестовый наборы. Это обусловлено тем, что задача анализа причинно-следственных связей заключается в том, чтобы оценить ненаблю-
330  Оценка качества причинно-следственных (каузальных) моделей даемую величину . Ну, если мы не можем наблюдать величину , то как же нам узнать, насколько хороши модели, пытающиеся оценить ее? Представим, что у каждого объекта есть соответствующая чувствительность, обозначаемая наклоном линии, идущей от воздействия к результату, но мы не можем измерить ее. Это очень-очень сложно осознать, и мне потребовались годы, чтобы найти хоть что-то, похожее на ответ. Это не окончательный ответ, но на практике он работает и обладает той конкретикой, которая, я надеюсь, приблизит анализ причинно-следственных связей к парадигме разделения на обучающий и тестовый наборы, аналогичной той, которая у нас есть в машинном обучении. Секрет заключается в использовании агрегированных показателей чувствительности. Даже если вы не можете оценить чувствительность индивидуально, вы можете оценить ее для группы, и именно этот подход мы будем использовать здесь. import pandas as pd import numpy as np from matplotlib import pyplot as plt import seaborn as sns from toolz import curry import statsmodels.formula.api as smf import statsmodels.api as sm from sklearn.ensemble import GradientBoostingRegressor import warnings warnings.filterwarnings("ignore") В этой главе для оценивания наших причинно-следственных моделей мы воспользуемся неслучайными данными, а для оценки качества полученных моделей применим случайные данные. Снова речь пойдет о том, как цена
Оценка качества причинно-следственных (каузальных) моделей  331 влияет на продажи мороженого. Как мы увидим, случайные данные очень полезны для оценки качества. Однако в реальной жизни сбор случайных данных часто является дорогостоящим мероприятием (зачем вам устанавливать цены случайным образом, если вы знаете, что некоторые цены будут неразумны и только приведут к потере денег???). Часто случается так, что у нас бывает избыточный объем данных, в которых воздействие НЕ является случайным, и очень маленький объем, если вообще есть, случайных данных. Оценка модели, построенной на неслучайных данных, чрезвычайно сложна, поэтому если у нас есть хотя бы небольшое количество случайных данных, мы обычно оставляем их для проверки качества. И на всякий случай покажем, как выглядят эти данные. # загружаем неслучайные данные prices = pd.read_csv("data/ice_cream_sales.csv") # загружаем случайные данные prices_rnd = pd.read_csv("data/ice_cream_sales_rnd.csv") print(prices_rnd.shape) prices.head() (5000, 5) Чтобы было с чем сравнивать, давайте обучим две модели. Первая модель будет линейной регрессией со взаимодействиями, чтобы чувствительность могла варьировать от объекта к объекту: salesi = β0 + β1pricei + β2 Xi + β3 Xi pricei + ei. Как только мы обучим эту модель, мы можем получить прогнозы чувствительности: Обратите внимание, что здесь нам не важен β2. β1 и β3 отвечают за влияние воздействия (цены), тогда как β2 относится к влиянию признаков X. β2 измеряет, как изменения в признаках влияют на результат (продажи) независимо от воздействия (цены). Однако нас интересует именно оценка эффекта воздействия, поэтому важными являются β1 и β3, связанные с самим воздействием. β1 оценивает базовый эффект воздействия (эффект воздей-
332  Оценка качества причинно-следственных (каузальных) моделей ствия при отсутствии взаимодействия с признаками), тогда как β3 отражает взаимодействие между воздействием и признаками. m1 = smf.ols( "sales ~ price*cost + price*C(weekday) + price*temp", data=prices).fit() Вторая модель будет полностью непараметрической, моделью машинного обучения, прогнозной моделью: salesi = G(Xi, pricei ) + ei. X = ["temp", "weekday", "cost", "price"] y = "sales" np.random.seed(1) m2 = GradientBoostingRegressor() m2.fit(prices[X], prices[y]); Чтобы просто убедиться в отсутствии у модели сильного переобучения, мы можем вычислить R2 для данных, использованных при обучении модели, а также для новых, неизвестных для модели данных. (Для тех, кто лучше знаком с машинным обучением, обратите внимание, что ожидается некоторое снижение качества из-за изменения концепции. Модель была обучена на данных, в которых цена не является случайной, но тестовый набор содержит только случайно назначенные цены.) print("R2 на обучающем наборе:", m2.score(prices[X], prices[y])) print("R2 на тестовом наборе:", m2.score(prices_rnd[X], prices_rnd[y])) R2 на обучающем наборе: 0.9251704824568053 R2 на тестовом наборе: 0.7711074163447711 После обучения наших моделей мы можем получить чувствительность. Мы снова прибегнем к численной аппроксимации: Наши модели были обучены на неслучайных данных. Теперь мы обращаемся к случайным данным для получения прогнозов. Чтобы все было в одном месте, мы добавим прогнозы, полученные с помощью модели машинного обучения, и прогнозы чувствительности, полученные с помощью причинноследственной модели, в единый датафрейм под названием prices_rnd_pred. Кроме того, давайте включим модель, выдающую случайные прогнозы. Идея заключается в том, что эта модель просто выдает случайные числа в качестве прогнозов. Очевидно, что она не очень полезна, но она может служить отличным бенчмарком. Когда речь идет о новых методах оценки, всегда полезно посмотреть, как справилась бы бесполезная модель, генерирующая случайные прогнозы. Если модель, генерирующая случайные прогнозы, дает
­ Чувствительность по диапазонам прогнозов модели  333 хорошее качество в соответствии с выбранным критерием оценки, это уже дает определенную информацию о качестве выбранного критерия. def predict_sensitivity(model, price_df, h=0.01): return (model.predict(price_df.assign(price=price_df["price"]+h)) - model.predict(price_df)) / h np.random.seed(123) prices_rnd_pred = prices_rnd.assign(**{ # модель чувствительности "sensitivity_m_pred": predict_sensitivity(m1, prices_rnd), # прогнозная модель "pred_m_pred": m2.predict(prices_rnd[X]), # модель, выдающая случайные прогнозы "rand_m_pred": np.random.uniform(size=prices_rnd.shape[0]) }) prices_rnd_pred.head() Чувствительность по диапазонам прогнозов модели Теперь, когда у нас есть прогнозы, нам нужно оценить их качество. И помните, мы не можем наблюдать чувствительность, поэтому нет простого фактического значения, с которым можно было бы сравнить наш прогноз. Вместо этого давайте зададимся вопросом, чего мы хотим от моделей чувствительности. Возможно, это даст нам некоторые идеи по поводу оценки их качества. Идея построения моделей, оценивающих чувствительность к воздействию, возникла в силу необходимости выяснить, какие объекты более чувствительны к воздействию, а какие менее чувствительны. Она возникла из желания персонализировать воздействие. Возможно, маркетинговая кампания очень эффективна только для одного сегмента популяции. Вероятно, скидки работают только для некоторых групп клиентов. Хорошая причинно-следственная модель должна помочь нам выяснить, какие клиенты будут лучше или хуже реагировать на предлагаемое воздействие. Она должна обладать способностью дискриминировать (различать) объекты по степени их эластичности
334  Оценка качества причинно-следственных (каузальных) моделей или чувствительности к воздействию. В нашем примере с мороженым модель должна определить, в какие дни люди готовы больше тратить на мороженое или в какие дни чувствительность к цене является менее отрицательной. Если это объявлено целью, было бы очень полезно упорядочить объекты от более чувствительных к менее чувствительным. Поскольку у нас есть спрогнозированная чувствительность, мы можем упорядочить объекты в соответствии с прогнозами и надеяться, что это также упорядочит объекты по их фактической чувствительности. К сожалению, мы не можем оценить это упорядочивание на уровне отдельных объектов. Но что, если нам это и не нужно? Что, если вместо этого мы оценим порядок не объектов, а групп? Если наше воздействие распределено случайным образом (и вот где вступает в свои права случайность), оценка чувствительности для группы объектов довольно проста. Все, что нам нужно, – это сравнить результаты между объектами, испытавшими воздействие, и объектами, не испытавшими воздействия. Чтобы лучше это понять, полезно представить себе ситуацию с бинарным воздействием. Давайте опять возьмем пример с ценообразованием, но теперь воздействием будет скидка. Другими словами, цены могут быть либо высокими (нет воздействия), либо низкими (есть воздействие). Нарисуем графики, отложим продажи по оси Y, прогноз каждой панели – по оси X, а цветом обозначим цену. Таким образом, будет три графика. Затем мы можем разбить прогнозы моделей на три группы равного размера (три нижних графика). Если воздействие было случайно назначенным, мы можем легко оценить ATE для каждой группы E[Y | T = 1] – E[Y | T = 0]. На рисунке мы видим, что первая модель довольно хорошо прогнозирует продажи (высокая корреляция с продажами), но группы, которые она формирует, характеризуются примерно одинаковым эффектом воздействия, как показано на графике внизу слева. Два из трех сегментов имеют одинаковую чувствительность, и только последний имеет другую, более низкую чувствительность.
Чувствительность по диапазонам прогнозов модели  335 С другой стороны, каждая группа, созданная второй моделью, имеет разный причинно-следственный эффект (график внизу посередине). Это признак того, что данная модель действительно может быть полезной для персонализации. Наконец, модель, генерирующая случайные прогнозы, создает группы с одинаковой чувствительностью (график внизу справа). Это не очень полезно, но ожидаемо. Если модель генерирует случайные прогнозы, каждый создаваемый сегмент, по сути, будет случайной и репрезентативной выборкой данных. Поэтому чувствительность в группах, созданных на основе прогнозов случайной модели, должна быть примерно такой же, как ATE на всем наборе данных. Просто глядя на эти графики, уже можно определить, какая модель лучше. Чем более упорядоченными выглядят значения чувствительности и чем больше они различаются по диапазонам, тем лучше. Здесь, вероятно, модель 2 лучше модели 1, которая, вероятно, лучше случайной модели. Чтобы обобщить это на случай непрерывных данных, мы можем оценить чувствительность, используя модель линейной регрессии с одной переменной: y i = β 0 + β 1t i + e i. Мы можем применить нашу модель к конкретной группе и так оценить чувствительность внутри данной группы. Из теории по простой линейной регрессии нам известно, что где –t – это средневыборочное воздействие, а – y – это средневыборочный результат. Вот как это выражение (наша модель чувствительности) выглядит в программном коде: @curry def sensitivity(data, y, t): # коэффициент наклона для одномерной линейной регрессии return (np.sum((data[t] - data[t].mean())*(data[y] - data[y].mean())) / np.sum((data[t] - data[t].mean())**2)) Давайте теперь применим эту функцию к данным о ценах на мороженое. Для этого нам нужна функция, которая разбивает прогнозы наших моделей на 10 равных по размеру бинов и применяет нашу модель чувствительности, реализованную в функции sensitivity(), к каждому из них. Следующий программный код должен справиться с этой задачей. def sensitivity_by_band(df, pred, y, t, bands=10): return (df # разбиваем на квантили .assign(**{f"{pred}_band":pd.qcut(df[pred], q=bands)}) .groupby(f"{pred}_band") # оцениваем чувствительность по каждому квантилю .apply(sensitivity(y=y, t=t)))
336  Оценка качества причинно-следственных (каузальных) моделей Наконец, давайте построим график чувствительности по бинам (диапазонам), используя прогнозы, которые мы получили ранее. Здесь мы берем каждую модель, разбиваем ее прогнозы на бины, а затем оцениваем чувствительность для каждого бина. fig, axs = plt.subplots(1, 3, sharey=True, figsize=(10, 4)) for m, ax in zip(["sensitivity_m_pred", "pred_m_pred", "rand_m_pred"], axs): sensitivity_by_band(prices_rnd_pred, m, "sales", "price").plot.bar(ax=ax) Во-первых, рассмотрим модель, генерирующую случайные прогнозы (rand_m). Она дает примерно одинаковую оценку чувствительности для каждого бина. Мы уже понимаем, просто глядя на график, что она не поможет нам с персонализацией, поскольку не способна отличать дни с высокой чувствительностью к ценам от дней с низкой чувствительностью. Затем рассмотрим прогнозную модель pred_m. Эта модель действительно выглядит многообещающей! Она умеет создавать группы, в которых чувствительность к цене высока, и группы, в которых чувствительность к цене является низкой. Это именно то, что нам нужно. Наконец, причинно-следственная модель sensitivity_m выглядит немного странно. Она выявляет группы с сильно выраженной отрицательной чувствительностью, что на самом деле означает высокую чувствительность к ценам (продажи сильно уменьшатся, если мы увеличим цены). Обнаружение подобных дней с высокой чувствительностью к ценам очень полезно для нас. Если мы знаем, когда они наступают, то будем проявлять осторожность при повышении цен в такие дни. Кроме того, причинно-следственная модель
Кривая накопленной чувствительности (cumulative sensitivity curve)  337 выделяет дни с меньшей чувствительностью, поэтому она успешно отличает дни с высокой чувствительностью от дней с низкой чувствительностью. Однако порядок значений у причинно-следственной модели хуже, чем у прогнозной модели. Итак, что мы должны решить? Какая из моделей более полезна? Прогнозная модель или причинно-следственная модель? У прогнозной модели лучше порядок, но причинно-следственная модель лучше выделяет крайние случаи (дни с выраженной отрицательной чувствительностью). График чувствительности по диапазонам прогнозов модели – хороший способ проверки для начала, но он не может точно сказать нам, какая модель является лучшей. Нам нужно перейти к более сложным методам. Кривая накопленной чувствительности (cumulative sensitivity curve) Снова рассмотрим пример, в котором цена была преобразована в бинарное воздействие. Мы остановились на моменте, когда разбили чувствительность воздействия на диапазоны. Следующим шагом может быть упорядочивание диапазонов в соответствии с их чувствительностью. То есть мы берем самую чувствительную группу и помещаем ее на первое место, вторую по чувствительности группу ставим на второе место и т. д. Для моделей 1 и 3 переупорядочивание не требуется, так как они уже упорядочены. Для модели 2 нам нужно изменить порядок в обратном направлении. После того как у нас есть упорядоченные группы, мы можем построить кривую накопленной чувствительности (cumulative sensitivity curve). Сначала мы вычисляем чувствительность для первой группы, затем для первой и второй и т. д., пока мы не включим все группы. В итоге мы просто вычисляем чувствительность для всего набора данных. Вот как это выглядело бы для нашего примера:
338  Оценка качества причинно-следственных (каузальных) моделей Обратите внимание, что первый бин на графике накопленной чувствительности – это просто оценка ATE для наиболее чувствительной группы согласно нашей модели. Кроме того, для всех моделей кривая накопленной чувствительности сойдется к одной и той же точке, которая представляет собой оценку ATE для всего набора данных. Математически мы можем определить кривую накопленной чувствительности как чувствительность, оцененную вплоть до объекта k: Для построения кривой накопленной чувствительности мы итеративно вызываем вышеуказанную функцию по набору данных, чтобы получить следующую последовательность: Эта последовательность очень интересна с точки зрения оценки качества модели, потому что мы уже можем делать определенные выводы о модели. Во-первых, модель лучше в той степени, в какой ŷ′(t)k > ŷ′(t)k+a для любого k и a > 0. Проще говоря, если модель хорошо упорядочивает объекты по чувствительности, чувствительность, фиксируемая для топ k объектов, должна быть выше, чем чувствительность, фиксируемая для топ k + a объектов. Или, другими словами, если я смотрю на объекты в самой верхней части списка, у них должна быть более высокая чувствительность, чем у объектов, расположенных ниже. Во-вторых, модель лучше в той степени, в какой разница ŷ′(t)k – ŷ′(t)k+a является наибольшей для любого k и a > 0. Таким образом, мы не только хотим, чтобы чувствительность для топ k объектов была выше, чем чувствительность для объектов, расположенных ниже, но мы также хотим, чтобы эта разница была максимальной. Для большей ясности реализуем обе идеи с помощью программного кода. def cumulative_sensitivity_curve(dataset, prediction, y, t, min_periods=30, steps=100):
Кривая накопленной чувствительности (cumulative sensitivity curve)  339 # вычисляем размер набора данных size = dataset.shape[0] # упорядочим данные по столбцу prediction и сбросим индекс ordered_df = dataset.sort_values( prediction, ascending=False ).reset_index(drop=True) # cоздаем список увеличивающихся количеств строк, которые будут # определять наши значения K, последний элемент # последовательности – все строки (размер набора данных) n_rows = list(range(min_periods, size, size // steps)) + [size] # Кумулятивно вычисляем чувствительность. Сначала для верхних # min_periods объектов. Затем для верхних (min_periods + step*1), # потом (min_periods + step*2) и т. д. return np.array([sensitivity(ordered_df.head(rows), y, t) for rows in n_rows]) Отметим некоторые моменты касательно этой функции. Она предполагает, что прогнозы, на основе которых мы упорядочиваем чувствительность, хранятся в столбце, который мы передаем в качестве аргумента prediction. Кроме того, первая группа включает min_periods объектов, поэтому она может отличаться от остальных. Причина заключается в том, что из-за малого размера выборки чувствительность может быть слишком шумной в начале кривой. Чтобы исправить это, мы можем передать первую группу уже достаточно большого размера. Наконец, аргумент steps определяет, сколько дополнительных объектов мы включаем в каждую последующую группу. Используя эту функцию, мы теперь можем построить кривую накопленной чувствительности. Для этого используем тот порядок значений, который получили ранее с помощью трех моделей. plt.figure(figsize=(10,6)) for m in ["sensitivity_m_pred", "pred_m_pred", "rand_m_pred"]: cumu_sens = cumulative_sensitivity_curve( prices_rnd_pred, m, "sales", "price", min_periods=100, steps=100) x = np.array(range(len(cumu_sens))) plt.plot(x/x.max(), cumu_sens, label=m) plt.hlines(sensitivity(prices_rnd_pred, "sales", "price"), 0, 1, linestyles="--", color="black", label="Средн. чувст-ть") plt.xlabel("Топ N дней по чувствительности (в %)") plt.ylabel("Накопленная чувствительность") plt.title("Кривая накопленной чувствительности") plt.legend();
340  Оценка качества причинно-следственных (каузальных) моделей Интерпретация кривой накопленной чувствительности может быть немного сложной, но вот как я ее себе представляю. Опять же, может быть проще рассмотреть бинарный случай. Ось X кривой представляет собой коли­ чество обрабатываемых объектов. Здесь я нормализовал значения оси так, что каждое значение – это доля от всего набора данных, поэтому .4 означает, что мы обрабатываем 40 % объектов (можно еще сказать, воздействуем на 40 % объектов). Ось Y – это чувствительность, которую мы можем ожидать при таком количестве объектов. Таким образом, если кривая имеет значение –1 при 40 %, это означает, что чувствительность для топ 40 % объектов составляет –1. В идеале мы хотим наивысшую чувствительность для максимально возможного количества объектов. Идеальная кривая должна начинаться с высокого значения по оси Y и медленно снижаться к средней чувствительности, это означает, что мы можем обрабатывать высокий процент объектов, сохраняя при этом чувствительность выше средней. Само собой разумеется, ни одна из кривых наших моделей даже близко не похожа на идеальную кривую чувствительности. Модель rand_m, генерирующая случайные прогнозы, колеблется вокруг средней чувствительности и никогда слишком далеко от нее не уходит. Это означает, что модель не может выявить группы, в которых чувствительность отличается от средней. Что касается прогнозной модели pred_m, похоже, что она упорядочивает чувствительность в обратном порядке, потому что ее кривая начинается ниже среднего уровня чувствительности. Более того, она быстро приближается к средней чувствительности, примерно при 50 % объектов. Наконец, чувствительность причинно-следственной модели sensitivity_m кажется более интересной. Сначала она ведет себя странно, когда накопленная чувстви-
Кривая накопленного выигрыша (cumulative gain curve)  341 тельность увеличивается относительно средней чувствительности, но затем достигает точки, в которой мы можем охватить около 75 % объектов, сохранив при этом чувствительность, почти равную 0 (т. е. отсутствует реакция на изменение цены). Вероятно, это происходит потому, что данная модель может выявлять дни с очень низкой чувствительностью. Таким образом, если мы до сих пор не увеличивали цены в эти дни, то мы вполне можем это сделать для большинства объектов – дней (около 75 %), сохранив при этом низкую чувствительность к цене. С точки зрения оценки качества модели кривая накопленной чувствительности уже намного лучше, чем простая идея оценивать чувствительность по диапазонам прогнозов. Здесь нам удалось сделать более точные выводы о наших моделях. Однако эта кривая сложна для понимания. По этой причине мы можем внести еще одно улучшение. Кривая накопленного выигрыша (cumulative gain curve) Следующая идея – это очень простое, но мощное улучшение подхода на основе кривой накопленной чувствительности. Мы просто умножаем накопленную чувствительность на пропорциональный размер выборки. Например, если накопленная чувствительность равна, скажем, –0.5 при 40 %, мы получим (–0.5 × 0.4) = –0.2 в этой точке. Затем мы сравним это значение со значением теоретической кривой, созданной моделью, генерирующей случайные прогнозы. Эта кривая фактически будет прямой линией от 0 до 100 %, или, как еще говорят, от 0 до среднего эффекта воздействия (ATE). Это объясняется тем, что поскольку модель случайных прогнозов просто генерирует случайные разбиения данных, то каждая точка кривой накопленной чувствительности, построенной на основе такой модели, является оценкой ATE для соответствующего процента выборки. Таким образом, при обработке всей выборки (100 %) мы получаем итоговую оценку среднего эффекта воздействия.
­ 342  Оценка качества причинно-следственных (каузальных) моделей Получив с помощью модели случайных прогнозов теоретическую кривую, мы можем использовать ее в качестве бенчмарка и сравнивать с ней остальные модели. Все кривые начинаются и заканчиваются в одних и тех же точках. Однако чем лучше модель упорядочивает чувствительность, тем сильнее кривая будет отличаться от теоретической кривой в точках между нулем и единицей. Например, на рисунке выше M2 лучше, чем M1, потому что она сильнее всего отличается от теоретической кривой (изображена пунктирной линией), прежде чем достичь ATE в конечной точке. Те, кто знаком с ROCкривой, могут считать кривую накопленного выигрыша ROC-кривой для причинно-следственных моделей. С математической точки зрения: Чтобы реализовать эту формулу в програмном коде, нам просто нужно добавить нормализацию на основе пропорционального размера выборки. def cumulative_gain(dataset, prediction, y, t, min_periods=30, steps=100): # вычисляем размер набора данных size = dataset.shape[0] # упорядочим данные по столбцу prediction и сбросим индекс ordered_df = dataset.sort_values( prediction, ascending=False ).reset_index(drop=True) # cоздаем список увеличивающихся количеств строк, которые будут # определять наши значения K, последний элемент # последовательности – все строки (размер набора данных) n_rows = list(range(min_periods, size, size // steps)) + [size] # добавляем (rows/size) в качестве нормализатора return np.array([sensitivity(ordered_df.head(rows), y, t) * (rows/size) for rows in n_rows]) Для нашего набора данных мы получаем следующие кривые. plt.figure(figsize=(10,6)) for m in ["sensitivity_m_pred", "pred_m_pred", "rand_m_pred"]: cumu_gain = cumulative_gain(prices_rnd_pred, m, "sales", "price", min_periods=50, steps=100) x = np.array(range(len(cumu_gain))) plt.plot(x/x.max(), cumu_gain, label=m) plt.plot([0, 1], [0, sensitivity(prices_rnd_pred, "sales", "price")], linestyle="--", label="Модель случайных прогнозов", color="black") plt.xlabel("Топ N дней по чувствительности (в %)") plt.ylabel("Накопленный выигрыш")
Кривая накопленного выигрыша (cumulative gain curve)  343 plt.title("Накопленный выигрыш") plt.legend(); Теперь отчетливо видно, что причинно-следственная модель (sensitivity_m) намного лучше двух других. Она сильнее отличается от теоретической линии, чем rand_m и pred_m. Также обратите внимание, как фактическая модель случайных прогнозов почти идентична теоретической модели случайных прогнозов. Разница между ними, вероятно, вызвана просто случайным шумом. Итак, мы рассмотрели несколько отличных подходов к оценке качества причинно-следственных моделей. Это уже большое достижение. Мы смогли оценить, насколько хороши модели с точки зрения упорядочивания объектов по чувствительности, даже не имея фактических значений для сравнения. Остался последний пункт – построить доверительный интервал для полученных измерений. В конце концов, мы ведь не варвары, правда? Когда я вижу коэффициенты линейной регрессии без доверительного интервала: Так нецивилизованно
344  Оценка качества причинно-следственных (каузальных) моделей Принимаем дисперсию во внимание Не учитывать дисперсию, когда мы имеем дело с кривыми чувствительности, – плохая практика. Все эти кривые используют теоретический аппарат линейной регрессии, поэтому добавление доверительного интервала для них должно быть довольно простой процедурой. Для решения этой задачи мы сначала напишем функцию, которая возвращает доверительный интервал для параметра линейной регрессии. Здесь я использую формулу для простой линейной регрессии, но вы можете вычислить доверительный интервал иным способом: def sensitivity_ci(df, y, t, z=1.96): n = df.shape[0] t_bar = df[t].mean() beta1 = elast(df, y, t) beta0 = df[y].mean() - beta1 * t_bar e = df[y] - (beta0 + beta1 * df[t]) se = np.sqrt(((1/(n-2))*np.sum(e**2))/np.sum((df[t]-t_bar)**2)) return np.array([beta1 - z*se, beta1 + z*se]) Заменив вызов функции sensitivity() на вызов функции sensitivity_ci() в нашей функции cumulative_sensitivity_ci_curve(), мы можем вывести доверительный интервал для чувствительности. def cumulative_sensitivity_ci_curve(dataset, prediction, y, t, min_periods=30, steps=100): size = dataset.shape[0] ordered_df = dataset.sort_values( prediction, ascending=False ).reset_index(drop=True) n_rows = list(range(min_periods, size, size // steps)) + [size] # просто заменяем вызов функции sensitivity() # на вызов функции sensitivity_ci() return np.array([sensitivity_ci(ordered_df.head(rows), y, t) for rows in n_rows]) Наконец, строим для причинно-следственной модели кривую накопленной чувствительности вместе с 95%-ным доверительным интервалом. plt.figure(figsize=(10,6)) cumu_sens_ci = cumulative_sensitivity_ci_curve( prices_rnd_pred, "sensitivity_m_pred", "sales", "price", min_periods=50, steps=200 )
Принимаем дисперсию во внимание  345 x = np.array(range(len(cumu_sens_ci))) plt.plot(x/x.max(), cumu_sens_ci, color="C0") plt.hlines(sensitivity(prices_rnd_pred, "sales", "price"), 0, 1, linestyles="--", color="black", label="Средн. чувст-ть") plt.xlabel("Топ N дней по чувствительности (в %)") plt.ylabel("Накопленная чувствительность") plt.title("Накопленная чувствительность для модели " "sensitivity_m_pred с 95%-ным ДИ") plt.legend(); Обратите внимание, как доверительный интервал становится все уже и уже по мере накопления объема данных. Это происходит потому, что увеличивается размер выборки. Что касается кривой накопленного выигрыша, для нее тоже легко получить доверительный интервал. Мы в функции cumulative_gain_ci_curve() просто заменяем вызов функции sensitivity() на вызов функции sensitivity_ci(). def cumulative_gain_ci_curve(dataset, prediction, y, t, min_periods=30, steps=100): size = dataset.shape[0] ordered_df = dataset.sort_values( prediction, ascending=False ).reset_index(drop=True) n_rows = list(range(min_periods, size, size // steps)) + [size] # просто заменяем вызов функции sensitivity()
346  Оценка качества причинно-следственных (каузальных) моделей # на вызов функции sensitivity_ci() return np.array([sensitivity_ci(ordered_df.head(rows), y, t) * (rows/size) for rows in n_rows]) Обратите внимание, что теперь доверительный интервал выглядит более узким даже при небольшом объеме выборки в начале кривой. Причина заключается в том, что нормализующий множитель уменьшает параметр ATE и, следовательно, его доверительный интервал. Поскольку данная кривая должна использоваться для сравнения моделей, этот момент не должен стать проблемой, так как нормализующий множитель будет применяться одинаково для всех оцениваемых моделей. plt.figure(figsize=(10,6)) cumu_gain = cumulative_gain_ci_curve( prices_rnd_pred, "sensitivity_m_pred", "sales", "price", min_periods=50, steps=200) x = np.array(range(len(cumu_gain))) plt.plot(x/x.max(), cumu_gain, color="C0") plt.plot([0, 1], [0, sensitivity(prices_rnd_pred, "sales", "price")], linestyle="--", label="Модель случайных прогнозов", color="black") plt.xlabel("Топ N дней по чувствительности (в %)") plt.ylabel("Накопленный выигрыш") plt.title("Накопленный выигрыш для модели " "sensitivity_m_pred с 95%-ным ДИ") plt.legend();
­ Дополнительное чтение  347 Ключевые идеи В этой главе мы рассмотрели три способа оценки качества модели, которая, в свою очередь, должна была оценить чувствительность к воздействию. Мы использовали эти методы для сравнения и выбора наилучшей модели. Это большое дело. Мы смогли проверить, насколько наша причинно-следственная модель хороша в выявлении групп с различной чувствительностью, даже не видя самой чувствительности! Здесь мы сильно полагались на случайные данные. Мы обучали модель на нерандомизированных данных, но всю оценку качества проводили на выборке, в которой воздействие было случайным. Это было обусловлено тем, что нам был нужен надежный способ оценить чувствительность. Без случайных данных простые формулы, которые мы использовали здесь, не сработают. Как мы отлично знаем, в случае наличия спутывающих переменных простая линейная регрессия подвержена смещению, обусловленному опущенной переменной. Тем не менее если у нас есть случайные данные, мы уже знаем, как сравнивать модели, построенные на случайных данных. В следующей главе мы рассмотрим проблему нерандомизированных данных, но прежде чем мы продолжим, я хочу сказать еще несколько слов об оценке качества модели. Давайте еще раз подчеркнем, насколько важна надежная оценка модели. Кривая накопленного выигрыша наконец-то дает хороший способ сравнивать причинно-следственные модели. Мы теперь можем определить, какая модель лучше подходит для персонализации воздействия. Это здорово. Большая часть материалов по анализу причинно-следственных связей, которые вы найдете, не дает нам хороших способов оценки качества моделей. По мое му мнению, это недостающее звено, которое нам нужно, чтобы сделать анализ причинно-следственных связей таким же популярным, как и машинное обучение. Получив надежный способ проверки модели, мы можем привнести в анализ причинно-следственных связей что-то, аналогичное парадигме разбиения данных на обучающий и тестовый наборы, которая уже стала такой полезной для прогнозных моделей. Это смелое заявление. Я осторожен в своем заявлении, но до сих пор я не нашел никакой обоснованной критики. Если у вас есть какие-либо замечания, пожалуйста, дайте мне знать. Дополнительное чтение Материал, который я написал в этой главе, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение
348  Оценка качества причинно-следственных (каузальных) моделей того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю данный материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. Я почерпнул идеи для этой главы из статьи Пьера Гутьерреса (Pierre Gutierrez) и Жан-Ива Жерарди (Jean-Yves Gerardy) «Causal Inference and Uplift Modeling: A review of the literature»: https://proceedings.mlr.press/v67/gutierrez17a/ gutierrez17a.pdf. Авторы объясняют концепцию кривой Джини. Если вы посмотрите ее, то увидите, что речь идет о методе, используемом для upliftмоделирования, который можно считать анализом причинно-следственных связей для случаев, когда воздействие является бинарным. Из этой статьи я взял идею кривой Джини и обобщил ее на случай непрерывного воздействия. Я думаю, что методы, представленные здесь, работают как для непрерывных, так и для бинарных случаев, но опять же повторюсь, я никогда не видел примеры их использования где-либо еще, так что помните об этом. Я также настоятельно рекомендую прочитать статью Лео Бреймана (Leo Breiman) «Statistical Modeling: The Two Cultures» (2001), посвященную парадигме разбиения на обучающий и тестовый наборы. Это отличный источник, позволяющий понять, что именно делает статистический метод успешным.
Глава 20 Модели «Подключи и пользуйся» До сих пор мы рассматривали способы устранения смещения в данных, когда назначение воздействия не являлось случайным и это приводило к смещению из-за спутывающего фактора. Это помогает нам решить проблему идентификации в анализе причинно-следственных связей. Другими словами, если объекты взаимозаменяемы, или Y(0), Y(1) ⊥ X, мы можем исследовать эффект воздействия. Но мы еще далеки от итогового решения. Идентификация означает, что мы можем найти средний эффект воздействия. Другими словами, мы можем выяснить, насколько эффективно воздействие в среднем. Конечно, это полезно, поскольку помогает нам решить, следует осуществлять воздействие или нет. Но мы хотим большего. Мы хотим знать, есть ли подгруппы объектов, которые реагируют на воздействие лучше или хуже. Это позволит разработать гораздо более эффективную стратегию, в которой мы подвергаем воздействию только тех, кто получит от этого выгоду. Формулировка проблемы Начнем с постановки проблемы. Получив потенциальные результаты, мы можем определить индивидуальный эффект воздействия как разницу между потенциальными результатами: τi = Yi (1) – Yi (0), или, в случае непрерывного воздействия, τi = ∂Y(t), где t – это переменная воздействия. Конечно же, мы никогда не сможем наблюдать индивидуальный эффект воздействия, потому что мы видим только один из потенциальных результатов:
350  Модели «Подключи и пользуйся» Мы можем определить средний эффект воздействия (average treatment effect – ATE) как τ = E[Yi (1) – Yi (0)] = E[τi ] и условный средний эффект воздействия (conditional average treatment effect – CATE) как τ(x) = E[Yi (1) – Yi (0) | X ] = E[τi | X ]. В части I этой книги мы в основном сосредоточились на ATE. Теперь нас интересует CATE. CATE полезен для персонализации процесса принятия решений. Например, если у вас есть лекарство, выступающее в качестве воздействия t, вы хотите знать, на какие группы пациентов это лекарство подействует лучше (даст более высокий CATE) и есть ли группы пациентов с отрицательным ответом (CATE < 0). Мы уже выяснили, как оценить CATE с помощью линейной регрессии, включающей взаимодействия между воздействием и признаками: y i = β 0 + β 1t i + β 2 X i + β 3t i X i + e i. Выполнив оценивание модели, мы получаем оценки для τ(x): τ̂(x) = βˆ1 + βˆ3 Xi. Однако линейные модели имеют недостатки. Главным из них является предположение о линейности, выдвигаемое по отношению к X. Заметьте, что вам даже не важно присутствие β2 в этой модели. Но если признаки X не имеют линейной связи с результатом, ваши оценки параметров β1 и β3 будут неточными. Было бы замечательно, если бы мы могли заменить линейную модель более гибкой моделью машинного обучения. Мы могли бы даже включить воздействие в качестве признака в модель машинного обучения типа градиентного бустинга или нейронной сети: yi = M(Xi, T i ) + ei. Однако не совсем понятно, как нам получить оценки эффекта воздействия, поскольку эта модель будет выдавать прогнозы ŷ вместо прогнозов τ̂(x). В идеале нам нужна модель машинного обучения, которая решает задачу регрессии и вместо минимизации MSE результирующей переменной E [(Yi – Ŷi )2] будет минизировать MSE эффекта воздействия E[(τ(x)i – τ̂(x)i )2] = E[Yi (1) – Yi (0) – τ̂(x)i )2].
Преобразование зависимой переменной  351 Однако это невозможно реализовать на практике. Вновь проблема здесь заключается в том, что τ(x)i – ненаблюдаемая величина, поэтому мы не можем оптимизировать ее напрямую. Это ставит нас в трудное положение... Давайте попробуем немного упростить задачу, и, возможно, мы что-нибудь придумаем. Ух ты! Это бесполезно Преобразование зависимой переменной Предположим, что ваше воздействие является бинарным. Допустим, вы – инвестиционная фирма, тестирующая эффективность отправки электронного письма с советами по инвестированию. Вы надеетесь, что электронное письмо подтолкнет людей к большему объему инвестиций. Кроме того, предположим, что вы провели случайный эксперимент, в котором 50 % клиентов получили электронное письмо, а другие 50 % – нет. Вот вам безумная идея: давайте выполним преобразование переменной результата (зависимой переменной), умножив ее на воздействие: Yi* = 2Yi ∗ T i – 2Yi ∗ (1 – T i ). Таким образом, если объект был подвергнут воздействию, вы умножите результат на 2. Если воздействие не было применено, вы умножите результат на –2. Например, если один из ваших клиентов инвестировал 2000 BRL и получил электронное письмо, значение преобразованной зависимой переменной составит 4000. Однако если клиент не получил электронное письмо, значение преобразованной зависимой переменной будет равно –4000.
352  Модели «Подключи и пользуйся» Это кажется довольно странным, потому что мы утверждаем, что эффект от электронного письма может быть отрицательным числом, но давайте разберемся. Если мы немного вникнем в математику, то увидим, что в среднем или в соответствии с математическим ожиданием эта преобразованная зависимая переменная будет эффектом воздействия. Это нечто удивительное. Применив это причудливое преобразование, я могу оценить то, что я даже не могу наблюдать. Чтобы понять это, нам потребуется немного математики. В силу случайного назначения воздействия у нас T ⊥ Y(0), Y(1), т. е. вменение воздействия не зависит от потенциальных результатов (наше старое доброе предположение об игнорировании/взаимозаменяемости/отсутствии спутывающих факторов). Это подразумевает, что E[T ∗ Y(t)] = E[T ] ∗ E[Y(t)], а это по сути является определением независимости. Также мы знаем, что Yi ∗ T i = Y(1)i ∗ T i и Yi ∗ (1 – T i ) = Y(0)i ∗ T i, потому что воздействие – это то, что материализует тот или иной потенциальный результат. Помня об этом, давайте разложим математическое ожидание преобразованной зависимой переменной Yi* при условии Xi = x и посмотрим, что у нас получится в итоге. применяем определение преобразованной зависимой переменной Yi* = 2Yi ∗ Ti − 2Yi ∗ (1 – Ti) E[Yi* | Xi = x] = E[2Y(1)i ∗ T i – 2Y(0)i ∗ (1 – T i ) | Xi = x] используем линейность математического ожидания, чтобы вынести константы перед математическими ожиданиями = 2E[Y(1)i ∗ T i | Xi = x] – 2E[Y(0)i ∗ (1 – T i) | Xi = x] выполняем разложение математического ожидания произведения в произведение математических ожиданий = 2E[Y(1)i | Xi = x] ∗ E[T i | Xi = x] – 2E[Y(0)i | Xi = x] ∗ E[(1 – T i) | Xi = x] предполагаем случайное назначение воздействия T ⊥ Y(0), Y(1), отсюда умножение на 0.5 = 2E[Y(1)i | Xi = x] ∗ 0.5 – 2E[Y(0)i | Xi = x] ∗ 0.5 упрощаем = E[Y(1)i | Xi = x] – E[Y(0)i | Xi = x] = τ(x)i.
Преобразование зависимой переменной  353 Таким образом, идея, поначалу кажущаяся безумной, в конечном итоге оказалась несмещенной оценкой индивидуального эффекта воздействия τ(x)i. Теперь мы можем заменить наш нереализуемый критерий оптимизации на E [Yi* – τ̂(x)i )2]. Проще говоря, все, что нам нужно сделать, – это использовать любую модель машинного обучения, решающую задачу регрессии, чтобы спрогнозировать Yi*, и эта модель спрогнозирует эффект воздействия. Теперь, когда мы нашли решение для простого случая, как насчет более сложного случая, когда воздействие распределено неравномерно (не 50 % на 50 %) или даже не является случайно назначенным? Ответить на этот вопрос немного сложнее, но вполне возможно. Во-первых, если у нас нет случайного назначения воздействия, нам нужна хотя бы условная независимость T ⊥ Y(1), Y(0) | X. Если мы контролируем X, то воздействие T будет так же хорошо работать, как и случайно назначенное. Помня об этом, мы можем обобщить преобразованную целевую переменную до вида: где e(Xi ) – это оценка склонности (к воздействию). Таким образом, если назначение воздействия не является равномерным (не 50 % на 50 %), а выполняется с разной вероятностью p, все, что вам нужно сделать, – это заменить оценку склонности в вышеуказанной формуле на p. Если воздействие не является случайным, то вам придется использовать оценку склонности, заранее известную или оцененную. Если вы возьмете математическое ожидание этого выражения, то увидите, что оно тоже совпадает с эффектом воздействия. Доказательство приведено ниже. Оно немного громоздко, поэтому можете пропустить его.
­ 354  Модели «Подключи и пользуйся» Как всегда, я считаю, формула станет намного более понятной, если привести пример. Вновь рассмотрим электронное письмо с советами по инвес тированию, которое мы отправляли, пытаясь побудить людей к бóльшему объему инвестиций. Результатом является бинарная переменная converted (инвестировал или не инвестировал). import pandas as pd import numpy as np from matplotlib import pyplot as plt import seaborn as sns from nb21 import cumulative_gain, elast email = pd.read_csv("data/invest_email_rnd.csv") email.head() Наша цель здесь заключается в персонализации. Давайте сосредоточимся на переменной em1. Сделаем ее переменной воздействия. Мы хотим отправить электронное письмо только тем клиентам, которые на него лучше среагируют. Другими словами, мы хотим оценить условный средний эффект отклика на em1: E [Converted(1)i – Converted(0)i | Xi = x] = τ(x)i. Таким образом, мы сможем нацелиться на тех клиентов, которые лучше отреагируют на письмо (нас интересует более высокий CATE). Но сначала давайте разделим наши данные на обучающий и проверочный наборы. Мы будем оценивать τ(x)i на одном наборе, а оценивать качество построенной модели на другом. from sklearn.model_selection import train_test_split np.random.seed(123) train, test = train_test_split(email, test_size=0.4) print(train.shape, test.shape) (9000, 8) (6000, 8)
Преобразование зависимой переменной  355 Теперь мы применим преобразование зависимой переменной, которое мы только что вывели. Поскольку электронные письма были распределены случайным образом (хотя и не в соотношении 50 % на 50 %), нам не нужно подумать об оценке склонности. Скорее всего, она является постоянной и равна вероятности воздействия. y = "converted" T = "em1" X = ["age", "income", "insurance", "invested"] ps = train[T].mean() y_star_train = train[y] * (train[T] - ps)/(ps*(1-ps)) Получив преобразованную зависимую переменную, мы можем выбрать любой метод машинного обучения, решающий задачу регрессии, чтобы спрогнозировать ее. Давайте здесь попробуем градиентный бустинг. from lightgbm import LGBMRegressor np.random.seed(123) cate_learner = LGBMRegressor(max_depth=3, min_child_samples=300, num_leaves=5) cate_learner.fit(train[X], y_star_train); Эта модель теперь может оценивать τ(x)i. Другими словами, результат данной модели – это τ̂(x)i. Например, если мы посмотрим прогнозы для тестового набора данных, мы увидим, что у некоторых объектов CATE выше, чем у других. Например, у клиента 6958 CATE равен 0.1, это означает, что вероятность того, что клиент купит наш инвестиционный продукт, увеличится на 0.1, если мы отправим ему это электронное письмо. Напротив, для клиента 3903 прогнозируется увеличение вероятности покупки продукта всего на 0.04. test_pred = test.assign(cate=cate_learner.predict(test[X])) test_pred.head()
356  Модели «Подключи и пользуйся» Чтобы оценить качество построенной модели, мы можем вывести кривые накопленного выигрыша как для обучающего, так и для тестового наборов. gain_curve_test = cumulative_gain( test_pred, "cate", y="converted", t="em1" ) gain_curve_train = cumulative_gain( train.assign(cate=cate_learner.predict(train[X])), "cate", y="converted", t="em1" ) plt.plot(gain_curve_test, color="C0", label="Тестовый набор") plt.plot(gain_curve_train, color="C1", label="Обучающий набор") plt.plot([0, 100], [0, elast(test, "converted", "em1")], linestyle="--", color="black", label="Базовая модель") plt.legend(); Видим, на тестовом наборе наша модель работает лучше случайной. Однако похоже, что она сильно переобучена, поскольку качество на обучающем наборе намного лучше, чем на тестовом наборе. На самом деле это один из самых больших недостатков метода преобразования зависимой переменной. С помощью преобразования зависимой переменной вы получаете простоту, поскольку вы можете просто преобразовать зависимую переменную и использовать любую модель машинного обучения для прогнозирования гетерогенных эффектов воздействия. За эту простоту вы расплачиваетесь большой дисперсией. Это обусловлено тем, что преобразованная зависимая переменная представляет собой очень зашумленную оценку индивидуального эффекта воздействия, и эта дисперсия переносится на процесс оценивания. Это огромная проблема, если у вас небольшой объем данных, однако она становится менее актуальной в случае больших данных, когда вы имеете дело с выборками объемом более миллиона наблюдений.
­ Случай непрерывного воздействия  357 Случай непрерывного воздействия У НАС БЫЛА ОДНА МОДЕЛЬ, ДА КАК НАСЧЕТ ВТОРОЙ МОДЕЛИ? Еще одним очевидным недостатком метода преобразования зависимой переменной является тот факт, что он работает только для дискретных или бинарных воздействий. Это распространенное явление в литературе по анализу причинно-следственных связей. В большинстве исследований рассмат риваются случаи бинарного воздействия, а о непрерывных воздействиях говорится немного. Это беспокоило меня, поскольку в промышленности непрерывные воздействия встречаются повсеместно, в основном в виде цен, которые необходимо оптимизировать. И хотя я не смог найти ничего относительно преобразования целевой переменной для непрерывных воздействий, я придумал нечто, что работает на практике. Однако имейте в виду, что у меня нет супернадежных эконометрических исследований по поводу этого метода. Давайте вернемся к примеру с продажами мороженого. Задача состоит в оценке эластичности спроса относительно цены, чтобы мы могли более рационально установить цены на мороженое для оптимизации наших доходов. Напомним, что в этом наборе данных наблюдение представляет собой день, и нам интересно определить дни, в которые люди демонстрируют меньшую чувствительность к увеличению цен. Также вспомним, что цены в этом наборе данных назначаются случайным образом, это означает, что нам не нужно беспокоиться о смещении, возникающем в силу спутывающих факторов. prices_rnd = pd.read_csv("data/ice_cream_sales_rnd.csv") prices_rnd.head()
358  Модели «Подключи и пользуйся» Как и прежде, давайте начнем с того, что разобьем данные на обучающий и тестовый наборы. np.random.seed(123) train, test = train_test_split(prices_rnd, test_size=0.3) train.shape, test.shape ((3500, 5), (1500, 5)) Теперь наступает время для творчества. Для дискретного случая условный средний эффект воздействия определяется тем, насколько изменится результат, когда мы переходим от объектов, не подвергнутых воздействию, к объектам, подвергнутым воздействию, в зависимости от характеристик X, присущих объектам: τ(x)i = E[Yi (1) – Yi (0) | X] = E [τi | X ]. Простыми словами, речь идет об оценке влияния воздействия на различные профили объектов в ситуации, когда профили определяются с помощью характеристик X. В случае непрерывного воздействия у нас нет этого переключателя «включено/выключено». У нас нет ситуации, когда объекты либо не подверглись воздействию, либо подверглись ему. Они все подвергаются воздействию, но с разной интенсивностью. Поэтому мы не можем говорить об эффекте осуществления воздействия. Скорее, нам нужно говорить о том, как увеличивается воздействие. Другими словами, мы хотим знать, как изменится результат, если мы увеличим воздействие на определенную величину. Это похоже на оценку частной производной функции отклика или результата Y относительно воздействия t. И поскольку мы хотим ее вычислить для каждой группы (нас интересует CATE, а не ATE), то вводим обусловленность по признакам X: τ(x)i = E[∂Yi (t ) | X] = E [τi | X ]. Как мы можем оценить это выражение? Сначала рассмотрим простой случай, когда результат линеен по отношению к воздействию. Предположим, у вас есть два типа дней: жаркие дни (отмечены желтым цветом на рисунке ниже) и холодные дни (отмечены синим цветом на рисунке ниже). В холодные дни люди более чувствительны к увеличению цен. Также с увеличением цен спрос линейно снижается.
Случай непрерывного воздействия  359 В данном случае CATE будет представлять собой наклон каждой линии спроса. Эти наклоны будут указывать нам, насколько упадет спрос, если мы повысим цену на определенную величину. Если данная зависимость действительно является линейной, мы можем оценить эти эластичности с помощью коэффициента простой линейной регрессии, построенной отдельно для жарких и холодных дней. Мы можем взять на вооружение эту формулу и подумать о том, как она будет выглядеть для отдельного объекта. Другими словами, как бы выглядела наша оценка для отдельного дня. На мой взгляд, она может выглядеть примерно так: Проще говоря, мы преобразовываем исходную зависимую переменную, вычитая из нее среднее значение, затем умножаем на переменную воздействия, из которой тоже вычли среднее значение. В конечном итоге мы делим результат на дисперсию переменной воздействия. Таким образом, у нас есть преобразованная зависимая переменная для случая непрерывного воздействия. Иногда моя гениальность просто пугает
360  Модели «Подключи и пользуйся» Теперь вопрос состоит в том, работает ли этот метод. По факту да, и мы можем привести аналогичное доказательство, почему он работает, как мы делали это в случае бинарного воздействия. Во-первых, давайте обозначим стандартизированное отклонение значения переменной воздействия T i от среднего значения T i как Vi: Обратите внимание, что E [Vi | Xi = x] = 0, потому что при случайном назна– чении воздействия E [T i | Xi = x] = T. Другими словами, для каждого значения – – Xi E [T i ] = T. Кроме того, E [T iVi | Xi = x] = 1, потому что E [T i (T i – T ) | X i = x] = – 2 E [(T i – T ) | X i = x], что представляет собой дисперсию воздействия. Таким образом, при случайном назначении воздействия стандартизированное отклонение переменной воздействия положительно скоррелировано с самой переменной воздействия, и их ожидаемое произведение равно 1. Проще говоря, Vi меняется вместе с переменной воздействия T i. Наконец, при условной независимости (которую мы получаем бесплатно при случайном назначении воздействия) ожидаемое произведение переменной воздействия T i и ошибки ei при условии Xi = x равно произведению условного математического ожидания T i и условного математического ожидания ошибки ei: E[T i ei | Xi = x] = E[T i | Xi = x]E[ei | Xi = x]. Таким образом, при условной независимости мы ожидаем, что воздействие T i и ошибка ei не взаимодействуют систематически. Это упрощает структуру модели и позволяет более просто интерпретировать результаты оценки эффектов воздействия. Теперь покажем, что данное преобразование зависимой переменной работает. Здесь мы должны помнить, что оцениваем параметр для локальной линейной модели: Yi = α + βT i + ei | Xi = x. В нашем примере это будут линейные модели для жарких и холодных дней. Здесь нас интересует параметр β, который представляет собой нашу условную эластичность, или CATE. С учетом всего этого мы можем доказать, что – E [Yi* | Xi = x] = E[(Yi – Y )Vi | Xi = x] подставляем линейную модель – = E[(α + βT i + ei – Y )Vi | Xi = x] раскладываем на три компонента = αE[Vi | Xi = x] + βE[T iVi | Xi = x] + E[eiVi | Xi = x]
Случай непрерывного воздействия  361 упрощаем с учетом свойств Vi: E[Vi | Xi = x] = 0 и E[TiVi | Xi = x] = 1 = β + E[eiVi | Xi = x] = β = τ(x). Имейте в виду, что это работает только в том случае, если воздействие является рандомизированным. Для нерандомизированного воздействия – мы должны заменить T наM(Xi ), где M – это модель, которая оценивает E[T i | Xi = x]: Это гарантирует, что член αE[Vi | Xi = x] в третьей строке обращается в ноль, а член E[T iVi | Xi = x] становится 1. Обратите внимание, что на самом деле вам не нужно, чтобы E[T iVi | Xi = x] становился 1, если вы просто хотите упорядочить объекты по эффекту воздействия. Другими словами, если вам просто нужно выяснить, в какие дни спрос более чувствителен к увеличению цен, но при этом вас не интересует, насколько именно он становится более чувствителен, вы можете игнорировать масштабирование оценки параметра β. В этом случае вы можете опустить знаменатель: – Yi* = (Yi – Y )(T i – M(T i )). Если весь этот математический аппарат кажется утомительным, не волнуйтесь. Программный код на самом деле очень прост. Давайте еще раз выполним преобразование нашей зависимой переменной с помощью вышеприведенных формул. Здесь у нас назначение воздействия является случайным, поэтому нам не нужно строить модель, прогнозирующую цены. Кроме того, я опускаю знаменатель, потому что здесь меня интересует только упорядочение по эффекту воздействия. y_star_cont = ( (train["price"] - train["price"].mean()) * (train["sales"] - train["sales"].mean()) ) Вновь обучаем модель машинного обучения, решающую задачу регрессии. cate_learner = LGBMRegressor(max_depth=3, min_child_samples=300, num_leaves=5) np.random.seed(123) cate_learner.fit(train[["temp", "weekday", "cost"]], y_star_cont) cate_test_transf_y = cate_learner.predict( test[["temp", "weekday", "cost"]] )
362  Модели «Подключи и пользуйся» test_pred = test.assign(cate=cate_test_transf_y) test_pred.sample(5) На этот раз интерпретация CATE не является интуитивно понятной. Поскольку мы удалили знаменатель из формулы преобразованной зависимой переменной, CATE, который мы видим, масштабируется по Var(X). Однако эти прогнозы все равно должны довольно точно упорядочивать объекты по эффекту воздействия. Чтобы убедиться в этом, мы можем воспользоваться кривой накопленного выигрыша, как мы это делали ранее. gain_curve_test = cumulative_gain( test.assign(cate=cate_test_transf_y), "cate", y="sales", t="price" ) gain_curve_train = cumulative_gain( train.assign(cate=cate_learner.predict( train[["temp", "weekday", "cost"]])), "cate", y="sales", t="price") plt.plot(gain_curve_test, label="Тестовый набор") plt.plot(gain_curve_train, label="Обучающий набор") plt.plot([0, 100], [0, elast(test, "sales", "price")], linestyle="--", color="black", label="Базовая модель") plt.legend();
­ ­ Нелинейные эффекты воздействия  363 Похоже, что применительно к нашим данным модель с преобразованной зависимой переменной работает значительно лучше, чем случайная модель. Более того, результаты на обучающем и тестовом наборах достаточно близки, поэтому разброс здесь не является проблемой. Однако это верно только для данного набора. Если вы помните, в случае бинарного воздействия картина была совсем иная. Там модель показала не столь замечательные результаты. Нелинейные эффекты воздействия Мы поговорили о непрерывном случае, но нам по-прежнему нужно разобраться со слоном в комнате. Мы предполагали линейность эффекта воздействия. Однако это предположение очень редко бывает разумным. Обычно эффекты воздействия насыщаются в той или иной форме. В нашем примере разумно предположить, что спрос уменьшится быстрее при первом увеличении цены, но затем уменьшение будет происходить медленнее. Проблема здесь заключается в том, что эластичность, или эффект воздействия, изменяется вместе с самим воздействием. В нашем примере эффект воздействия более выражен в начале кривой и уменьшается при увеличении цен. Предположим, у нас есть два типа дней: жаркие дни (отмечены желтым цветом) и холодные дни (отмечены синим цветом), и мы хотим отличать их друг от друга с помощью причинно-следственной модели. Проб лема заключается в том, что причинно-следственные модели должны прог нозировать эластичность, но в нелинейном случае эластичность для жарких и холодных дней может быть одинаковой, если мы рассмотрим разные точки цен на кривой (рисунок справа). У этой проблемы нет легкого решения, и я признаюсь, что все еще ищу оптимальный метод. Сам я предпочитаю изучить функциональную форму эффекта воздействия и как-то линеаризовать ее. Например, спрос обычно имеет следующую функциональную форму, в которой более высокие значения α означают, что спрос уменьшается быстрее с каждым увеличением цены:
­ ­ 364  Модели «Подключи и пользуйся» Таким образом, если я применю логарифмическое преобразование как к спросу Y , так и к ценам T, я должен получить результат, который будет линейным: Линеаризация не так проста, как может показаться, поскольку включает в себя ряд размышлений. Однако вы тоже можете поэкспериментировать и проверить, что работает лучше всего. Часто помогают логарифмы и квад ратные корни. Ключевые идеи Теперь мы научились оценивать условный средний эффект воздействия с помощью моделей машинного обучения. Самая большая сложность заключается в том, чтобы превратить прогнозную модель в модель, которая будет оценивать причинно-следственные эффекты. Прогнозные модели фокусируются на оценивании результата Y как функции от набора признаков X и, возможно, воздействия T Y = M(X, T ), тогда как причинноследственные модели должны оценивать частные производные функции отклика относительно воздействия ∂Y = ∂M(X, T ). Это далеко не тривиально, потому что хотя мы и наблюдаем результат Y, мы не можем наблюдать ∂Y, по крайней мере на уровне объектов он не поддается наблюдению. В результате нам нужно быть креативными при разработке целевой функции для наших моделей. В этой главе мы рассмотрели очень простую технику преобразования зависимой переменной. Идея заключается в том, чтобы объединить исходную зависимую переменную Y с воздействием T, дабы сформировать преобразованную зависимую переменную, которая, как ожидается, равна CATE. С помощью этой новой зависимой переменной мы можем подключить любую прогнозную модель машинного обучения для ее оценки, и тогда прогнозы модели будут представлять собой оценки CATE. Кстати, этот метод преобразования зависимой модели известен под названием F-модель (F-Learner). У этой простоты есть и своя цена. Преобразованная зависимая переменная является очень шумной оценкой индивидуального эффекта воздействия, и этот шум отразится на дисперсии оценок модели. Это делает преобразование зависимой переменной более подходящим для задач, где используются большие данные, когда дисперсия менее проблематична в силу больших объемов выборки. Еще один недостаток метода преобразования зависимой переменной заключается в том, что он определен только для бинарного или категориального воздействия. Мы сделали все возможное, чтобы предложить
­ Дополнительное чтение  365 решение для непрерывного воздействия, и даже пришли к методу, который, казалось бы, работает, но до сих пор у него нет прочной теоретической основы, подтверждающей его надежность. Наконец, мы закончили главу обсуждением нелинейных эффектов воздействия и связанных с этим проблем. А именно когда эффект воздействия меняется вместе с самим воздействием, мы можем ошибочно предположить, что у объектов – одна и та же кривая реакции на воздействие, потому что у них одинаковая чувствительность к воздействию, но на самом деле они получают разные дозы воздействия1. Дополнительное чтение Материал, который я написал в этой главе, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю этот материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. Большая часть этой главы заимствована из статьи Сьюзан Эти (Susan Athey) и Гвидо У. Имбенса (Guido W. Imbens) «Machine Learning Methods for Estimating Heterogeneous Causal Effects»: https://gsb-faculty.stanford.edu/guido-w-imbens/ files/2022/04/3350.pdf. Некоторую информацию о преобразовании зависимой 1 Предположим, у нас есть два пациента, Алекс и Виктория, оба страдают от одного и того же инфекционного заболевания. Рассмотрим антибиотик, который применяется для лечения этого заболевания. Оба пациента обладают одинаковой чувствительностью к антибиотику, что означает, что они реагируют на лекарство примерно одинаково. Однако если у нас нет информации о дозировке, мы можем ошибочно предположить, что их ответ на воздействие полностью аналогичен. Теперь представим, что врачи решают прописать разные дозы антибиотика для Алекса и Виктории. Алекс получает более высокую дозу, чем Виктория. Это может быть вызвано различиями в их физических характеристиках, состоянии здоровья или другими факторами. Хотя оба пациента имеют одинаковую чувствительность к антибиотику, различная дозировка может привести к разным результатам. Например, Алекс может продемонстрировать более быстрое восстановление или снижение симптомов благодаря более высокой дозе, в то время как Виктория может испытывать медленное улучшение из-за менее высокой дозы. Таким образом, даже при одинаковой чувствительности к лекарству у пациентов различная дозировка может привести к различным эффектам лечения, подчеркивая важность учета дозы при оценке индивидуальных реакций на лекарства. – Прим. перев.
366  Модели «Подключи и пользуйся» переменной можно также найти в статье Пьера Гутьерреса (Pierre Gutierrez) и Жан-Ива Жерарди (Jean-Yves Gerardy) «Causal Inference and Uplift Modeling: A review of the literature»: https://proceedings.mlr.press/v67/gutierrez17a/gutierrez17a.pdf. Обратите внимание, что эти статьи охватывают только случай бинарного воздействия. Еще один обзор причинно-следственных моделей для оценки CATE, в котором разбирается F-модель, – это статья Серена Кюнцеля и коллег от 2019 года (Kuenzel et al, 2019) «Meta-learners for Estimating Heterogeneous Treatment Effects using Machine Learning»: https://arxiv.org/abs/1706.03461. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Глава 21 Метамодели Итак, напомню, сейчас мы ищем гетерогенность эффекта воздействия, то есть мы оцениваем, как объекты по-разному реагируют на воздействие. В рамках этой задачи мы хотим оценить τ(x) = E[Yi (1) – Yi (0) | X ] = E[τi | X ], или E[δYi (t) | X ] в случае непрерывного воздействия. Другими словами, мы хотим знать, насколько объекты чувствительны к воздействию. Это особенно полезно в случае, когда невозможно подвергнуть воздействию всех и необходимо расставить приоритеты в плане воздействия, например когда вы хотите предоставить скидки, но у вас при этом ограничен бюджет. Ранее мы рассмотрели, как можно преобразовать переменную результата Y так, чтобы мы могли использовать ее в прогнозной модели и получить оценку условного среднего эффекта воздействия (CATE). При этом нам пришлось заплатить цену в виде увеличения дисперсии. Это типичное явление в data science. Нет единственного наилучшего метода, потому что у каждого метода есть свои плюсы и минусы. Поэтому стоит изучить множество методов, чтобы иметь возможность выбрать наиболее подходящий. Эта глава призвана дать вам дополнительные инструменты, чтобы расширить выбор. Метамодели представляют собой простой способ использования готовых прогнозных методов машинного обучения для решения все той же проблемы, которую мы рассматривали ранее, то есть для оценки CATE. Опять же, ни одна из них не является наилучшей, и у каждой из них есть свои слабые стороны. Я постараюсь рассмотреть их, но имейте в виду, что все это сильно зависит от контекста. Более того, метамодели используют прогнозные модели машинного обучения, которые могут варьировать от линейной регрессии и градиентного бустинга до нейронных сетей и гауссовых процессов.
­ 368  Метамодели Кроме того, успех метамодели будет сильно зависеть от того, какой метод машинного обучения используется в качестве ее компонент. Зачастую вам просто придется попробовать много разных вариантов и посмотреть, какой работает лучше. Мы воспользуемся теми же данными, что и ранее, они касаются рекламных электронных писем, посвященных инвестированию. Опять же, цель здесь – определить, кто лучше отреагирует на электронное письмо. Однако теперь есть одно небольшое отличие. На этот раз мы будем использовать неслучайные данные для обучения моделей и случайные данные для их валидации. Работа с неслучайными данными является более сложной задачей, поскольку метамоделям придется учитывать смещение данных И оценивать CATE одновременно. import pandas as pd import numpy as np from matplotlib import pyplot as plt import seaborn as sns from nb21 import cumulative_gain, elast test = pd.read_csv("data/invest_email_rnd.csv") train = pd.read_csv("data/invest_email_biased.csv") train.head() Наша зависимая переменная – это конверсия (converted), а переменная воздействия – это em1. Давайте запишем их, а также набор признаков X в отдельные переменные, с их помощью будем искать гетерогенность эффекта воздействия. y = "converted" T = "em1" X = ["age", "income", "insurance", "invested"] S-модель Первой моделью, которой мы воспользуемся, будет S-модель. Это самая простая модель, которая приходит на ум. Мы будем использовать одну (отсюда и S – single) модель машинного обучения Ms для оценки
S-модель  369 μ(x) = E[Y | T, X ]. Для этого в модель, которая пытается предсказать результат Y, мы просто включим воздействие в качестве признака. Хотя я буду использовать регрессионную модель для оценки E[Y | T, X ], поскольку переменная результата является бинарной, вы также можете использовать классификатор. Только не забудьте адаптировать код таким образом, чтобы модель выводила вероятности вместо бинарного класса (0, 1). from lightgbm import LGBMRegressor np.random.seed(123) s_learner = LGBMRegressor(max_depth=3, min_child_samples=30) s_learner.fit(train[X+[T]], train[y]); Затем нам нужно получить прогнозы при наличии воздействия (все наблюдения обучающего/тестового набора в столбце em1 принимают значения 1) и при отсутствии воздействия (все наблюдения обучающего/тестового набора в столбце em1 принимают значения 0). Разница в прогнозах, полученных для тестовой и контрольной групп, будет нашей оценкой CATE: τ̂(x)i = Ms(Xi, T = 1) – Ms(Xi, T = 0). Если мы представим это в виде диаграммы, то она будет выглядеть примерно так: Обучение Прогнозирование Х Х T=0 T=1 Х M(X, T) M(X, T) T y*|T = 0 y*|T = 1 CATE
­ 370  Метамодели Еще можно визуализировать так: Обучение Прогнозирование M(X, T) M(X, T) y*|T = 0 y*|T = 1 CATE Теперь пишем программный код для оценки CATE. # вычисляем разницу прогнозов между тестовой (T = 1) # и контрольной (T = 0) группами для обучающего набора s_learner_cate_train = (s_learner.predict(train[X].assign(**{T: 1})) s_learner.predict(train[X].assign(**{T: 0}))) # вычисляем разницу прогнозов между тестовой (T = 1) # и контрольной (T = 0) группами для тестового набора s_learner_cate_test = test.assign( cate=(s_learner.predict(test[X].assign(**{T: 1})) s_learner.predict(test[X].assign(**{T: 0}))) ) Для оценки качества этой модели мы рассмотрим кривую накопленного выигрыша на тестовом наборе данных. Кроме того, я построю кривую накоп ленного выигрыша для обучающего набора данных. Поскольку обучающий набор смещен, эта кривая не сможет ничего сказать о качестве модели, но она может указать на переобучение. Когда возникает переобучение, кривая на обучающем наборе становится очень высокой. Если вы хотите увидеть, как это выглядит, попробуйте сменить значение параметра max_depth с 3 на 20. gain_curve_test = cumulative_gain( s_learner_cate_test, "cate", y="converted", t="em1" ) gain_curve_train = cumulative_gain(
­ S-модель  371 train.assign(cate=s_learner_cate_train), "cate", y="converted", t="em1" ) plt.plot(gain_curve_test, color="C0", label="Тестовый набор") plt.plot(gain_curve_train, color="C1", label="Обучающий набор") plt.plot([0, 100], [0, elast(test, "converted", "em1")], linestyle="--", color="black", label="Базовая модель") plt.legend() plt.title("S-модель"); НЕ ХОРОШО, НЕ ПЛОХО Как видно на графике кривой накопленного выигрыша, S-модель, несмот ря на свою простоту, может неплохо справляться с поставленной задачей. Одно важное замечание – качество модели сильно зависит от конкретного набора данных. В зависимости от типа данных, которыми вы располагаете, S-модель может показывать наилучшие или наихудшие результаты. На практике я наблюдаю, что S-модель – неплохой первоначальный выбор для любой задачи каузального анализа преимущественно из-за своей простоты.
372  Метамодели Более того, S-модель может обрабатывать как непрерывные, так и дискретные виды воздействия, в то время как остальные модели в этой главе могут работать только с дискретными видами воздействия. Основным недостатком S-модели является ее склонность смещать эффект воздействия к нулю. Поскольку S-модель обычно использует регуляризованную модель машинного обучения, эта регуляризация может ограничивать оценку эффекта воздействия. Черножуков совместно с коллегами (Chernozhukov et al., 2016) выявили эту проблему, используя симулированные данные. Здесь они строят разницу между истинным причинно-следственным эффектом (красный контур) и оцененным причинно-следственным эффектом τ – τ̂ с помощью S-модели. Оценка причинно-следственного эффекта сильно смещена. Еще хуже, если влияние воздействия очень слабо по сравнению с влиянием других ковариат, объясняющих результат, S-модель может полностью отбросить переменную воздействия. Обратите внимание, что это тесно связано с выбранной вами моделью машинного обучения. Чем выше регуляризация, тем более выражена проблема. Один из способов исправить это – воспользоваться следующей моделью, которую мы сейчас рассмотрим. T-модель T-модель пытается решить проблему полного игнорирования переменной воздействия. Вместо использования одной модели мы будем использовать по одной модели для каждого значения переменной воздействия. В бинарном случае нам нужно обучить всего две модели (отсюда и название T – two):
T-модель  373 μ0(x) = E[Y | T = 0, X ], μ1(x) = E[Y | T = 1, X ]. Затем на этапе прогнозирования мы можем получить контрфактические прогнозы для каждого значения переменной воздействия и вычислить оценку CATE следующим образом: τ̂(x)i = M1(Xi ) – M0(Xi ). Вот диаграмма для этой модели: Обучение Х Х T=0 Прогнозирование Х Х M0(X) M1(X) y*|T = 0 y*|T = 1 M0(X) T T=1 Х M1(X) CATE Еще можно визуализировать так: Обучение Прогнозирование M0(X) M0(X) M1(X) y*|T = 0 y*|T = 1 M1(X) CATE
374  Метамодели Достаточно теории, пишем программный код. np.random.seed(123) m0 = LGBMRegressor(max_depth=2, min_child_samples=60) m1 = LGBMRegressor(max_depth=2, min_child_samples=60) m0.fit(train.query(f"{T}==0")[X], train.query(f"{T}==0")[y]) m1.fit(train.query(f"{T}==1")[X], train.query(f"{T}==1")[y]) # оцениваем CATE t_learner_cate_train = m1.predict(train[X]) - m0.predict(train[X]) gain_curve_test = cumulative_gain( t_learner_cate_test, "cate", y="converted", t="em1" ) gain_curve_train = cumulative_gain( train.assign(cate=t_learner_cate_train), "cate", y="converted", t="em1" ) plt.plot(gain_curve_test, color="C0", label="Тестовый набор") plt.plot(gain_curve_train, color="C1", label="Обучающий набор") plt.plot([0, 100], [0, elast(test, "converted", "em1")], linestyle="--", color="black", label="Базовая модель") plt.legend(); plt.title("T-модель"); T-модель тоже неплохо работает на этом наборе данных. Качество на тес­ товом наборе не сильно отличается от результатов S-модели. Возможно, это обусловлено тем, что влияние воздействия не так слабо. Кроме того, мы видим, что качество на обучающем наборе намного выше, чем на тестовом наборе. Это указывает на переобучение модели. Такое может произойти по причине того, что мы обучаем каждую модель только на подмножестве дан-
­ T-модель  375 ных. На меньшем количестве наблюдений модель, вероятно, выучивает некоторые шумы. T-модель избегает проблемы игнорирования слабой переменной воздействия, но все еще может страдать от регуляризационного смещения. Рассмотрим следующую ситуацию, взятую из статьи Серена Кюнцеля и коллег от 2019 года (Kuenzel et al, 2019) «Meta-learners for Estimating Heterogeneous Treatment Effects using Machine Learning»: https://cutt.ly/cwLUQcrH. Допустим, у вас есть много данных для объектов, не подвергнутых воздействию, и очень мало данных для объектов, подвергнутых воздействию. Это довольно распространенная ситуация во многих областях, поскольку воздействие часто является дорогостоящей процедурой. Теперь предположим, что у нас есть некоторая нелинейность результата Y, однако эффект воздействия постоянен. На рисунке ниже мы можем увидеть, что происходит. Поскольку у нас очень мало объектов, подвергнутых воздействию, M1 будет очень простой моделью (линейной в данном случае), чтобы избежать пере обучения. M0 будет более сложной, но это нормально, потому что достаточный объем данных предотвращает переобучение. Все это разумно с точки зрения машинного обучения. Однако если мы используем эти модели для вычисления CATE τ̂ = M1(X) – M0(X), линейность M1(X) за вычетом нелинейности M0(X) приведет к нелинейному CATE (синяя линия за вычетом красной линии), что неверно, поскольку CATE является постоянным и равным 1 в данном случае. В данном случае модель M0 для объектов, не подвергнутых воздействию, может улавливать нелинейность, однако модель M1 для объектов, подвергнутых воздействию, обнаружить нелинейность не может, поскольку мы использовали регуляризацию для работы с небольшим объемом выборки. Конечно же, вы могли бы использовать меньшую регуляризацию, но тогда небольшой объем выборки заставит вашу модель переобучаться. Кажется, что мы попали между молотом и наковальней. Для решения этой проблемы мы можем воспользоваться X-моделью, предложенной в той же статье Кюнцеля и его коллег.
­ 376  Метамодели X-модель X-модель значительно сложнее объяснить, чем предыдущие модели, но ее реализация довольно проста, так что не волнуйтесь. X-модель включает два этапа и модель оценки склонности. Первый этап идентичен T-модели. Сначала мы разбиваем наблюдения на тестовую и контрольную группы, обучаем модель машинного обучения для контрольной группы, затем обучаем модель машинного обучения для тестовой группы: M̂0(X) ≈ E[Y | T = 0, X ], M̂1(X) ≈ E[Y | T = 1, X ]. На втором этапе мы восстанавливаем эффект воздействия для контрольной и тестовой групп, используя вышеприведенные модели: τ̂(X, T = 0) = M̂1(X, T = 0) – YT = 0, τ̂(X, T = 1) = YT = 1 – M̂0(X, T = 1). Затем мы обучаем еще две модели для прогнозирования этих эффектов: M̂τ0(X) ≈ E[τ̂(X) | T = 0], M̂τ1(X) ≈ E[τ̂(X) | T = 1]. Если применить это к картинке, которую мы показали ранее, τ̂(X, T = 0), восстановленный эффект воздействия для объектов контрольной группы будет представлен красными крестиками, а модель M̂ τ0(X) будет красной пунктирной линией. Обратите внимание, что эта модель неверна, поскольку τ̂(X, T = 0) был получен с использованием регуляризованной простой модели, оцененной на объектах тестовой группы, M̂ 1. Эффект воздействия, который она восстанавливает, является нелинейным, поскольку она не улавливает нелинейность переменной Y. Синие точки – восстановленный эффект воздействия для объектов тестовой группы, τ̂(X, T = 1). Эти эффекты оцениваются с использованием правильной модели M0, обученной на объектах контрольной группы, большой выборке. В результате, поскольку ее восстановленный эффект воздействия является верным, мы можем обучить правильную модель второго этапа M̂τ1(X), представленную синей линией.
X-модель  377 Итак, у нас есть модель второго этапа M̂τ0(X), которая ошибочна, потому что мы неправильно восстановили эффект воздействия, и модель второго этапа M̂τ1(X), которая верна, потому что мы верно восстановили эффект воздействия. Теперь нам нужен способ объединить их таким образом, чтобы больший вес присваивался правильной модели. Здесь на сцену выходит модель оценки склонности. Пусть ê(x) будет моделью оценки склонности, тогда мы можем объединить две модели второго этапа следующим образом: τ̂(x) = M̂τ0(X)ê(x) + M̂τ1(X)(1 – ê(x)). Поскольку объектов тестовой группы очень мало, значение ê(x) является довольно маленьким. Это придаст неправильной модели M̂τ0(X) очень маленький вес. Напротив, 1 – ê(x) близко к единице, поэтому мы присвоим высокий вес правильной модели M̂τ1(X). В целом взвешенное среднее, используя оценку склонности, обеспечит более высокий вес модели CATE, в которой назначенное воздействие получает более высокие вероятности. Другими словами, мы будем отдавать предпочтение модели, которая была обучена с использованием большего объема данных. На следующем рисунке приведена оценка CATE, полученная с помощью X-модели и T-модели.
378  Метамодели Как видно, по сравнению с T-моделью X-модель гораздо лучше справляется с коррекцией неверно оцененного CATE при наличии нелинейности. В целом X-модель показывает лучшие результаты, когда тестовая группа намного больше, чем контрольная. Понимаю, что это может показаться немного сложным, но, надеюсь, все станет ясным, когда мы перейдем к программной реализации. Ниже приведем диаграмму, иллюстрирующую X-модель. Первый этап Х Х T=0 Y0 T Y Х T=1 Y1 ❶ Train Второй этап M1(X) – Y0 M0(X) Х CATE 0 ❺ Train CATE 1 ❻ Train Prediction ❷ Train M1(X) Х Y1 – M0(X) ❸ ❹ MTAU0(X) ❼ CATE Final MTAU1(X) ❹ PS(X) Наконец-то переходим к программному коду! Сначала реализуем первый этап, который полностью совпадает с T-моделью. from sklearn.linear_model import LogisticRegression np.random.seed(123) # модели первого этапа m0 = LGBMRegressor(max_depth=2, min_child_samples=30) m1 = LGBMRegressor(max_depth=2, min_child_samples=30) # модель оценки склонности g = LogisticRegression(solver="lbfgs", penalty='none') m0.fit(train.query(f"{T}==0")[X], train.query(f"{T}==0")[y]) ❶ m1.fit(train.query(f"{T}==1")[X], train.query(f"{T}==1")[y]) ❷ g.fit(train[X], train[T]); ❸ Теперь мы восстанавливаем эффект воздействия и обучаем модели второго этапа. d_train = np.where(train[T]==0, m1.predict(train[X]) - train[y], train[y] - m0.predict(train[X])) ❹ # модели второго этапа mx0 = LGBMRegressor(max_depth=2, min_child_samples=30)
X-модель  379 mx1 = LGBMRegressor(max_depth=2, min_child_samples=30) mx0.fit(train.query(f"{T}==0")[X], d_train[train[T]==0]) ❺ mx1.fit(train.query(f"{T}==1")[X], d_train[train[T]==1]); ❻ И наконец, мы получаем скорректированные прогнозы, используя модель оценки склонности. def ps_predict(df, t): return g.predict_proba(df[X])[:, t] x_cate_train = (ps_predict(train,1)*mx0.predict(train[X]) + ps_predict(train,0)*mx1.predict(train[X])) ❼ x_cate_test = test.assign(cate=(ps_predict(test,1)*mx0.predict(test[X]) + ps_predict(test,0)*mx1.predict(test[X]))) Давайте посмотрим, как справляется наша X-модель на тестовых данных. Снова построим кривую накопленного выигрыша. gain_curve_test = cumulative_gain( x_cate_test, "cate", y="converted", t="em1" ) gain_curve_train = cumulative_gain( train.assign(cate=x_cate_train), "cate", y="converted", t="em1" ) plt.plot(gain_curve_test, color="C0", label="Тестовый набор") plt.plot(gain_curve_train, color="C1", label="Обучающий набор") plt.plot([0, 100], [0, elast(test, "converted", "em1")], linestyle="--", color="black", label="Базовая модель") plt.legend(); plt.title("X-модель");
­ 380  Метамодели Вновь у нас неплохой результат на этом наборе данных. Здесь S-, Tи X-модели дают сопоставимое качество. Однако я считаю, что стоит знать обо всех имеющихся метамоделях, чтобы использовать ту, которая лучше всего подходит для вашего случая. Имейте в виду, что качество еще сильно зависит от выбранной базовой модели машинного обучения. Здесь мы все делали с помощью модели градиентного бустинга LightGBM, но, возможно, другие методы или та же модель градиентного бустинга LightGBM, но с иными значениями гиперпараметров может сработать лучше. Ключевые идеи Снова самое простое, что мы можем сделать, – это воспользоваться одной моделью (S-моделью) с воздействием в качестве признака. Это обычно хорошо работает в ситуациях, когда воздействие является сильным предик тором результата. Но если это не так (т. е. воздействие слабо прогнозирует результат), то S-модель имеет тенденцию быть смещенной к нулю или даже полностью отбрасывает (игнорирует) воздействие. Немного усложнив модель, мы можем заставить модель учесть воздействие, используя T-модель. Здесь мы подгоняем одну модель машинного обучения для каждого уровня переменной воздействия. Это хорошо работает, когда есть достаточно наблюдений для каждого уровня воздействия, но это может не сработать, когда у одного уровня воздействия – небольшое количество наблюдений, что приводит к сильной регуляризации модели. Чтобы исправить это, мы можем добавить еще один уровень сложности, используя X-модель, здесь у нас уже будут два этапа обучения, и мы используем модель оценки склонности для исправления потенциальных ошибок моделей, построенных на очень небольших объемах данных. Один из больших недостатков этих моделей (кроме S-модели) – это предположение о бинарном или категориальном характере воздействия. Есть еще одна модель, которую мы не разобрали, и ее можно распространить на большее количество типов воздействия: R-модель. Но не волнуйтесь, у нас будет целая глава, посвященная ей. Дополнительное чтение Материал, который я написал в этой главе, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако
­ ­ Дополнительное чтение  381 я выставляю этот материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. При написании этой главы я руководствовался библиотекой causalml от Uber и их документацией по метамоделям. Также многое было взято из статьи Серена Кюнцеля и коллег от 2019 года (Kuenzel et al, 2019) «Metalearners for Estimating Heterogeneous Treatment Effects using Machine Learning»: https://arxiv.org/abs/1706.03461. Наконец, утверждение о том, что S-модель смещена к нулю, было взято из статьи Черножукова и коллег от 2017 года (Chernozhukov et al., 2017) «Double/Debiased Machine Learning for Treatment and Causal Parameters»: https://arxiv.org/abs/1608.00060.
Глава 22 Несмещенное/ортогональное машинное обучение Следующая метамодель, которую мы рассмотрим, на самом деле появилась еще до того, как все эти подходы стали называться метамоделями. Насколько я могу судить, она появилась благодаря замечательной статье 2016 года, которая стала плодотворным направлением в литературе по анализу причинно-следственных связей. Эта статья называлась «Double Machine Learning for Treatment and Causal Parameters» («Двойное машинное обучение для воздействия и каузальных параметров»), и над ее написанием работало много людей: Виктор Черножуков (Victor Chernozhukov), Денис Четвериков (Denis Chetverikov), Мерт Демирер (Mert Demirer), Эстер Дюфло (Esther Duflo) (которая, кстати, получила Нобелевскую премию по экономике в 2020 году вместе с Абхиджитом Банерджи (Abhijit Banerjee) и Майклом Кремером (Michael Kremer) «за их экспериментальный подход к смягчению глобальной бедности»), Кристиан Хансен (Christian Hansen), Уитни Ньюи (Whitney Newey) и Джеймс Робинс (James Robins). Неудивительно, что это была очень хорошая статья, и я даже позволил себе представить ее авторов в роли Мстителей (с благодарностью Полу Голдсмит-Пинкхему (Paul Goldsmith-Pinkham), который придумал эту идею первым).
Дополнительное чтение  383 Есть только одна проблема с этой статьей: ее крайне сложно читать (что ожидаемо, так как это статья по эконометрике). И поскольку эта книга посвящена тому, чтобы сделать анализ причинно-следственных связей широко доступным, мы попытаемся сделать метод несмещенного/ортогонального машинного обучения (Debiased/Orthogonal Machine Learning) интуитивно понятным. Однако что делает его таким особенным, чтобы посвящать ему отдельную главу, в отличие от других метамоделей? Особенность, которая привлекла мое внимание, – это хорошая обоснованность метода несмещенного/ортогонального машинного обучения. Методы, которые мы рассматривали до сих пор (T-модель, S-модель и X-модель), кажутся немного хакерскими. Мы можем дать интуитивное объяснение, почему они работают, но они не кажутся универсальными. В отличие от них, у несмещенного/ортогонального машинного обучения есть подходящая для применения универсальная структура, которая одновременно очень интуитивна и очень строга. Дополнительным бонусом является и то, что метод несмещенного/ортогонального машинного обучения работает как в случае непрерывного воздействия, так и в случае дискретного воздействия, чего нельзя сказать о T-модели и X-модели. Не говоря уже о том, что в статье, описывающей метод, проделана невероятная работа по асимптотическому анализу этой модели. Так что, не теряя времени, давайте приступим. Вновь в качестве мотивирующего примера мы обратимся к нашему набору данных по продажам мороженого. Напомним, здесь мы пытаемся выявить гетерогенность воздействия цены на продажи. В нашем тестовом наборе цены назначены случайным образом, но наши обучающие данные содержат только наблюдаемые цены, такие данные потенциально подвержены смещению. import pandas as pd import numpy as np from matplotlib import pyplot as plt import seaborn as sns from nb21 import cumulative_gain, elast import statsmodels.formula.api as smf from matplotlib import style style.use("ggplot") test = pd.read_csv("data/ice_cream_sales_rnd.csv") train = pd.read_csv("data/ice_cream_sales.csv") train.head()
384  Несмещенное/ортогональное машинное обучение np.random.seed(123) sns.scatterplot(data=train.sample(1000), x="price", y="sales", hue="weekday"); Здесь источник смещения довольно ясен. Как мы видим, цены намного выше в выходные (дни недели 1 и 7), но у нас еще могут быть и другие спутывающие факторы, такие как температура и затраты на производство мороженого. Поэтому если мы хотим провести анализ причинно-следственных связей, нам нужно исправить это смещение. Машинное обучение для мешающих параметров Один из способов попытаться устранить это смещение – использовать линейную модель для оценки влияния цен (воздействия) на продажи (результат), учитывая спутывающие факторы: Salesi = α + τ pricei + β1tempi + β2costi + β3Weekdayi + ei, где β3 – вектор параметров, связанных с каждым днем недели, представленным в виде дамми-переменной. Обратите внимание, что нас интересует только параметр τ, потому что это эффект воздействия. Остальные параметры мы будем называть мешающими параметрами (nuisance parameters), потому что они нас не интересуют. Но, как оказывается, даже если они нас не интересуют, нам нужно правильно их
Теорема Фриша–Во–Ловелла  385 оценить, потому что иначе оценка эффекта воздействия будет неверной. Это в какой-то степени раздражает. Связь между температурой и продажами, вероятно, является нелинейной. Поначалу с увеличением температуры большее количество людей поедет на пляж и купит мороженое, поэтому продажи увеличатся. Но в какой-то момент станет слишком жарко, и люди решат, что лучше остаться дома. В этот момент продажи упадут. Вероятно, что связь между температурой и продажами достигает пика и затем снижается. Это означает, что вышеприведенная линейная модель будет, вероятно, некорректной. Ее формула должна быть примерно такой: Salesi = α + τ pricei + β1tempi + β2tempi2 + β3costi + β4Weekdayi + ei Продажи мороженого с квадратичным членом. Идеально Слишком жарко Слишком холодно Температура Мысль о необходимости контролировать мешающие параметры уже напрягает, даже если речь идет о нескольких ковариатах. Но что, если у нас их десятки или сотни? Для современных наборов данных это довольно распространенная ситуация. Так что же мы можем с этим сделать? Ответ кроется в самой крутой эконометрической теореме, когда-либо выведенной учеными. Теорема Фриша–Во–Ловелла Фриш (Frisch), Во (Waugh) и Ловелл (Lovell) – это эконометристы XX века, которые заметили самую интересную вещь в линейной регрессии. Она не является новостью для вас, поскольку мы уже говорили о ней в контексте остатков регрессии и при обсуждении фиксированных эффектов. Но так как эта теорема является ключевой для понимания ортогонального машинного обучения, ее очень полезно еще раз вспомнить. Предположим, у вас есть модель линейной регрессии с набором признаков X1 и еще одним набором признаков X2. Затем вы оцениваете параметры этой модели: Ŷ = β̂ 1 X1 + β̂ 2 X2,
   386  Несмещенное/ортогональное машинное обучение где X1 и X2 – это матрицы признаков (один столбец на признак и одна строка на наблюдение), а β̂ 1 и β̂ 2 – это векторы-строки. Вы можете получить точно такой же параметр β̂1, выполнив следующие шаги: 1) регрессируем результирующую переменную y на второй набор признаков ŷ* = γ̂1 X2; 2) регрессируем первый набор признаков на второй набор признаков X̂1 = γ̂2 X2; 3) получаем остатки X̃ 1 = X1 – X̂1 и ỹ = y – ŷ*; 4) регрессируем остатки результирующей переменной на остатки, полученные в результате регрессирования признаков ỹ = β̂ 1 X̃ 1. Это невероятно круто. Здесь у нас обобщенное представление, но обратите внимание, что один набор признаков может быть просто переменной воздействия. Это означает, что вы можете оценить все мешающие параметры отдельно. Сначала регрессируем результирующую переменную на признаки, чтобы получить остатки результирующей переменной. Затем регрессируем переменную воздействия на признаки, чтобы получить остатки переменной воздействия. Наконец, регрессируем остатки результирующей переменной на остатки, полученные в результате регрессирования признаков. Это даст точно такую же оценку, как если бы мы регрессировали результирующую переменную на признаки и переменную воздействия одновременно. Но не принимайте мое слово на веру. Теорема Фриша–Во–Ловелла – это та вещь, которую каждый, интересующийся анализом причинно-следственных связей, должен применить сам хотя бы раз. В нижеприведенном примере мы оцениваем эффект воздействия, сначала оценив эффекты ковариат на результирующую переменную (продажи) и переменную воздействия (цену). my = smf.ols("sales~temp+C(weekday)+cost", data=train).fit() mt = smf.ols("price~temp+C(weekday)+cost", data=train).fit() Затем с помощью остатков мы оцениваем средний эффект воздействия (ATE) цены на продажи. smf.ols("sales_res~price_res", data=train.assign(sales_res=my.resid, # остатки sales price_res=mt.resid) # остатки price ).fit().summary().tables[1] Мы получили оценку ATE –4, это означает, что каждое увеличение цены на одну единицу приведет к снижению продаж на 4 единицы. Теперь давайте оценим тот же самый параметр, но на этот раз мы включим в одну и ту же модель воздействие и ковариаты.
Теорема Фриша–Во–Ловелла  387 smf.ols("sales~price+temp+C(weekday)+cost", data=train).fit().params["price"] -4.000429145475454 Как видим, эти числа абсолютно идентичны! Это показывает, что математически оценка эффекта воздействия, вычисляемая сразу или поэтапно, как в теореме Фриша–Во–Ловелла, будет одной и то же. Еще можно сказать, что эффект воздействия можно получить из регрессии на остатки, где мы получаем остатки от регрессирования Y на X и затем регрессируем их на остатки от регрессирования T на X. Предположим, что ∼ является оператором регрессии, тогда мы можем сформулировать теорему Фриша–Во–Ловелла следующим образом: (Y – (Y ∼ X )) ∼ (T – (T ∼ X )), что по сути оценивает каузальный параметр τ в следующей модели: Yi = E [Yi | Xi] = τ · (T i – E[T i | Xi]) + ϵ. Как я уже упоминал, теорема Фриша–Во–Ловелла восхитительна, потому что она позволяет нам отделить процедуру оценки каузального параметра от процедуры оценки мешающих параметров. Но мы все еще не ответили на наш первоначальный вопрос: как избежать необходимости задавать правильную функциональную форму для мешающих параметров? Или, другими словами, как мы можем сосредоточиться только на каузальном параметре, не беспокоясь о мешающих параметрах? И здесь на сцену выходит машинное обучение.
   388  Несмещенное/ортогональное машинное обучение Теорема Фриша–Во–Ловелла на стероидах Двойное/несмещенное обучение можно рассматривать как теорему Фриша, Вауга и Ловелла на стероидах. Идея очень проста: использовать модели машинного обучения для получения остатков результирующей переменной и переменной воздействия: Yi – M̂y(Xi) = τ · (T i – M̂t (Xi)) + ϵ, где M̂y(Xi) оценивает E[Y | X ], а M̂t(Xi) оценивает E[T | X ]. Идея заключается в том, что модели машинного обучения являются очень гибкими, следовательно, они могут улавливать взаимодействия и нелинейности при вычислении остатков Y и T, сохраняя при этом ортогонализацию в соответствии с теоремой Фриша–Во–Ловелла. Это означает, что нам не нужно делать параметрических предположений о характере связи между ковариатами X и результирующей переменной Y, а также между ковариатами X и переменной воздействия T, чтобы получить корректный эффект воздействия. При условии отсутствия ненаблюдаемых факторов мы можем восстановить ATE с помощью следующей процедуры ортогонализации: 1. Оцениваем результирующую переменную Y с помощью признаков X, используя гибкую регрессионную модель машинного обучения My. 2. Оцениваем переменную воздействия T с помощью признаков X, используя гибкую регрессионную модель машинного обучения Mt. 3. Получаем остатки Ỹ = Y – My(X ) и T̃ = T – Mt(X ). 4. Регрессируем остатки результирующей переменной на остатки переменной воздействия Ỹ = α + τT̃, где τ – это каузальный параметр ATE, который мы можем оценить, например, с помощью МНК. Сила, которую дает машинное обучение, заключается в гибкости. Mашинное обучение является настолько мощным, что позволяет улавливать сложные функциональные формы мешающих зависимостей. Однако эта гибкость одновременно вызывает беспокойство, потому что это означает, что теперь нам нужно учитывать возможность переобучения. В чем проблема использования машинного обучения для анализа причинно-следственных связей? Я рад, что ты спросил
Теорема Фриша–Во–Ловелла на стероидах  389 Черножуков совместно с коллегами (Chernozhukov et al., 2016) дали более глубокое и строгое объяснение причин, в силу которых переобучение может вызывать проблемы, и я определенно рекомендую вам ознакомиться с ним. Но здесь я продолжу более интуитивное объяснение. Чтобы понять проблему, предположим, что ваша модель My переобучается. В этой ситуации остаток Ỹ будет меньше, чем должен быть. Это еще означает, что My улавливает нечто большее, чем просто связь между X и Y. Частью этого чего-то большего является связь между T и Y, и если My в какой-то степени улавливает ее, регрессия остатков будет смещена в сторону нуля. Другими словами, My улавливает причинно-следственную связь, не оставляя ее для итоговой регрессии остатков. Теперь рассмотрим проблему переобучения Mt. Переобученная модель объяснит больше дисперсии T, чем следовало бы. В результате остаток переменной воздействия будет обладать меньшей дисперсией, чем следовало бы. Если воздействие характеризуется меньшей дисперсией, то дисперсия итоговой модели будет высокой. Это как если бы воздействие было бы одинаковым для почти всех объектов. И если все объекты испытывают одинаковый уровень воздействия, становится очень сложно оценить, что произошло бы при разных уровнях воздействия. Заметим, что это также произойдет, когда T является детерминированной функцией от X, что означает нарушение предположения о позитивности. Вот это и есть проблемы, с которыми мы столкнемся при использовании моделей машинного обучения, но как их можно исправить? Ответ заключается в применении перекрестной проверки и вычислении остатков для тестовых блоков перекрестной проверки (остатков out-of-fold). Прогнозируем Оцениваем Прогнозируем Оцениваем Оцениваем Данные Оцениваем Оцениваем Прогнозируем Оцениваем Прогнозируем Прогнозируем Мы разбиваем наши данные на K блоков равного размера. Затем на каждой итерации мы обучаем модели машинного обучения на K – 1 блоках и получаем остатки для блока, не участвовавшего в обучении. Обратите внимание, что эти остатки мы получаем на основе прогнозов, полученных для блока, не участвовавшего в обучении (прогнозов out-of-fold). Мы обучаем модель на одной части данных (обучающих блоках), но делаем прогнозы и вычисляем остатки на другой части данных (тестовом блоке). Таким образом, даже если модель переобучается, она не приведет остатки к искусственному нулю. Наконец, мы объединяем прогнозы по всем K тестовым блокам для оценки итоговой причинно-следственной модели Ỹ = α + τT̃.
390  Несмещенное/ортогональное машинное обучение Итак, мы охватили большой объем материала, и, возможно, становится трудно следить за мыслью без примера. Чтобы подкрепить всю эту теорию практикой, давайте поэтапно рассмотрим реализацию двойного/несмещенного машинного обучения. При этом я попытаюсь объяснить, что происходит на каждом этапе. Сначала давайте оценим мешающие зависимости с помощью моделей машинного обучения. Я начну с модели воздействия Mt. Мы будем использовать модель LGBM для прогнозирования цен по ковариатам temp, weekday и cost. Эти прогнозы будут прогнозами перекрестной проверки, которые мы можем получить с помощью функции cross_val_predict() из библиотеки sklearn. Я также добавляю среднее μ̂t к остаткам просто для визуализации. from lightgbm import LGBMRegressor from sklearn.model_selection import cross_val_predict y = "sales" T = "price" X = ["temp", "weekday", "cost"] debias_m = LGBMRegressor(max_depth=3) train_pred = train.assign( price_res = train[T] cross_val_predict(debias_m, train[X], train[T], cv=5) + train[T].mean()) # добавляем mu_t для визуализации Обратите внимание, что я назвал модель Mt моделью, устраняющей смещение (debias_m). Это обусловлено тем, что роль, которую эта модель играет в двойном/несмещенном машинном обучении, заключается в том, чтобы устранить смещение воздействия. Остатки T̃ = T – Mt(X ) можно рассматривать как вариант воздействия, в котором все смещение, вызванное спутывающими факторами, удалено моделью из X. Другими словами, остатки T̃ ортогональны X. Интуитивно понятно, что T̃ уже нельзя объяснить с помощью X, потому что это уже было сделано. Чтобы убедиться в этом, мы можем показать тот же самый график, который видели ранее, но теперь заменим цену остатками цены. Помните, раньше в выходные цены были выше? Теперь это смещение исчезло. Все дни имеют одинаковое распределение остатков цены. np.random.seed(123) sns.scatterplot(data=train_pred.sample(1000), x="price_res", y="sales", hue="weekday");
Теорема Фриша–Во–Ловелла на стероидах  391 Роль модели Mt заключается в устранении смещения воздействия, но как насчет My? Ее роль – удалить дисперсию из Y. Поэтому я назову ее моделью, устраняющей шум. Интуитивно понятно, что My создает результирующую переменную, в которой вся дисперсия, обусловленная X, объяснена. В результате становится проще получить причинно-следственную оценку в Ỹ . Поскольку в Ỹ меньше шума, причинно-следственная связь становится более явной. denoise_m = LGBMRegressor(max_depth=3) train_pred = train_pred.assign( sales_res = train[y] cross_val_predict(denoise_m, train[X], train[y], cv=5) + train[y].mean()) Если мы построим тот же самый график, что и раньше, но теперь заменим продажи остатками продаж, мы увидим, что дисперсия в Y стала намного меньше, чем была ранее. np.random.seed(123) sns.scatterplot(data=train_pred.sample(1000), x="price_res", y="sales_res", hue="weekday");
392  Несмещенное/ортогональное машинное обучение Теперь легко увидеть отрицательную связь между ценами и продажами (выше цена – меньше продажи). Наконец, чтобы оценить эту причинно-следственную связь, мы можем регрессировать остатки продаж на остатки цены. final_model = smf.ols(formula='sales_res ~ price_res', data=train_pred).fit() final_model.summary().tables[1] Как видно, когда мы используем резидуализированную, или ортогонализированную, версию продаж и цены, мы можем быть уверены в том, что связь между ценами и продажами является отрицательной, что вполне логично. По мере увеличения цен спрос на мороженое должен снижаться. Но если мы посмотрим на исходную (дерезидуализированную) связь между ценами и продажами, то из-за смещения увидим положительную связь. Это происходит из-за того, что в ожидании высоких продаж цены повышаются. final_model = smf.ols(formula='sales ~ price', data=train_pred).fit() final_model.summary().tables[1]
Оценивание CATE с помощью двойного машинного обучения  393 Оценивание CATE с помощью двойного машинного обучения До сих пор мы видели, как двойное/несмещенное машинное обучение позволяет нам сфокусироваться на оценке среднего эффекта воздействия (ATE), но его также можно использовать для оценки гетерогенности эффекта воздействия или условного среднего эффекта воздействия (CATE). По сути, мы теперь утверждаем, что каузальный параметр τ изменяется в зависимости от значений ковариат объекта: Yi – My(Xi) = τ(Xi) · (T i – Mt (Xi )) + ϵ. Для оценивания этой модели воспользуемся той же самой резидуализированной, или ортогонализированной, версией цен и продаж, но теперь включим взаимодействие остатков цен с другими ковариатами. Затем мы можем обучить линейную модель для получения оценки CATE: Ỹ i = α + β1T̃ i + β2 Xi T̃ i + ϵi. После построения модели для получения прогнозов CATE мы воспользуемся рандомизированным тестовым набором. Поскольку эта итоговая модель является линейной, мы можем автоматически вычислить CATE: μ̂(∂Salesi, Xi ) = M(Price = 1, Xi ) – M(Price = 0, Xi ), где M – это наша итоговая линейная модель. final_model_cate = smf.ols( formula='sales_res ~ price_res * (temp + C(weekday) + cost)', data=train_pred).fit() cate_test = test.assign( cate=final_model_cate.predict(test.assign(price_res=1)) - final_model_cate.predict(test.assign(price_res=0)) ) Чтобы выяснить, насколько хорошо эта модель отличает объекты с высокой чувствительностью к ценам от объектов с низкой чувствительностью к ценам, мы воспользуемся кривой накопленной эластичности. gain_curve_test = cumulative_gain(cate_test, "cate", y=y, t=T) plt.plot(gain_curve_test, color="C0", label="Тестовый набор") plt.plot([0, 100], [0, elast(test, y, T)], linestyle="--", color="black", label="Базовая модель") plt.legend(); plt.title("R-модель");
394  Несмещенное/ортогональное машинное обучение Процедура двойного/несмещенного машинного обучения с итоговой линейной моделью уже очень хороша, как видно из графика выше. Но, возможно, мы можем сделать ее еще лучше. Фактически перед нами – очень универсальная процедура, которую мы можем представить в виде метамодели. Ние (Nie) и Уэйджер (Wager) назвали ее R-моделью в знак признания работы Робинсона (Robinson, 1988) и желания подчеркнуть роль резидуализации (процедуры вычисления остатков). В основе этой универсальной концепции лежит понимание того, что процедура двойного/несмещенного машинного обучения задает новую функцию потерь, которую мы можем минимизировать как угодно. Далее мы увидим, как это реализовать с помощью способа, очень похожего на ранее обсуждавшийся метод преобразования зависимой переменной, или F-модель. Непараметрическое двойное/ несмещенное машинное обучение Восхитительный аспект двойного машинного обучения заключается в том, что оно освобождает нас от всех хлопот по изучению мешающих параметров в причинно-следственной модели. Благодаря этому мы можем сосредоточить все наше внимание на изучении интересующего каузального параметра, будь то ATE или CATE. Однако с учетом вышеприведенной спецификации после резидуализации мы все еще использовали линейную модель в качестве итоговой причинно-следственной модели. Для нашего примера это означает, что мы предполагаем, что цена влияет на продажи линейно. Вероятно, это нормально для небольшого диапазона цен, но из микроэкономической
Непараметрическое двойное/несмещенное машинное обучение  395 теории мы знаем, что это не обязательно так. Может случиться так, что при низких ценах повышение цены на единицу снизит спрос на 2 единицы. Но затем при более высоких ценах повышение цены на единицу снизит спрос всего на 1 единицу. Здесь уже речь идет о нелинейной зависимости. Мы могли бы здесь воспользоваться микроэкономической теорией, чтобы порассуждать о функциональной форме результата воздействия, но, возможно, мы можем и эту задачу делегировать модели машинного обучения. Другими словами, предоставим машине самой изучить эту сложную функциональную форму. Как оказалось, это вполне возможно, если мы внесем несколько изменений в наш оригинальный алгоритм двойного/несмещенного машинного обучения. Мы начинаем точно так же, как и раньше, ортогонализируя воздействие и результат, используя прогнозы модели машинного обучения, полученные в ходе перекрестной проверки. y = "sales" T = "price" X = ["temp", "weekday", "cost"] debias_m = LGBMRegressor(max_depth=3) denoise_m = LGBMRegressor(max_depth=3) train_pred = train.assign( price_res = train[T] - cross_val_predict( debias_m, train[X], train[T], cv=5), sales_res = train[y] - cross_val_predict( denoise_m, train[X], train[y], cv=5) ) Пока ничего не изменилось. Теперь начинается самое интересное. Напомним, что двойное/несмещенное машинное обучение моделирует данные следующим образом: Yi = M̂y(Xi ) + τ(Xi)(T i – M̂t (Xi )) + ϵ̂i, где M̂y и M̂t – это модели, которые предсказывают результат и воздействие на основе признаков соответственно. Если мы переставим члены уравнения местами, мы можем изолировать член ошибки ϵ̂i = (Yi – M̂y(Xi )) – τ(Xi)(T i – M̂t (Xi )). Это просто потрясающе, потому что теперь мы можем назвать это выражение причинно-следственной, или каузальной, функцией потерь (causal loss function). Это означает, что, минимизируя квадрат этой функции потерь, мы оцениваем ожидаемое значение τ(Xi ), которое является условным средним эффектом воздействия (CATE):
 ­ 396  Несмещенное/ортогональное машинное обучение Эта функция потерь также называется R-функцией потерь (R-Loss), поскольку именно ее минимизирует R-модель. Хорошо, но как нам минимизировать эту функцию потерь? На самом деле существует несколько способов, однако здесь мы рассмотрим самый простой. Во-первых, для упрощения давайте перепишем нашу функцию потерь, используя резидуализированные варианты воздействия и результата: Наконец, мы можем проделать некоторый алгебраический паркур, чтобы вынести T̃ i за скобки и изолировать τ(Xi ) в квадратной части функции потерь: Минимизация вышеприведенной функции потерь эквивалентна минимизации разницы внутри круглых скобок, но при этом каждый член взвешивается по T̃ i2. Минимизация разницы внутри круглых скобок эквивалентна прогнозированию . Этот прием называется трюком со взвешиванием, чтобы получить непараметрическую причинно-следственную функцию потерь. Обратите внимание, насколько это похоже на идею преобразования зависимой переменной, которую мы разбирали ранее. Это по сути преобразование зависимой переменной, но дополнительно использующее трюк со взвешиванием. Прежде чем мы перейдем к программному коду, подведем итог. Теперь, когда у нас есть модели помех и резидуализированные версии воздействия и результата, мы: 1) создаем веса T̃ i2; 2) создаем зависимую переменную ; 3) применяем любой метод прогнозирования, чтобы спрогнозировать зависимую переменную (2), используя веса (1). И вот теперь перейдем к программному коду. Вы увидите, что он невероятно простой. model_final = LGBMRegressor(max_depth=3) # создаем веса w = train_pred["price_res"] ** 2 # создаем преобразованную зависимую переменную y_star = (train_pred["sales_res"] / train_pred["price_res"]) # используем взвешенную регрессионную модель машинного обучения, # чтобы спрогнозировать зависимую переменную с помощью весов model_final.fit(X=train[X], y=y_star, sample_weight=w);
­ Непараметрическое двойное/несмещенное машинное обучение  397 Вышеприведенная модель машинного обучения, являясь прогнозной моделью, оценивает CATE. В этом и заключается сила непараметрической модели двойного машинного обучения. Раньше мы использовали линейную регрессию в качестве итоговой модели для оценки CATE. Теперь, поскольку мы задали общую функцию потерь, мы можем использовать в качестве итоговой модели любую имеющуюся в нашем распоряжении прогнозную модель. Давайте теперь воспользуемся тестовым набором, чтобы сравнить эту непараметрическую версию с линейной версией, которую мы использовали ранее. Сначала мы оценим индивидуальный эффект воздействия. cate_test_non_param = test.assign(cate=model_final.predict(test[X])) Далее мы можем построить кривую непараметрической накопленной элас тичности вместе с кривой, которую мы получили с помощью параметрического (линейного) варианта двойного/ортогонального машинного обучения. gain_curve_test_non_param = cumulative_gain( cate_test_non_param, "cate", y=y, t=T ) plt.plot(gain_curve_test_non_param, color="C0", label="Непараметрический вариант") plt.plot(gain_curve_test, color="C1", label="Параметрический вариант") plt.plot([0, 100], [0, elast(test, y, T)], linestyle="--", color="black", label="Базовая модель") plt.legend(); plt.title("R-модель");
­ 398  Несмещенное/ортогональное машинное обучение Не слишком большое улучшение, но это уже кое-что. Кроме того, отсутствие необходимости указывать функциональную форму функции воздействия уже является огромным преимуществом. Что такое непараметрическая оценка? Прежде чем мы продолжим, я хотел бы обратить внимание на одно распространенное заблуждение. Когда мы думаем об использовании непараметрической модели двойного машинного обучения для оценки CATE, кажется, что мы получим нелинейный эффект воздействия. Например, предположим очень простой процесс генерации данных, в котором скидка влияет на продажи нелинейно, через функцию квадратного корня: Эффект воздействия определяется производной функции зависимости продаж (результата) от скидки (воздействия): Как мы видим, эффект воздействия не является линейным. Он ослабевает по мере увеличения воздействия. Это имеет важный смысл для процесса, генерирующего данные. Сначала небольшая скидка значительно увеличивает продажи. Но когда мы даем слишком большую скидку, дополнительная единица скидки будет влиять на продажи все меньше и меньше, потому что люди не склонны покупать до бесконечности. Следовательно, скидка будет действовать только до некоторого момента насыщения. Вопрос заключается в том, сможет ли непараметрическое машинное обучение уловить в эффекте воздействия поведение, связанное с насыщением. Может ли оно выполнить экстраполяцию небольшой скидки так, что если скидка становится еще больше, эффект воздействия становится ниже? Ответ – ну типа да. Чтобы лучше разобраться в этом, давайте сгенерируем данные, как в вышеприведенном примере. np.random.seed(321) n=5000 discount = np.random.gamma(2,10, n).reshape(-1,1) discount.sort(axis=0) # для лучшей визуализации sales = np.random.normal(20+10*np.sqrt(discount), 1) Если мы визуализируем процесс, генерирующий данные, то увидим зависимость между продажами и скидкой в виде квадратного корня.
Что такое непараметрическая оценка  399 plt.plot(discount, 20 + 10*np.sqrt(discount)) plt.ylabel("Продажи") plt.xlabel("Скидка"); Теперь давайте применим к этим данным непараметрическое двойное/ несмещенное машинное обучение. debias_m = LGBMRegressor(max_depth=3) denoise_m = LGBMRegressor(max_depth=3) # этап ортогонализации discount_res = discount.ravel() - cross_val_predict( debias_m, np.ones(discount.shape), discount.ravel(), cv=5 ) sales_res = sales.ravel() - cross_val_predict( denoise_m, np.ones(sales.shape), sales.ravel(), cv=5 ) # итоговая, непараметрическая причинно-следственная модель non_param = LGBMRegressor(max_depth=3) w = discount_res ** 2 y_star = sales_res / discount_res non_param.fit(X=discount_res.reshape(-1,1), y=y_star.ravel(), sample_weight=w.ravel()); С помощью вышеприведенной модели мы можем получить оценку CATE. Проблема заключается в том, что CATE не является линейным. По мере увеличения воздействия CATE должен уменьшаться. Вопрос, на который мы пытаемся ответить, заключается в том, может ли непараметрическая модель отразить эту нелинейность.
400  Несмещенное/ортогональное машинное обучение Чтобы правильно ответить на этот вопрос, давайте вспомним, какое предположение о процессе, генерирующем данные, выдвигает двойное/несмещенное машинное обучение. Это предположение можно увидеть в уравнении, которое мы приводили ранее: Ỹi = τ(Xi)T̃i + ei. Другими словами, оно гласит, что резидуализированный результат равен резидуализированному воздействию, умноженному на условный эффект воздействия. Это означает, что воздействие влияет на результат линейно. Здесь нет никакой нелинейности. Согласно вышеприведенной модели, если мы увеличим воздействие с 1 до 10 или со 100 до 110, результат возрастет на фиксированную величину τ(Xi ). Это простое умножение. Значит ли это, что непараметрическая модель не может отразить нелинейность эффекта воздействия? Опять же, не совсем... Скорее, дело в том, что двойное машинное обучение выполняет локальную линейную аппроксимацию нелинейного CATE. Другими словами, оно каждый раз находит производную результата по воздействию для данного уровня воздействия. Это эквивалентно поиску тангенса угла наклона касательной, проведенной к графику функции результата в точке воздействия, т. е. эквивалентно поиску углового коэффициента касательной. Это означает, что да, непараметрическое двойное машинное обучение определит, что эффект воздействия становится меньше по мере увеличения воздействия. Однако оно скорее найдет локальный линейный эффект воздействия вместо нелинейного эффекта воздействия. Мы даже можем построить график этих линейных аппроксимаций, сравнив с истинным нелинейным причинно-следственным эффектом, и действительно, эти линейные аппроксимации являются полезными. cate = non_param.predict(X=discount) plt.figure(figsize=(15,5))
Что такое непараметрическая оценка  401 plt.subplot(1,2,1) plt.scatter(discount, sales) plt.plot(discount, 20 + 10*np.sqrt(discount), label="Истинный эффект", c="C1") plt.title("Продажи в зависимости от скидки") plt.xlabel("Скидка") plt.legend() plt.subplot(1,2,2) plt.scatter(discount, cate, label="$\hat{\\tau}(x)$", c="C4") plt.plot(discount, 5/np.sqrt(discount), label="Истинный эффект", c="C2") plt.title("CATE в зависимости от скидки") plt.xlabel("Скидка") plt.legend(); Вся эта информация может показаться перегруженной техническими деталями, но у нее есть весьма полезное практическое применение. Предположим, в вышеприведенном примере вы обнаружили для клиента эффект воздействия, равный 2, это означает, что если вы увеличиваете скидку на 1 единицу, ваши продажи по этому клиенту увеличиваются на 2 единицы. Вы можете посмотреть на это и подумать: «Отлично! Я дам этому клиенту скидку большего размера! Ведь за каждую единицу скидки я получу дополнительно 2 единицы продаж». Однако это неверный вывод. Эффект воздействия равен 2 только для этого уровня скидки. Как только вы увеличите скидку, эффект уменьшится. Предположим, что этот гипотетический клиент получил только 5 единиц скидки и поэтому эффект воздействия на него так высок. Скажем, вы наблюдаете этот огромный эффект воздействия и на этом основании хотите дать 20 единиц скидки этому клиенту. Однако в этом случае эффект может уменьшиться с 2 до 0.5, например. И скидка в 20 единиц, которая име-
402  Несмещенное/ортогональное машинное обучение ла смысл при эффекте воздействия 2, может перестать быть прибыльной при эффекте воздействия 0.5. Это означает, что вам нужно быть особенно осторожными при экстраполяции нелинейного эффекта воздействия на новый уровень воздействия. Если вы не будете осторожны, вы можете принять весьма убыточные решения. Когда эффект воздействия является нелинейным, даже непараметрическая модель двойного/несмещенного машинного обучения будет сталкиваться с трудностями в прогнозировании контрфактических результатов. Она будет пытаться линейно экстраполировать эффект воздействия, переходя от низкого уровня воздействия к высокому или наоборот. И в силу нелинейности эта экстраполяция, скорее всего, будет неточной. Для решения данной проблемы рассмотрим еще одну идею. Имейте в виду, что эта идея гораздо менее научная, в отличие от увиденных ранее. Она сводится к использованию S-модели после применения процедуры ортогонализации, но я опережаю события. Давайте все разберем по порядку. Ненаучное двойное/несмещенное машинное обучение Последняя идея, которую мы попробуем, – это фундаментальная смена мышления. Мы больше не будем пытаться оценивать линейную аппроксимацию CATE. Вместо этого мы будем делать контрфактические прогнозы.
Линейная аппроксимация CATE Sales Sales Ненаучное двойное/несмещенное машинное обучение  403 Price Контрфактические прогнозы Price CATE (Conditional Average Treatment Effect) представляет собой наклон касательной функции результата в точке данных. Мы вычисляем, насколько изменится результат, если увеличить воздействие на очень небольшую величину. Говоря более техническим языком, это производная в данной точке. С другой стороны, контрфактические прогнозы – это попытка воссоздать всю кривую результатов из единственной точки данных. Мы прогнозирум, каким был бы результат, если бы воздействие было на каком-то другом уровне, отличающемся от текущего, отсюда и название «контрфактический». Если нам удастся это сделать, мы сможем моделировать различную величину воздействия для объекта и прогнозировать, как он среагирует при различных уровнях воздействия. Это очень рискованный подход, поскольку мы будем экстраполировать всю кривую, исходя из единственной точки. Кроме того, хотя я часто использовал этот метод на практике, я так и не нашел научной статьи, объясняющей, как или почему он работает. Вот почему я называю его ненаучным двойным машинным обучением. Проще говоря, будьте осторожны! Сначала давайте начнем с традиционной формулировки двойного/несмещенного машинного обучения, в котором у нас есть резидуализированная версия воздействия и результата: Ỹi = τ(Xi)T̃i + ei. Теперь помещаем воздействие внутрь функции эффекта воздействия. Это позволяет эффекту воздействия быть нелинейным, то есть эффект воздействия может изменяться в зависимости от самого воздействия: Ỹi = τ(Xi, T̃i ) + ei. Это опасное дело, потому что у нас нет представления, как это воздействие функционирует. Мы знаем, что оно может выглядеть как какая-нибудь странная нелинейная функция. Но, к счастью, мы знаем, как оценивать странные функции с помощью машинного обучения. Именно это мы и сделаем. Проще говоря, мы обучим модель машинного обучения, чтобы спрогнозировать резидуализированный результат Ỹ на основе резидуализированного воздействия T̃ и признаков X. Резидуализация важна для удаления смеще-
404  Несмещенное/ортогональное машинное обучение ния и шума, чтобы эта итоговая модель могла сосредоточиться только на исследовании эффекта воздействия и влияния ковариат X на этот эффект воздействия. Обучив модель, мы получаем контрфактические прогнозы в два этапа. Сначала мы делаем прогноз для воздействия, чтобы получить T̃, затем мы подаем этот прогноз вместе с признаками в нашу итоговую модель τ̂(Xi, T̃ i). Поскольку нам нужно получить T̃, надо сначала реализовать функцию, которая вернет не только прогнозы перекрестной проверки, но и модели, использованные для получения этих прогнозов. from sklearn.model_selection import KFold def cv_estimate(train_data, n_splits, model, model_params, X, y): cv = KFold(n_splits=n_splits) models = [] cv_pred = pd.Series(np.nan, index=train_data.index) for train, test in cv.split(train_data): m = model(**model_params) m.fit(train_data[X].iloc[train], train_data[y].iloc[train]) cv_pred.iloc[test] = m.predict(train_data[X].iloc[test]) models += [m] return cv_pred, models Теперь, когда у нас есть собственная функция, которая возвращает прог­ нозы и модели перекрестной проверки, мы можем приступить к этапу ортогонализации. y = "sales" T = "price" X = ["temp", "weekday", "cost"] debias_m = LGBMRegressor(max_depth=3) denoise_m = LGBMRegressor(max_depth=3) y_hat, models_y = cv_estimate( train, 5, LGBMRegressor, dict(max_depth=3), X, y ) t_hat, models_t = cv_estimate( train, 5, LGBMRegressor, dict(max_depth=3), X, T ) y_res = train[y] - y_hat t_res = train[T] - t_hat После ортогонализации мы передаем T̃ вместе с X в модель машинного обуче­ния, которая пытается спрогнозировать Ỹ. Здесь я использую модель LGBM, но вы можете выбрать любую модель машинного обучения. Одна интересная особенность LGBM заключается в том, что я могу задать для нее монотонные ограничения. Учитывая наши знания о ценах, продажи должны умень-
Ненаучное двойное/несмещенное машинное обучение  405 шаться при увеличении цены. Мы можем учесть это и ограничить нашу модель LGBM так, чтобы ее прогнозы не увеличивались при увеличении цены. # –1 для price (T) означает, что прогнозы не # должны увеличиваться по мере роста цены monotone_constraints = [-1 if col == T else 0 for col in X+[T]] model_final = LGBMRegressor( max_depth=3, monotone_constraints=monotone_constraints ) model_final = model_final.fit(X=train[X].assign(**{T: t_res}), y=y_res) Сейчас все станет немного странным. Если задуматься, итоговая модель машинного обучения оценивает следующую функцию воздействия τ: Ỹi = τ(Xi, T̃i ) + ei. Однако явного способа извлечь эффект воздействия из этой функции не существует. Поэтому вместо извлечения эффекта воздействия мы будем получать контрфактические прогнозы так же, как я показывал на предыдущем рисунке. Мы будем симулировать различные уровни цен для каждого объекта и использовать нашу модель двойного машинного обучения, чтобы спрогнозировать объем продаж при различных уровнях цен. С этой целью мы выполним следующие шаги: 1) выполним слияние тестового набора данных с таблицей цен, содержащей все симулированные цены. Конечный результат будет следующим: pred_test = (test # в тестовом наборе переименовываем # столбец price в factual_price .rename(columns={"price":"factual_price"}) # в тестовом наборе создаем вспомогательный # столбец jk c константным значением 1 .assign(jk = 1) # в тестовом наборе создаем столбец # index – идентификатор дня .reset_index() # создаем датафрейм со вспомогательным столбцом jk и столбцом # price с 9 симулированными ценами, затем выполняем # слияние тестового набора и набора с 9 симулированными # ценами по столбцу jk (длина с 5000 возрастает # до 5000 × 9 = 45 000 строк) .merge(pd.DataFrame(dict(jk=1, price=np.linspace(3, 10, 9))), on="jk") # удаляем столбец jk .drop(columns=["jk"])) pred_test.query("index==0")
406  Несмещенное/ортогональное машинное обучение Обратите внимание, что здесь мы привели только один день, день с индексом 0, то есть показали информацию по одному объекту. В этот день (т. е. для этого объекта) фактическая цена или воздействие была равна 7. Но мы симулировали различные контрфактические воздействия, от 3 до 10. Теперь мы передадим все эти контрфактические цены в нашу причинно-следственную модель, которая будет генерировать контрфактические прогнозы продаж на основе этих симулированных цен. Поскольку наша модель имеет следующий формат: прежде чем генерировать контрфактические прогнозы продаж, нам нужно получить T̃ i, т. е. остатки цен. Для этого сначала получаем прогнозы цен с помощью всех наших моделей воздействия (вспомним, что мы использовали 5-блочную перекрестную проверку при обучении) и затем усредняем прог­ нозы пяти моделей, наконец, вычитаем из симулированных контрфактических цен усредненные прогнозы цен. def ensamble_pred(df, models, X): return np.mean([m.predict(df[X]) for m in models], axis=0) t_res_test = pred_test[T] - ensamble_pred(pred_test, models_t, X) pred_test[f"{y}_pred"] = model_final.predict( X=pred_test[X].assign(**{T: t_res_test}) ) pred_test.query("index==0")
Ненаучное двойное/несмещенное машинное обучение  407 Видим, теперь у нас для каждой симулированной цены есть свой прогноз продаж. Чем ниже цена, тем выше продажи. Прогнозы варьируют в диапазоне от 24 до –24. Это обусловлено тем, что модель прогнозирует резидуализированный результат, который приблизительно равен нулю. Это нормально, если вам нужно получить наклон кривой продаж, который представляет собой эффект ценового воздействия. Кроме того, если вы хотите скорректировать прогнозы, все, что вам нужно сделать, – это добавить прогнозы модели, устраняющей шум My. y_hat_test = ensamble_pred(pred_test, models_y, X) pred_test[f"{y}_pred"] = ( y_hat_test + model_final.predict(X=pred_test[X].assign(**{T: t_res_test})) ) pred_test.query("index==0")
408  Несмещенное/ортогональное машинное обучение Кроме того, можно построить кривую продаж на уровне объекта. Давайте случайно отберем десять объектов и посмотрим, как они будут вести себя при разных ценах. np.random.seed(1) sample_ids = np.random.choice(pred_test["index"].unique(), 10) plt.figure(figsize=(10, 8)) ax = sns.lineplot(data=pred_test.query("index in @sample_ids"), x="price", y="sales_pred", legend="full", hue="index"); Интересно отметить, что некоторые объекты очень чувствительны к повышению цен. В некоторых случаях мы ожидаем, что продажи упадут с 250 до почти 200 при увеличении цены с 3 до 10. С другой стороны, некоторые объекты являются очень неэластичными по цене: при увеличении цен с 3 до 10 мы ожидаем, что продажи упадут примерно с 195 до 185. Эти различия в чувствительности к цене трудно заметить, поэтому я предпочитаю привести все кривые к одной и той же отправной точке (средним продажам). Так нам проще будет увидеть, что некоторые объекты демонстрируют резкое падение продаж при увеличении цен, тогда как у других объектов это происходит не так сильно.
Возможно, потребуется больше эконометрики  409 np.random.seed(1) sample_ids = np.random.choice(pred_test["index"].unique(), 10) plt.figure(figsize=(10, 4)) sns.lineplot(data=(pred_test .query("index in @sample_ids") .assign(max_sales = lambda d: d.groupby("index")[ ["sales_pred"]].transform("max")) .assign(sales_pred = lambda d: d["sales_pred"] d["max_sales"] + d["sales_pred"].mean())), x="price", y="sales_pred", legend="full", hue="index"); Возможно, потребуется больше эконометрики! Я хочу завершить раздел о ненаучном двойном машинном обучении предостережением. Я назвал ВНИМАНИЕ этот подход «ненаучным» не просто так. Это своего рода хак для получения нелинейных контр­ фактических прогнозов. И поскольку это хак, я считаю, что стоит сказать о его потенциальных недостатках. Прежде всего у него те же проблемы, что и у всех ДЕЙСТВУЙТЕ методов машинного обучения, когда они применяС ОСТОРОЖНОСТЬЮ! ются наивно для анализа причинно-следственных связей: смещение. Поскольку итоговая модель – ВОЗМОЖНО, ПОТРЕБУЕТСЯ БОЛЬШЕ ЭКОНОМЕТРИКИ! это регуляризованная модель машинного обучения, эта регуляризация может сместить оценку причинно-следственного эффекта к нулю. Вторая проблема связана с выбором алгоритма машинного обучения. Здесь мы выбрали градиентный бустинг деревьев решений. Деревья не очень хоро-
410  Несмещенное/ортогональное машинное обучение ши в плане генерации плавных прогнозов. В результате у нас могут возникнуть разрывы в кривой прогнозов. Вы можете увидеть это на графиках выше: мы видим ступенчатый характер кривой прогнозов. Кроме того, деревья не очень хороши с точки зрения экстраполяции, поэтому эта модель может выдавать странные прогнозы для цен, которые не встречались во время обучения. pred_test = (test .rename(columns={"price":"factual_price"}) .assign(jk = 1) .reset_index() .merge(pd.DataFrame(dict(jk=1, price=np.linspace(3, 30, 30))), on="jk") .drop(columns=["jk"])) t_res_test = pred_test[T] - ensamble_pred(pred_test, models_t, X) y_hat_test = ensamble_pred(pred_test, models_y, X) pred_test[f"{y}_pred"] = model_final.predict( X=pred_test[X].assign(**{T: t_res_test}) ) + y_hat_test np.random.seed(1) sample_ids = np.random.choice(pred_test["index"].unique(), 10) plt.figure(figsize=(10, 4)) sns.lineplot(data=(pred_test .query("index in @sample_ids") .assign(max_sales = lambda d: d.groupby("index")[[ "sales_pred"]].transform("max")) .assign(sales_pred = lambda d: d["sales_pred"] d["max_sales"] + d["sales_pred"].mean())), x="price", y="sales_pred", legend="full", hue="index"); Все это говорит о том, что данный метод в значительной степени зависит от итоговой модели машинного обучения. Если вы возьмете сильно регуляризованную модель, вы сместите оценку причинно-следственного эффекта к нулю. Если вы используете тот или иной метод машинного обучения, вы пе-
­ ­   ­ Ключевые идеи  411 редаете все его ограничения своим итоговым контрфактическим прогнозам. Однако если вы считаете, что этот подход стоит попробовать, не стесняйтесь! Просто не забывайте о недостатках, которые я здесь указал. Ключевые идеи Двойное/несмещенное/ортогональное машинное обучение – это способ, упрощающий оценку мешающих параметров, что позволяет сосредоточить внимание на интересующем нас каузальном параметре. Сначала применяется процедура двухшаговой ортогонализации: 1. Обучаем модель Mt(X), чтобы спрогнозировать воздействие по ковариатам X, и получаем остатки на основе прогнозов, полученных по тестовым блокам перекрестной проверки t̃ = t – Mt(X ). Мы назвали эту модель моделью, устраняющей смещение, потому что остатки по определению ортогональны к признакам, которые были использованы для их вычисления. 2. Обучаем модель My(X ), чтобы спрогнозировать результат по ковариа там X, и получаем остатки на основе прогнозов, полученных по тес товым блокам перекрестной проверки ỹ = y – My(X ). Мы назвали эту модель моделью, устраняющей шум, потому что остаток ỹ можно рассматривать как версию результата, в котором удалена вся дисперсия, обусловленная признаками. Получив остатки и выдвинув предположение об отсутствии ненаблюдаемых спутывающих факторов, мы можем регрессировать ỹ на t̃ для линейной аппроксимации ATE. Кроме того, мы можем включить взаимодействие t̃ с ковариатами для оценки CATE или применить трюк с весами, чтобы использовать любую модель машинного обучения в качестве итоговой модели CATE. Ортогонализация Построение причинно-следственных моделей Х T Y T̂ T̂ Mt(X) Mt(X) T T Х Х Х Х Y Y My(X) My(X) Ŷ Ŷ Х T-res Y-res M-Final(X) CATE
412  Несмещенное/ортогональное машинное обучение Наконец, я считаю, что этапы ортогонализации – это универсальный инструмент, позволяющий упростить обучение причинно-следственных моделей (каузальное обучение). В рамках этого подхода мы пытались передать остатки воздействия и остатки результата модели машинного обучения, работающей по типу S-модели. С ее помощью мы смогли получить контрфактические прогнозы для симулированных воздействий. Действительно, ортогональное машинное обучение является предварительной обработкой во многих задачах анализа причинно-следственных связей. Дополнительное чтение Материал, который я написал в этой главе, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю данный материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. При написании этой главы я руководствовался статьей Черножукова и его коллег «Double/Debiased Machine Learning for Treatment and Causal Parameters» (Chernozhukov et al., 2016): https://arxiv.org/abs/1608.00060, статьей Д. Фостера и В. Сиргканиса «Orthogonal Statistical Learning» (D. Foster and V. Syrgkanis, 2019): https://arxiv.org/abs/1901.09036, а также документацией по библиотеке econml: https://econml.azurewebsites.net. Ортогональное машинное обучение в последнее время привлекло к себе много внимания, поэтому есть много других источников по этой теме. К примеру, Ни и Уэйджер (Nie and Wager, 2020) подробно разобрали R-функцию потерь в статье «Quasi-Oracle Estimation of Heterogeneous Treatment Effects»: https://arxiv.org/pdf/1712.04912, Эти с коллегами (Athey et al, 2019) в статье «Estimating Treatment Effects with Causal Forests: An Application»: https://www.semanticscholar.org/reader/ee07ca60a0619f2f0cb72c7fc690c14a6598801e – разбирают ортогональное машинное обучение в контексте каузальных деревьев решений, и есть много последующих работ Черножукова, развивающих эту тему. Кроме того, я позаимствовал картинку из слайдов Педро Сант-Анны: https://pedrohcgs.github.io/files/Callaway_SantAnna_2020_slides.pdf.
Глава 23 Проблемы, связанные с гетерогенностью эффекта и нелинейностью Прогнозирование эффектов воздействия на уровне объектов крайне сложно из-за отсутствия фактической информации для сравнения. Поскольку мы наблюдаем только один потенциальный результат T(t), мы не можем оценить его напрямую. Вместо этого мы должны полагаться на преобразования зависимой переменной (которые еще можно рассматривать как искусно выведенные функции потерь), чтобы оценить математическое ожидание условных эффектов воздействия. Но это не единственная проблема. Поскольку эффекты воздействия варьируют, их оценки часто получаются довольно шумными. Это имеет огромные практические последствия для задач, в рамках которых мы хотим сегментировать объекты по эффекту воздействия, например когда мы хотим осуществить персонализированное распределение воздействия (персонализированное лечение). Сейчас мы увидим, что иногда можно получить более точную сегментацию эффектов воздействия, если не пытаться оценивать CATE напрямую, а вместо этого сфокусироваться на какой-то другой зависимой переменной-прокси, которая обычно имеет меньшую дисперсию. Общераспространенным случаем, когда так делают, является бинарная природа интересующей нас переменной Y. Эффекты воздействия для бинарного результата import pandas as pd import numpy as np import seaborn as sns
414  Проблемы, связанные с гетерогенностью эффекта и нелинейностью import statsmodels.formula.api as smf from matplotlib import pyplot as plt from matplotlib import style style.use("ggplot") from typing import List import numpy as np import pandas as pd from toolz import curry, partial @curry def avg_treatment_effect(df, treatment, outcome): return ( df.loc[df[treatment] == 1][outcome].mean() df.loc[df[treatment] == 0][outcome].mean() ) @curry def cumulative_effect_curve( df: pd.DataFrame, treatment: str, outcome: str, prediction: str, min_rows: int = 30, steps: int = 100, effect_fn = avg_treatment_effect) -> np.ndarray: size = df.shape[0] ordered_df = df.sort_values( prediction, ascending=False).reset_index(drop=True) n_rows = list(range(min_rows, size, size // steps)) + [size] return np.array( [effect_fn(ordered_df.head(rows), treatment, outcome) for rows in n_rows] ) @curry def cumulative_gain_curve( df: pd.DataFrame, treatment: str, outcome: str, prediction: str, min_rows: int = 30, steps: int = 100, effect_fn = avg_treatment_effect) -> np.ndarray: size = df.shape[0] n_rows = list(range(min_rows, size, size // steps)) + [size] cum_effect = cumulative_effect_curve(
Эффекты воздействия для бинарного результата  415 df=df, treatment=treatment, outcome=outcome, prediction=prediction, min_rows=min_rows, steps=steps, effect_fn=effect_fn ) return np.array( [effect * (rows / size) for rows, effect in zip(n_rows, cum_effect)] ) Приведем пример чрезвычайно распространенной задачи, с которой вы можете столкнуться, если работаете в технологической компании: руководство хочет увеличить конверсию клиентов по вашему продукту с помощью какого-то стимула. Например, оно хочет увеличить количество установок приложения, предлагая ваучер на 10 BRL для совершения покупок в приложении. Или предложить бесплатную поездку при первом использовании их приложения для райдшеринга. Или уменьшить комиссии при совершении транзакций в течение первых трех месяцев на своей инвестиционной платформе. Поскольку стимул часто бывает затратным, было бы замечательно не стимулировать всех. Было бы замечательно давать стимул ради увеличения конверсии только тем клиентам, которые наиболее чувствительны к этому стимулу. Если использовать язык анализа причинно-следственных связей, вы, вероятно, уже поняли, что этот тип бизнес-проблемы относится к категории гетерогенности эффекта воздействия (ГЭВ). Конкретно у вас есть дорогостоящий стимул в качестве воздействия T, конверсия в виде бинарного результата Y и конкретные клиентские признаки (характеристики) X, зарегистрированные перед воздействием. Затем вы можете оценить условный средний эффект воздействия E [Y1 – Y0 | X ] (или E [Y′(T ) | X ], если воздействие является непрерывным) в $, используя что-то вроде двойного/несмещенного машинного обучения, и в конечном итоге стимулировать только тех клиентов, которые демонстрируют наивысший оцененный эффект воздействия. Вы персонализируете свою стратегию конверсии. Вы находите сегмент клиентов с высокой инкрементальностью конверсии1 и стимулируете только их. Однако здесь есть одна сложность, которая делает наш подход к оценке гетерогенности эффекта воздействия не таким очевидным. Бинарность результата существенно усложняет вещи. Давайте я сначала покажу, что происходит, а затем объясню, почему это происходит. 1 Инкрементальность конверсии относится к изменению уровня конверсии в результате внесения изменений или внедрения новых стратегий в маркетинг, продвижение продукта или другие аспекты бизнеса. Это понятие используется для измерения того, насколько новые инициативы или изменения повышают эффективность конверсии в сравнении с базовым уровнем, который был до внесения изменений. – Прим. перев.
416  Проблемы, связанные с гетерогенностью эффекта и нелинейностью Симулируем данные Постараюсь сделать объяснение очень простым, но понятным. Мы будем симулировать воздействие nudge как полностью случайное, выбранное из распределения Бернулли с параметром p = 0.5. Это означает, что назначение воздействия nudge клиентам эквивалентно броску честной монеты. Кроме того, это означает, что нет спутывающих факторов, которые могли бы влиять на результат и требовали бы нашего внимания: nudge ∼ ℬ(0, 5). Затем симулируем ковариаты клиентов age и income в соответствии с гамма-распределением. Это информация о клиенте, которую вы знаете и хотите использовать для персонализации. Другими словами, вы хотите выделить группы клиентов, образованные возрастом и доходом, так чтобы одна группа была очень отзывчивой на воздействие nudge: age ∼ G(10, 4). income ∼ G(20, 2). Наконец, мы симулируем конверсию. Для этого мы создадим латентную переменную, которая будет соответствовать линейной модели с добавлением случайного шума. Важно отметить, что income является сильным предиктором Ylatent, но не модифицирует эффект воздействия. Проще говоря, nudge увеличивает Ylatent одинаково для всех уровней income. Переменная age, напротив, влияет на Ylatent только через свое взаимодействие с воздействием nudge: Ylatent ∼ N(–4.5 + 0.001income + nudge + 0.01nudge age, 1). Получив Ylatent, мы можем симулировать conversion, задав Ylatent > x. Для начала зададим x = 0, чтобы конверсия составляла примерно 50 %. То есть в среднем 50 % покупателей переходят на наш продукт. conversion = 1{Ylatent > 0}. np.random.seed(123) n = 100000 nudge = np.random.binomial(1, 0.5, n) age = np.random.gamma(10, 4, n) estimated_income = np.random.gamma(20, 2, n)*100 latent_outcome = np.random.normal( -4.5 + estimated_income*0.001 + nudge + nudge*age*0.01 ) conversion = (latent_outcome > .1).astype(int)
Симулируем данные  417 Кроме того, давайте сохраним все в датафрейме для удобства. Также убедимся, что средняя конверсия действительно близка к 50 %. df = pd.DataFrame(dict(conversion=conversion, nudge=nudge, age=age, estimated_income=estimated_income, latent_outcome=latent_outcome)) df.mean() conversion nudge age estimated_income latent_outcome dtype: float64 0.518260 0.500940 40.013487 3995.489527 0.197076 Поскольку воздействие было рандомизированным, мы можем оценить средний эффект воздействия как простую разницу средних значений для тестовой и контрольной групп E[Y | T = 1] – E[Y | T = 0]. Давайте посмотрим, как выглядят эти средние для латентной переменной результата и конверсии. Здесь есть нечто важное. df.groupby("nudge")[["latent_outcome", "conversion"]].mean() avg_treatment_effect(df, "nudge", "latent_outcome") 1.4023163965477752 avg_treatment_effect(df, "nudge", "conversion") 0.39477273768476406 ATE для латентного результата вычисляется довольно просто. Исходя из нашей модели генерации данных, мы знаем, что этот эффект должен быть равен 1 + avg(age)*0,01. И поскольку средний возраст составляет около 40 лет, это дает нам ATE около 1.4. Более интересным (и сложным) является вычисление ATE для конверсии. Поскольку конверсия ограничена диапазоном от 0 до 1, ATE для конверсии не будет линейным. Следовательно, мы не можем вывести его из простой формулы, как в случае с латентным результатом (формула есть, но она довольно сложная). Давайте просто скажем, что эффект будет меньше. И в этом есть смысл, верно? Я имею в виду, что воздействие не может увеличить конверсию на 1.4 пункта, просто потому что конверсия не
418  Проблемы, связанные с гетерогенностью эффекта и нелинейностью может превысить 100 %. Теперь я хочу, чтобы вы запомнили этот факт, потому что он будет ключевым для понимания того, что мы увидим далее. Давайте сейчас поговорим об условных средних эффектах воздействия (CATE). Исходя из нашей модели генерации данных, мы точно знаем, что estimated_income прогнозирует конверсию, но не модифицирует влияние (эффект) nudge на conversion. Следовательно, сегментация клиентов на основе estimated_income создаст сегменты с одинаковым эффектом воздействия. Переменная age, напротив, влияет на conversion только через свое взаимодействие с nudge. Таким образом, различные возрастные сегменты будут очень по-разному реагировать на воздействие, а различные сегменты с разным доходом – нет. Другими словами, estimated_income, вероятно, не будет хорошей переменной для персонализации, в то время как age, скорее всего, может быть такой переменной. Один из способов выяснить это – построить кривую накопленного эффекта. Кривая для переменной age должна начинаться далеко от ATE и медленно сходиться к нему, в то время как кривая для estimated_income должна колебаться вокруг значения ATE. Именно это мы и видим, когда строим кривую накопленного эффекта, показывающую влияние (эффект) nudge на латентный результат. cumulative_effect_fn = cumulative_effect_curve( df, "nudge", "latent_outcome", min_rows=500 ) age_cumm_effect_latent = cumulative_effect_fn(prediction="age") inc_cumm_effect_latent = cumulative_effect_fn( prediction="estimated_income" ) plt.plot(age_cumm_effect_latent, label="age") plt.plot(inc_cumm_effect_latent, label="est. income") plt.legend() plt.xlabel("Процентиль") plt.ylabel("Эффект (влияние) на латентный результат");
Симулируем данные  419 Снова латентный результат выглядит очень хорошо. В силу своей линейности наши математические ожидания достаточно точно соответствуют реальности. Но в реальной жизни нас не интересует латентный результат (и у нас его нет). У нас есть только конверсия. В случае с конверсией все выглядит намного сложнее. Если мы построим кривые накопленного эффекта, age все еще будет демонстрировать некоторую гетерогенность эффекта воздействия, стартуя со значения выше ATE и медленно сходясь к нему. Это означает, что чем выше age, тем выше эффект воздействия. Пока все хорошо. Это вполне ожидаемо. cumulative_effect_fn = cumulative_effect_curve( df, "nudge", "conversion", min_rows=500 ) age_cumm_effect_latent = cumulative_effect_fn(prediction="age") inc_cumm_effect_latent = cumulative_effect_fn( prediction="estimated_income" ) plt.plot(age_cumm_effect_latent, label="age") plt.plot(inc_cumm_effect_latent, label="est. income") plt.legend() plt.xlabel("Процентиль") plt.ylabel("Эффект (влияние) на конверсию"); Однако мы дополнительно наблюдаем БОЛЬШУЮ гетерогенность эффекта воздействия в зависимости от estimated_income. У клиентов с более высоким значением estimated_income эффект воздействия гораздо ниже, это приводит к тому, что кривая накопленного эффекта поначалу стремится к нулю, а затем медленно сходится к ATE. Это говорит нам о том, что в плане персонализации estimated_income создаст сегменты с большей гетерогенностью эффекта воздействия (ГЭВ) по сравнению с сегментами, которые мы получили бы с помощью age.
420  Проблемы, связанные с гетерогенностью эффекта и нелинейностью Это неудобно, верно? Почему признак, который, как мы знаем, определяет гетерогенность эффекта, age, оказывается хуже для персонализации по сравнению с признаком (estimated_income), который, как мы знаем, не модифицирует эффект воздействия? Ответ кроется в нелинейности функции результата. Хотя estimated_income не модифицирует эффект воздействия nudge на латентный результат, он изменяет его, когда мы трансформируем этот латентный результат в конверсию (по крайней мере косвенно). Конверсия не является линейной. Это означает, что ее производная меняется в зависимости от того, где вы находитесь. Поскольку конверсия может достигнуть только 1, если она уже очень высокая, ее трудно увеличить. Другими словами, производная высокой конверсии будет очень маленькой. Но поскольку конверсия ограничена нулем, у нее также будет маленькая производная, если она уже является очень низкой. Конверсия имеет форму S, с маленькими значениями производных на обоих концах. Это можно увидеть, построив среднюю конверсию по бинам переменной estimated_income. (df .assign(estimated_income_bins=(df["estimated_income"]/100).astype(int)*100) .groupby("estimated_income_bins") [["conversion"]] .mean() .plot() ); Обратите внимание, как наклон (производная) этой кривой становится очень маленьким при очень высокой и очень низкой конверсии (хотя это трудно увидеть из-за небольшого количества наблюдений в этом диапазоне). Исходя из данной информации, мы теперь можем объяснить, почему estimated_income создает сегменты с высокой гетерогенностью эффекта воздействия.
Симулируем данные  421 Поскольку estimated_income является сильным предиктором конверсии, можно сказать, что клиенты с разными значениями estimated_income оказываются в разных точках S-образной кривой конверсии. Клиенты с очень высоким или очень низким значением estimated_income оказываются на краях кривой, где производная меньше, это означает, что увеличение конверсии здесь дается труднее, а это, в свою очередь, означает, что эффект воздействия, вероятно, будет невелик. С другой стороны, клиенты с доходом, находящимся посередине диапазона, также оказываются посередине кривой конверсии, где производная выше, и, следовательно, эффект воздействия, вероятно, также будет выше. Говорю «вероятно», потому что теоретически возможно, что переменная может обладать настолько высокой силой модификации эффекта, что будет доминировать над изменением производной, которое мы наблюдаем по мере прохождения кривой конверсии. Однако, по крайней мере по моему опыту, изгиб S-образной кривой конверсии обычно доминирует над всеми другими модификациями эффекта. Однако это не только мое мнение. Вот слайд из презентации Сьюзан Эти для Колумбийского института Data Science. Здесь она обсуждает эффект подталкивания студентов к подаче заявки на федеральную финансовую помощь для оплаты учебы. Это также задача оценки конверсии. Она пришла к выводу, что лучшая стратегия – нацелиться на студентов, которые склонны обратиться за помощью (т. е. на студентов с высокой вероятностью конверсии). Кроме того, по ее словам, нецелесообразно нацеливаться на тех, у кого низкая вероятность конверсии. Подождите минуту! Но это не то, что было сказано нами ранее! Мы сказали, что и для очень высокой, и для очень низкой конверсии будет маленькое значение производной и, следовательно, маленький эффект воздействия!
422  Проблемы, связанные с гетерогенностью эффекта и нелинейностью Ну, это правда. Однако в реальной жизни конверсия редко охватывает всю S-образную кривую. Обычно у нас все оказываются на одном из двух концов кривой. В бизнес-терминах средняя конверсия редко составляет 50 %. Намного чаще это что-то вроде 70–90 % или 1–20 %. В таких более вероятных ситуациях упор на тех, кто демонстрирует высокий начальный уровень конверсии, может быть как хорошей, так и плохой идеей. Вот что я имею в виду: давайте возьмем тот же самый латентный результат, что и ранее, но теперь сгенерируем ситуацию, в которой конверсия в среднем будет низкой, задав latent_outcome > 2. Затем создадим ситуацию, в которой конверсия будет высокой, задав latent_outcome > -2. df["conversion_low"] = conversion = (latent_outcome > 2).astype(int) df["conversion_high"] = conversion = (latent_outcome > -2).astype(int) print("Avg. Low Conversion: ", df["conversion_low"].mean()) print("Avg. High Conversion: ", df["conversion_high"].mean()) Avg. Low Conversion: 0.12119 Avg. High Conversion: 0.9275 Основываясь на наших знаниях о нелинейности конверсии, мы уже можем предсказать, что произойдет. В ситуации с низкой конверсией нацеливание на клиентов с высоким базовым уровнем конверсии1 (с высоким значением estimated_income) будет гораздо более эффективным. Это обусловлено тем, что мы находимся в левой части S-образной кривой конверсии, где производная будет меньше при более низком базовом уровне конверсии. В этом диапазоне высокий базовый уровень конверсии будет означать высокий эффект воздействия. Следовательно, мы должны стимулировать тех, у кого высокий базовый уровень конверсии, то есть тех, у кого выше estimated_income. cumulative_effect_fn = cumulative_effect_curve( df, "nudge", "conversion_low", min_rows=500 ) age_cumm_effect_latent = cumulative_effect_fn(prediction="age") inc_cumm_effect_latent = cumulative_effect_fn( prediction="estimated_income" ) plt.plot(age_cumm_effect_latent, label="age") plt.plot(inc_cumm_effect_latent, label="est. income") plt.xlabel("Процентиль") plt.ylabel("Эффект (влияние) на конверсию"); plt.legend(); 1 Базовый уровень конверсии, или базовая конверсия, – это конверсия до воздействия. – Прим. перев.
Симулируем данные  423 Как мы и прогнозировали, у клиентов с высоким значением estimated_income, что соответствует высокому базовому уровню конверсии, наблюдается гораздо более высокий эффект воздействия. Теперь возьмем другую ситуацию, когда конверсия в среднем является высокой: у клиентов с высоким базовым уровнем конверсии будет более низкий эффект воздействия. Поэтому не рекомендуется нацеливаться на клиентов с высоким значением estimated_income. Мы видим это по инвертированной кривой накопленного эффекта, которая показывает, что у клиентов с высоким значением estimated_income – более низкий эффект воздействия. cumulative_effect_fn = cumulative_effect_curve( df, "nudge", "conversion_high", min_rows=500 ) age_cumm_effect_latent = cumulative_effect_fn(prediction="age") inc_cumm_effect_latent = cumulative_effect_fn( prediction="estimated_income" ) plt.plot(age_cumm_effect_latent, label="age") plt.plot(inc_cumm_effect_latent, label="est. income") plt.xlabel("Процентиль") plt.ylabel("Эффект (влияние) на конверсию") plt.legend();
424  Проблемы, связанные с гетерогенностью эффекта и нелинейностью В заключение можно сказать, что при бинарном результате эффект воздействия обычно подчиняется производной (изгибу) логистической функции. Например, применительно к нашей задаче конверсии, если средняя конверсия является низкой, мы находимся в левой части логистической кривой и эффект воздействия будет выше при высокой базовой конверсии. Это можно интерпретировать как стратегию «стимуляции» тех клиентов, у которых уже высокая вероятность конверсии. С другой стороны, если средняя конверсия является высокой, мы находимся в правой части логистической кривой, где производная (и, следовательно, эффект воздействия) будет выше для клиентов с более низкой базовой конверсией.
Непрерывное воздействие и нелинейность  425 Это, конечно, слишком большой объем для запоминания, но мы можем упростить: просто подвергайте воздействию тех, кто ближе к базовой конверсии 50 %. Математическое обоснование здесь довольно прочное: производная логистической функции достигает пика при 50 %, поэтому просто подвергайте воздействию те объекты, которые ближе к этой точке. Еще более замечательный момент заключается в том, что это один из тех редких случаев, когда здравый смысл соответствует математике. В маркетинге, где подобные проблемы с конверсией встречаются часто, существует убеждение, что мы не должны нацеливаться на проигранные ставки (тех, у кого очень низкая вероятность конверсии) или надежные выигрыши (тех, у кого очень высокая вероятность конверсии). Вместо этого следует нацеливаться на тех, кто находится посередине. Это довольно интересно, поскольку именно это мы и выяснили, используя более формальные аргументы причинно-следственного анализа. Непрерывное воздействие и нелинейность Мы подробно рассмотрели лишь один пример гетерогенного эффекта воздействия для бинарного результата, что делает анализ сложнее. Но это явление выходит за пределы проблемы конверсии в маркетинге. Например, в 2021 году мир получил первую партию одобренных вакцин против COVID-19 в широкий доступ. Тогда вопрос о том, кто должен первым получить вакцину, был крайне важным. И это, как неудивительно, вновь проблема гетерогенного эффекта воздействия. Разработчики хотели бы вакцинировать тех, кто извлечет из этого наибольшую пользу. В данной ситуации эффект воздействия – это предотвращение смерти или госпитализации. Итак, чья смерть или госпитализация уменьшилась наиболее значительно после вакцинации? В большинстве стран это были пожилые люди и те, у кого в прошлом были проблемы со здоровьем (сопутствующие заболевания). Именно эти люди с большей вероятностью умирают, заразившись COVID-19. Кроме того, смертность от COVID значительно ниже 50 %, что помещает нас в левую часть логистической функции. В этом диапазоне, в силу тех же самых аргументов, которые мы использовали для маркетинга, целесообразно лечить тех, у кого высокая базовая вероятность смерти от COVID-19, а это как раз те группы, о которых мы говорили ранее. Это совпадение? Может быть. Имейте в виду, я не являюсь экспертом в области здравоохранения, так что могу сильно ошибаться. Но для меня важна логика. Что в случае маркетингового стимулирования, что в случае с вакцинами против COVID-19, ключевым фактором, осложняющим гетерогенность эффекта воздействия, является нелинейность функции результата Y(0). Эта нелинейность приводит к тому, что при переходе от Y(0) к Y(1) увеличение результата в основном обусловлено изгибом функции результата. Мы
426  Проблемы, связанные с гетерогенностью эффекта и нелинейностью видели, как это происходит в случае бинарного результата, где E[Y | X] подчиняется логистической функции. Но это еще более распространенная проб­ лема. На самом деле это проблема, которая постоянно возникает в бизнесе, особенно если воздействие является непрерывной переменной. Давайте рассмотрим последний пример, чтобы сделать этот тезис еще более понятным. Разберем классическую проблему ценообразования. Вы работаете в стриминговой компании типа Netflix или HBO. Ключевой вопрос, который компания хочет решить: какую плату взимать с клиентов. Чтобы ответить на этот вопрос, они проводят эксперимент, случайным образом назначая клиентам различные ценовые предложения: 5 BRL/месяц, 10 BRL/месяц, 15 BRL/месяц или 20 BRL/месяц. Таким образом они надеются ответить не только на вопрос о том, насколько клиенты чувствительны к увеличению цены, но и на вопрос о том, являются ли некоторые типы клиентов более чувствительными, чем другие. На графике ниже вы можете увидеть результаты этого эксперимента, разбитые на две группы клиентов: A, клиенты с более высоким предполагаемым доходом, и B, клиенты с более низким предполагаемым доходом. data = pd.DataFrame(dict( segment=["b", "b", "b", "b", "a", "a", "a", "a"], price=[5, 10, 15, 20] * 2, sales=[5100, 5000, 4500, 3000, 5350, 5300, 5000, 4500] )) plt.figure(figsize=(8,4)) sns.lineplot(data=data, x="price", y="sales", hue="segment") plt.title("Уср. продажи в зависимости от цены (%) и сегмента"); Получив такие данные, компания хочет ответить на следующий вопрос: кто более чувствителен к скидкам? Другими словами, как мы можем ранжировать клиентов по их чувствительности к цене (эластичности продаж по цене)? При визуальном анализе кривых у нас складывается ощущение, что сегмент A в целом менее чувствителен к скидкам, несмотря на то что он
Непрерывное воздействие и нелинейность  427 генерирует больший доход. Однако мы также видим, что присутствует некоторая кривизна. На самом деле если мы учтем эту кривизну, ранжирование эффекта воздействия уже не ограничится только клиентами из сегментов A и B. Эффект воздействия также будет зависеть от того, где именно эти клиенты находятся на кривой воздействия. Например, эффект воздействия при переходе от 15 BRL к 10 BRL у клиентов сегмента A (обозначен синим овалом) выше, чем эффект воздействия при переходе от 5 BRL к 10 BRL у клиентов сегмента B (обозначен коричневым овалом): E[Y(10) – Y(5) | Seg = B] < E[Y(15) – Y(10) | Seg = A]. Если бы мы упорядочили полученные эффекты воздействия для этого эксперимента, график выглядел бы примерно так: plt.figure(figsize=(8,4)) sns.lineplot(data=data, x="price", y="sales", hue="segment") plt.annotate("1", plt.annotate("2", plt.annotate("3", plt.annotate("4", plt.annotate("4", plt.annotate("5", (8, 5350), bbox=dict(boxstyle="round", fc="1")) (8, 5000), bbox=dict(boxstyle="round", fc="1")) (13, 5100), bbox=dict(boxstyle="round", fc="1")) (13, 4700), bbox=dict(boxstyle="round", fc="1")) (17, 4800), bbox=dict(boxstyle="round", fc="1")) (17, 3900), bbox=dict(boxstyle="round", fc="1")) plt.title("Упорядочивание эффекта увеличения цены"); Как и в случае, когда результат был бинарным, в данном примере эффект воздействия коррелирует с результатом. Чем выше продажи (ниже цена), тем ниже абсолютный эффект воздействия. Чем ниже продажи (выше цена), тем выше абсолютный эффект воздействия. Однако в данном случае ситуация еще более сложная, поскольку эффект коррелирует не только с результатом, но и с уровнем воздействия. Это делает ответы на контрфактические вопросы более сложными. Например, представьте себе, что ваши экспериментальные данные на самом деле выглядят как на графике ниже,
428  Проблемы, связанные с гетерогенностью эффекта и нелинейностью где вы тестируете более высокие цены для сегмента A (богатого населения), а более низкие цены только для сегмента B. Это довольно распространенное явление, так как фирмы часто хотят экспериментировать с тем воздействием, которое, как им кажется, является более разумным. data = pd.DataFrame(dict( segment=["b", "b", "b", "b", "a", "a", "a", "a"], price=[5, 10, 15, 20] * 2, sales=[5100, 5000, 4500, 3000, 5350, 5300, 5000, 4500] )) plt.figure(figsize=(8,4)) sns.lineplot(data=data.loc[lambda d: (d["segment"] == "a") | (d["price"] < 12)], x="price", y="sales", hue="segment") plt.title("Уср. продажи в зависимости от цены (%) и сегмента"); Теперь, если вы выполните наивное агрегирование результатов эффекта воздействия, может показаться, что сегмент A более эластичен к увеличению цен (имеет более высокий абсолютный эффект воздействия), чем сегмент B. Но это происходит только потому, что для сегмента B вы исследовали область низкого эффекта воздействия. Что же делать, когда эффекты воздействия меняются в зависимости от того, где вы находитесь, с точки зрения воздействия и результата? Если честно, это все еще активная область исследований. Практически лучшее, что вы можете сделать, – быть очень осторожным, пытаясь ответить, какой тип клиента более чувствителен к воздействию. Убедитесь, что все сравниваемые типы клиентов имеют одинаковое распределение воздействия. Если это не так, будьте очень скептичными к экстраполяции эффекта воздействия. Например, в вышеприведенном примере, даже если клиент из сегмента B кажется менее чувствительным к увеличению цен, вы не знаете, будет ли это так, если назначите для этого сегмента более высокие цены, чем 10BRL. Еще одна вещь, которую можно попробовать, – линеаризовать кривую отклика. Идея здесь заключается в том, чтобы избавиться от кривизны, преоб-
Ключевые идеи  429 разуя воздействие или результат (или и то, и другое) так, чтобы зависимость между ними выглядела как прямая линия. Поскольку прямая линия имеет постоянную производную, это позволит избежать проблемы изменения эффекта воздействия в зависимости от того, в какой точке кривой вы находитесь. К примеру, если мы возьмем нашу переменную price и преобразуем ее, сделав отрицательной, возведя в степень 4 и изменив знак, мы получим вполне линейную (или почти линейную) зависимость. После такого преобразования данных утверждение о том, что A менее чувствителен к увеличению цен, чем B, становится более логичным, поскольку оно теперь не зависит от того, в какой точке кривой мы находимся. plt.figure(figsize=(8,4)) sns.lineplot(data=data.assign(price = lambda d: -1*(-d["price"]**4)), x="price", y="sales", hue="segment") plt.title("Уср. продажи в зависимости от -(-цены^4)"); Однако у этого подхода есть множество недостатков. Во-первых, не всегда возможно линеаризовать кривую. В нашем примере вы явно видите, что такая линеаризация далека от идеала. Но что более важно, иногда нет смысла избавляться от кривизны с точки зрения бизнеса. В нашем примере с ценообразованием может быть вполне целесообразно считать клиента A при цене 15 более чувствительным, чем клиента B при цене 5. Это приведет нас к разумному решению снизить цену для клиента A с 15 до 10, но при этом не трогать цену для клиента B. Ключевые идеи Я понимаю, что, возможно, я принес больше вопросов, чем ответов, но иногда лучший способ разобраться в проблеме – это быть очень внимательным к ней. Надеюсь, что в этой главе я смог привлечь ваше внимание
430  Проблемы, связанные с гетерогенностью эффекта и нелинейностью к сложностям, возникающим при работе с важными для нас нелинейными результатами. Мы разобрали более изученную проблему бинарных результатов. В этом случае эффект воздействия обычно выше, когда мы находимся ближе к среднему результату 0.5. Поскольку результат ограничен значениями 0 и 1, эффекты обычно очень малы, если мы находимся слишком близко к 0 или 1. Сложнее обстоит дело с нелинейностями, возникающими в случае непрерывных результатов. Здесь лучшее, что вы можете сделать, – очень внимательно проанализировать проблему. Постарайтесь ответить, важен ли вам эффект воздействия независимо от базового уровня воздействия или же для вас имеет значение этот базовый уровень. Уже одно это будет ценным руководством к действию. Дополнительное чтение Большинство из сказанного здесь основано на моем собственном опыте работы с этой проблемой. Однако я нашел одну академическую статью, затрагивающую эту тему, – «Causal Classification: Treatment Effect Estimation vs. Outcome Prediction» (https://jmlr.org/papers/volume23/19-480/19-480.pdf), авторы Фернандез-Лория (Fernandez-Loria) и Провост (Provost) обсуждают случай, когда эффект воздействия коррелирует с переменной результата.
Глава 24 Сага о разности разностей Обсудив гетерогенность эффекта воздействия, мы теперь переключим передачу и вернемся к среднему эффекту воздействия. В следующих нескольких главах мы рассмотрим некоторые последние достижения в области методов исследования панельных данных. Панель – это структура данных, в которой наблюдения повторяются во времени. Тот факт, что мы наблюдаем за одним и тем же объектом в течение нескольких временных периодов, позволяет нам увидеть, что происходит до и после осуществления воздействия. Это делает панельные данные перспективной альтернативной структурой данных, которая позволяет выявить причинно-следственные эффекты в тех случаях, когда рандомизация невозможна. Чтобы подчеркнуть актуальность использования панельных данных, мы в основном будем говорить о применении анализа причинно-следственных связей в маркетинге. Маркетинг особенно интересен из-за пресловутых трудностей, возникающих при проведении рандомизированных экспериментов. В маркетинге мы часто не можем контролировать, кто подвергается воздействию, то есть кто видит нашу рекламу. Когда новый пользователь заходит на наш сайт или скачивает наше приложение, мы не можем точно знать, пришел ли он потому, что увидел одну из наших рекламных кампаний, или по какой-то другой причине.
432  Сага о разности разностей (Для тех, кто лучше знаком с маркетинговой атрибуцией1, я знаю о множест­ ве инструментов атрибуции, которые направлены на решение этой проб­ лемы. Но я также знаю об их многочисленных ограничениях.) В офлайн-маркетинге проблема еще больше. Как узнать, принесла ли телевизионная кампания прибыль, превышающую ее стоимость? Поэтому в маркетинге часто практикуются геоэксперименты: мы проводим маркетинговую кампанию в одном географическом регионе, но не в остальных, и сравниваем их. В этом случае особенно интересны методы панельных данных: мы можем собирать данные по всему географическому региону (объекту) в течение нескольких периодов времени. Для осмысления такого рода данных и выявления причинно-следственного эффекта, пожалуй, самыми популярными методами являются методы разности разностей (Diff-in-Diff – DiD). 2020 и 2021 годы не были легкими для большинства из нас. Но особенно тяжелыми они были для DiD. Множество недавних исследований выявили серьезные недостатки этих методов, о которых раньше не было известно. Поэтому хотя в первой части книги уже есть глава, посвященная методу разности разностей, ее содержание носит скорее ознакомительный характер. Она не охватывает новые достижения и бурные дискуссии вокруг методов исследования панельных данных. Теперь мы должны рассмотреть их более подробно, начав с метода разности разностей. В этой отдельной главе я постараюсь крат­ ко описать недавно обнаруженные недостатки метода разности разностей, а также показать, как их решить. Эта глава состоит из трех разделов: 1. Рождение. Здесь мы напомним, почему панельные данные так привлекательны для анализа причинно-следственных связей и как метод разности разностей (DiD) и модели с временными и индивидуальными фиксированными эффектами (Two Way Fixed Effect – TWFE) используют временную структуру в свою пользу. 1 Модель атрибуции – это способ определения того, какой канал маркетинга или какой этап взаимодействия с пользователем привел к желаемому результату (например, покупке, регистрации или заявке). Модель атрибуции используется в маркетинге для оценки эффективности различных каналов и кампаний, определения ROI (возврата инвестиций) и принятия решений о распределении бюджета. Существует несколько различных моделей атрибуции, которые могут быть использованы в зависимости от бизнес-целей, бюджета, типа каналов маркетинга и других факторов. Некоторые из наиболее распространенных моделей атрибуции следующие. Последнее касание (Last touch) – вся заслуга за конверсию приписывается последнему каналу, который привел пользователя на сайт перед покупкой или другим желаемым действием. Первое касание (First touch) – вся заслуга за конверсию приписывается первому каналу, который привел пользователя на сайт. Линейная (Linear) – вся заслуга за конверсию равномерно распределяется между всеми каналами, которые привлекли пользователя на сайт. По позициям (Position based) – заслуга за конверсию распределяется между каналами, которые привлекли пользователя на сайт, с учетом того, какие каналы находятся в начале и в конце взаимодействия и какой канал был наиболее эффективен. Алгоритмические (Datadriven) – модель, основанная на данных и аналитике, которая учитывает все каналы и взаимодействия пользователя на сайте перед конверсией. Она использует алгоритмы и методы машинного обучения для определения важности каждого канала в конверсии. – Прим. перев.
1. Рождение: многообещающие панельные данные  433 2. Смерть. Мы ознакомимся с одним ключевым предположением, которое подразумевается в моделях DiD и TWFE и на которое мало кто обращает внимание. Здесь мы должны понять, когда и как это предположение может дать сбой. 3. Просветление. Поняв проблему DiD и TWFE, мы теперь можем подумать о ее решении. В этой части мы покажем простой обходной путь к решению проблемы, рассмотренной в пункте 2. Давайте приступим! 1. Рождение: многообещающие панельные данные КАК ВСЕ НАЧАЛОСЬ КАК ВСЕ ИДЕТ Как я уже говорил, панельные данные – это когда у нас есть несколько объектов i, за которыми мы наблюдаем в течение нескольких периодов времени t. Допустим, мы хотим оценить эффективность той или иной политики в США, например вы хотите проверить влияние легализации каннабиса на уровень преступности. У вас есть данные об уровне преступности в нескольких штатах i за несколько периодов времени t. Кроме того, вы фиксируете, в какой момент времени каждый штат принимает законодательство в пользу легализации каннабиса. Надеюсь, вы понимаете, почему это невероятно мощный инструмент для анализа причинно-следственных связей. Пусть легализация каннабиса будет воздействием D (поскольку T уже занято, оно обозначает время). Мы можем проследить тенденцию изменения уровня преступности в конкретном штате, который в итоге получает воздействие (т. е. в нем легализуют каннабис), и посмотреть, происходят ли какие-либо нарушения тренда в момент воздействия. В некотором смысле в сравнении «до и после» штат служит контрольным объектом по отношению к самому себе. Кроме того,
­ 434  Сага о разности разностей поскольку у нас есть несколько штатов, мы также можем сравнить штаты, подвергнутые воздействию, с контрольными штатами. Когда мы объединяем оба сравнения – сравнение тестовой группы с контрольной группой, сравнение периода до воздействия с периодом после воздействия, – мы получаем невероятно мощный инструмент для получения контрфактических данных и, следовательно, оценки причинно-следственных эффектов. Методы исследования панельных данных часто используются для оценки эффективности государственной политики, но мы можем легко аргументировать, почему они также невероятно полезны для (технологической) индустрии. Компании часто отслеживают данные о пользователях в течение нескольких периодов времени, что приводит к получению сложной структуры панельных данных. Мало того, иногда эксперименты невозможны, поэтому приходится полагаться на другие стратегии идентификации. Чтобы подробнее рассмотреть этот подход, давайте разберем гипотетический пример молодой технологической компании, которая отслеживает количество людей, установивших ее приложение в нескольких городах. В какой-то момент в 2021 году компания запустила новую функциональную возможность в своем приложении. Теперь она хочет знать, сколько новых пользователей эта функциональная возможность принесла компании. Реа лизация этой возможности была постепенной. Некоторые города получили эту функциональную возможность в 2021-06-01. Другие – в 2021-07-15. Полная реализация функциональной возможности в остальных городах произойдет только в 2022 году. Поскольку наши данные охватывают период до 2021-07-31, эту последнюю группу можно считать контрольной. С точки зрения анализа причинно-следственных связей реализацию этой функциональной возможности можно рассматривать как воздействие, а количество установок приложения – как результат. Мы хотим оценить эффект воздействия на результат, то есть влияние новой функциональной возможности на количество установок. Обратите внимание, что технологическая компания не может провести эксперимент. Она не может контролировать, кто из пользователей узнает о появлении новой функциональной возможности. Мы говорим, что у компании – ограниченный контроль над назначением воздействия. Это обусловлено тем, что объектами анализа являются люди, которые еще не являются клиентами компании. Компания хочет выяснить, сколько людей станут ее клиентами, установив ее приложение. Конечно, она не может рандомизировать воздействие на этих людей. Поэтому компания в качестве объектов анализа выбрала города. Релиз в одном городе и отсутствие релиза в другом – это то, что сотрудники компании могут проконтролировать, чего нельзя сказать о релизе для одного человека и отсутствии релиза для другого человека. from toolz import * import pandas as pd import numpy as np from scipy.special import expit
1. Рождение: многообещающие панельные данные  435 from linearmodels.panel import PanelOLS import statsmodels.formula.api as smf import seaborn as sns from matplotlib import pyplot as plt from matplotlib import style style.use("ggplot") date = pd.date_range("2021-05-01", "2021-07-31", freq="D") cohorts = pd.to_datetime( ["2021-06-01", "2021-07-15", "2022-01-01"]).date units = range(1, 100+1) np.random.seed(1) df = pd.DataFrame(dict( date = np.tile(date, len(units)), unit = np.repeat(units, len(date)), cohort = np.repeat( np.random.choice(cohorts, len(units)), len(date) ), unit_fe = np.repeat( np.random.normal(0, 5, size=len(units)), len(date) ), time_fe = np.tile(np.random.normal(size=len(date)), len(units)), week_day = np.tile(date.weekday, len(units)), w_seas = np.tile(abs(5-date.weekday) % 7, len(units)) )).assign( trend = lambda d: (d["date"] - d["date"].min()).dt.days/70, day = lambda d: (d["date"] - d["date"].min()).dt.days, treat = lambda d: (d["date"] >= d["cohort"]).astype(int) ).assign( y0 = lambda d: 10 + d["trend"] + d["unit_fe"] + 0.1*d["time_fe"] + d["w_seas"]/10 ).assign( y1 = lambda d: d["y0"] + 1 ).assign( tau = lambda d: d["y1"] - d["y0"], installs = lambda d: np.where(d["treat"] == 1, d["y1"], d["y0"]) plt.figure(figsize=(10,4)) [plt.vlines(x=cohort, ymin=9, ymax=15, color=color, ls="dashed") for color, cohort in zip(["C0", "C1"], cohorts[:-1])] sns.lineplot( data=(df .groupby(["cohort", "date"])["installs"] .mean() .reset_index()), x="date", y="installs", hue="cohort" );
436  Сага о разности разностей Пунктирными линиями отмечены моменты, когда когорта получила воздействие (функциональная возможность была реализована). Обратите внимание на большой объем данных, представленных на графике выше. Во-первых, мы видим, что у каждой когорты – свое базовое количество установок приложения (количество установок приложения до воздействия). Это объясняется тем, что в разных городах разная численность населения, что приводит к большему или меньшему количеству установок в зависимости от размера города. Например, похоже, что города первой когорты (получившие воздействие в 06/01) имеют более высокое базовое количество установок по сравнению с городами в других когортах. Кроме того, похоже, что контрольная когорта имеет более низкое базовое количество установок. Это означает, что простое сравнение тестовых когорт с контрольной даст смещенный результат, поскольку Y0 для контрольной группы ниже, чем Y0 для тестовой группы, или Y0 | G = Контрольная < Y0 | G = Тестовая, где G – это когорта. К счастью, эта проблема поправима. Панельные данные позволяют сравнивать города и время, что позволяет учитывать разные базовые уровни (в данном случае – разное базовое количество установок приложения). Говоря о времени, обратите внимание на общий восходящий тренд с некоторыми колебаниями (что похоже на недельную сезонность). Если сфокусироваться на контрольной когорте, то ежедневное количество установок увеличились с примерно 10 в мае до примерно 11 в июне, т. е. примерно на 1. С технической точки зрения, в последние временные периоды наблюдается более высокий уровень Y0, чем в ранние периоды. Это означает, что простое сравнение одних и тех же городов в разные периоды времени также даст смещенные результаты. И снова нам повезло, что структура панельных данных позволяет сравнивать количество установок не только по времени, но и по городам с поправкой на тренды. В идеале, чтобы сделать вывод о том, как реализация новой функциональной возможности повлияла на количество установок, нам нужно знать, что случилось бы с когортами, получившими функциональную возможность, если бы они ее не получили. Мы хотим оценить контрфактический резуль-
1. Рождение: многообещающие панельные данные  437 тат Y(0) для тестовых когорт в периоды после воздействия. Если мы для каждой когорты добавим момент времени, когда она получила воздействие g (вспомним, что когорта – это просто группа городов, которые получили воздействие в один и тот же момент времени), мы можем записать этот контрфактический результат как E[Y0 | t ≥ g], что позволит нам определить эффект воздействия на объекты, подвергнутые воздействию (ATT), из когорты g следующим образом: E[Y1 | t ≥ g] – E[Y0 | t ≥ g]. Следующий вопрос: как мы можем оценить этот эффект на основе имеющихся у нас данных. Один из способов – использовать возможности структуры панельных данных и оценить эти контрфактические данные. Например, мы можем воспользоваться линейной регрессией и методом разности разностей, чтобы получить модель с временными и индивидуальными фиксированными эффектами. Допустим, в каждом городе i есть базовое количест­ во установок приложения γi . Это связано с тем, о чем мы говорили ранее: возможно, в городе чаще устанавливают это приложение, потому что в нем более высокая численность населения или потому что его культура потребления больше соответствует продукту нашей технологической компании. Независимо от причины и даже если мы не знаем эти причины, мы говорим, что эти индивидуальные особенности объектов можно представить с помощью индивидуального фиксированного параметра или фиксированного параметра объекта (unit fixed parameter) γi . Аналогично мы можем сказать, что каждый период времени t характеризуется базовым количеством установок приложения, который мы можем отразить с помощью временного фиксированного параметра или фиксированного параметра времени (time fixed parameter) θt. Если все верно, можно сказать, что количество установок приложения зависит от эффекта города (объекта) γ и эффекта периода времени θ плюс некоторый случайный шум: Installsit = γi + θt + eit. Чтобы включить воздействие в эту формулу, задаем переменную D it, которая равна 1, если объект подвергся воздействию, и нулю в противном случае. В нашем примере эта переменная всегда будет нулевой для когорты, никогда не подвергавшейся воздействию. Кроме того, поначалу эта переменная будет равна нулю для всех остальных когорт, однако в точке 06/01 она превратится в 1 для когорты, подвергнутой воздействию 06/01 (1 июня), и после этого останется равной 1. Кроме того, переменная D превратится в 1 в точке 07/15 для когорты, подвергнутой воздействию 07/15 (15 июля). Мы можем включить эти индикаторы воздействия в нашу модель следующим образом: Installsit = τD it + γi + θt + eit. Оценка вышеприведенной модели с помощью OLS называется моделью с временными и индивидуальными фиксированными эффектами (Two Way
438  Сага о разности разностей Fixed Effect – TWFE). Обратите внимание, что τ – это эффект воздействия, поскольку он показывает, насколько изменилось количество установок приложения, после того как объекты подверглись воздействию. Можно еще сказать, что мы пользуемся свойством линейной регрессии сохранять значения предикторов фиксированными. Если мы оценим вышеприведенную модель, то получим оценку τ как изменение количества установок, если переключим воздействие с 0 на 1, зафиксировав объект i и период времени t. Обратите внимание, как смело это звучит! Сказать, что мы фиксируем каждый объект, наблюдая, как D изменяет результат, – это равносильно сказать, что мы контролируем все конкретные характеристики объекта, которые неизменны во времени, без разницы, известны они нам или неизвестны. Например, мы будем контролировать базовое количество установок приложения в городах, которое мы можем измерить, а также то, о чем мы понятия не имеем, например степень соответствия культуры потребления в данном городе нашему продукту. Единственное требование – чтобы эта характеристика оставалась неизменной в течение всего временного периода, охватываемого анализом. Более того, сказать, что мы фиксируем каждый период времени, – это равносильно сказать, что мы контролируем все характеристики, зависящие от года. Например, поскольку мы фиксируем год, при рассмотрении эффекта D мы будем контролировать тренд и сезонность, которые видели ранее. Чтобы увидеть все эти возможности в действии, достаточно обучить модель OLS с индикатором воздействия D (здесь treat) плюс дамми-переменные для объектов и моментов времени. В нашем конкретном примере я сгенерировал данные таким образом, что эффект от воздействия (реализации новой функциональной возможности) заключается в увеличении количества установок на 1. Обратите внимание, как TWFE идеально восстанавливает этот эффект воздействия: formula = f"""installs ~ treat + C(unit) + C(date)""" twfe_model = smf.ols(formula, data=df).fit() twfe_model.params["treat"] 1.0000000000000406 Поскольку данные, приведенные выше, я получил в результате симуляции, я точно знаю истинный индивидуальный эффект воздействия, который хранится в столбце tau. Так как модель TWFE должна восстанавливать эффект воздействия для подвергнутых воздействию, мы можем убедиться, что истинный ATT совпадает с ATT, оцененным выше. Все, что нам нужно сделать, – это отобрать объекты, подвергнутые воздействию (treat==1), и взять среднее значение столбца tau. df.query("treat==1")["tau"].mean() 1.0
1. Рождение: многообещающие панельные данные  439 Прежде чем кто-то скажет, что создание одной дамми-переменной для каждого объекта невозможно при работе с большими данными, позвольте мне выступить и сказать, что да, это правда. Но есть простое решение. Мы можем использовать теорему Фриша–Во–Ловелла для разбиения одной регрессии на две. Фактически обучение вышеприведенной модели численно эквивалентно оцениванию следующей модели: Inst̃allsit = τD̃it + eit, где Усреднение по периодам времени Усреднение по объектам и Если математика кажется вам слишком сложной, давайте поясним. Мы сначала из количества установок вычитаем количество установок, усредненное по T периодам времени для каждого объекта, и количество установок, усредненное по N объектам для каждого периода времени. Затем из воздействия мы вычитаем воздействие, усредненное по T периодам времени для каждого объекта, и воздействие, усредненное по N объектам для каждого периода времени. Так мы получаем остатки. Этот процесс часто называют центрированием, поскольку мы вычитаем среднее значение из результата и воздействия. Наконец, все то же самое представим в виде программного кода: @curry def demean(df, col_to_demean): return df.assign( **{ col_to_demean: ( df[col_to_demean] - df.groupby("unit")[col_to_demean].transform("mean") - df.groupby("date")[col_to_demean].transform("mean") ) } ) formula = f"""installs ~ treat""" mod = smf.ols( formula,
440  Сага о разности разностей data=df .pipe(demean(col_to_demean="treat")) .pipe(demean(col_to_demean="installs")) ) result = mod.fit() result.summary().tables[1] Еще один способ понять, что делает модель TWFE, – это построить график контрфактических прогнозов Ŷ0 | t ≥ g. Это полезно, поскольку для нашей модели эффект воздействия τ̂ – это просто оцененная разница Y1 – Ŷ0. Взгляд на эту явную разницу может объяснить работу модели. На графике ниже мы видим эту работу, Ŷ0 представлена пунктирными линиями. df_pred = df.assign( **{"installs_hat_0": twfe_model.predict(df.assign(**{"treat":0}))}) plt.figure(figsize=(10,4)) [plt.vlines(x=cohort, ymin=9, ymax=15, color=color, ls="dashed") for color, cohort in zip(["C0", "C1"], cohorts[:-1])] sns.lineplot( data=(df_pred .groupby(["cohort", "date"])["installs_hat_0"] .mean() .reset_index()), x="date", y="installs_hat_0", hue="cohort", alpha=0.7, ls="dotted", legend=None ) sns.lineplot( data=(df_pred .groupby(["cohort", "date"])["installs"] .mean() .reset_index()), x="date", y="installs", hue="cohort" );
2. Смерть: проблемы из-за гетерогенности эффекта  441 Этот график показывает, как модель TWFE проецирует тренд, наблюдаемый для контрольных объектов, на тестовые объекты, а также корректирует уровни. Например, если мы посмотрим на красную когорту, то контрфактическое значение Y0 – это усреднение трендов из синей и фиолетовой когорт (проекция тренда), но сдвинутое на уровень красной когорты (корректировка уровня). Вот почему мы рассматриваем TWFE как продолжение метода разности разностей. Он также выполняет прогнозирование тренда и корректировку уровня, но работает в ситуации нескольких временных периодов и нескольких объектов (в случае 2 объектов с 2 периодами TWFE и DiD эквивалентны). 2. Смерть: проблемы из-за гетерогенности эффекта Как мы только что увидели, у DiD и TWFE есть свои достоинства. Оба метода могут достаточно хорошо оценивать контрфактические результаты, учитывая вариации как по периодам времени, так и по объектам. Это делает TWFE мощным методом анализа причинно-следственных связей. Но если бы дело было только в этом, нам бы не понадобилась эта глава, поскольку все это очень подробно рассматривалось в первой части данной книги. В последнее время многие исследователи заметили, что расширение метода DiD с 2 объектами и 2 периодами (его еще обозначают DiD 2×2) на большое количество периодов с помощью TWFE не так-то просто, как мы думали вначале. На самом деле TWFE в своей классической формулировке оказывается необъективным во многих реальных задачах. Это событие заставило пересмотреть многочисленные исследования в области экономики, которые опирались на этот метод. Чтобы понять все это, лучше всего начать с разбора основного предположения.
442  Сага о разности разностей ГЕТЕРОГЕННОСТЬ ВО ВРЕМЕНИ TWFE СМЕЩЕНИЕ Для простоты рассмотрим модель фиксированных эффектов без эффектов времени: yit = τD it + γi + eit. Мы можем разбить предположения, которые выдвигает эта модель, на две группы. 1. Предположения о функциональной форме: – отсутствие изменения эффектов со временем (постоянные эффекты); – линейная связь между ковариатами и зависимой переменной (линейность ковариат); – аддитивные фиксированные эффекты (общий эффект формируется путем сложения (или добавления) фиксированных эффектов). 2. Строгая экзогенность: – параллельные тренды (до воздействия тренды изменений в группе, подвергнутой воздействию, и в контрольной группе были бы параллельными, если бы воздействия не произошло); – отсутствие предвосхищения (объекты не могут предвидеть будущее и, следовательно, не могут адаптировать свое поведение в предвидении будущего воздействия); – отсутствие ненаблюдаемых спутывающих факторов, изменяющихся во времени; – воздействие в прошлом не влияет на текущий результат (нет переноса);
­ 2. Смерть: проблемы из-за гетерогенности эффекта  443 – прошлый результат не влияет на текущее воздействие (нет обратной связи). Здесь мы будем придерживаться предположений о функциональной форме. Линейность ковариат хорошо известна и применима ко всем моделям линейной регрессии. Но как мы уже видели в главе о двойном/несмещенном машинном обучении, мы можем легко ослабить это предположение с помощью модели машинного обучения. Это означает, что мы можем ослабить данное предположение, если захотим. Что касается аддитивного фиксированного эффекта, то это не слишком строгое предположение, поэтому оно не вызывает особых проблем. Единственное предположение, на котором я хочу остановиться (и которое вызвало много шума), – это предположение об отсутствии изменения эффектов со временем (или предположение об отсутствии временнóй гетерогенности эффектов). Изменение эффекта воздействия с течением времени Если вы когда-либо работали в сфере маркетинга или технологий, то знае те: чтобы технология стала зрелой, требуется время. Когда вы запускаете новую функциональную возможность, пользователям потребуется время, чтобы привыкнуть к ней. Точно так же, если вы начинаете маркетинговую кампанию, эффект от нее не будет мгновенным. Он будет нарастать с течением времени и, возможно, приведет новых пользователей даже после завершения кампании. Это не та картина, которую мы наблюдали ранее в примере с установками приложения. Там количество установок подскочило мгновенно в момент оказания воздействия на когорту. Что произойдет, если мы изменим ATT так, чтобы он больше соответствовал реальной картине. А именно сделаем так, чтобы ATT по-прежнему равнялся 1, но теперь для его созревания требуется 10 дней (так что в первый день воздействия он будет равен 0.1, во второй – 0.2 и т. д., пока не достигнет 1 на 10-й день). Кроме того, я уменьшу размер эффектов времени и объектов, чтобы было легче увидеть общий тренд. date = pd.date_range("2021-05-01", "2021-07-31", freq="D") cohorts = pd.to_datetime(["2021-06-01", "2021-07-15", "2022-01-01"]).date units = range(1, 100+1) np.random.seed(1) df_heter = pd.DataFrame(dict( date = np.tile(date.date, len(units)), unit = np.repeat(units, len(date)), cohort = np.repeat(np.random.choice(cohorts, len(units)), len(date)), unit_fe = np.repeat(np.random.normal(0, 5, size=len(units)), len(date)), time_fe = np.tile(np.random.normal(size=len(date)), len(units)),
444  Сага о разности разностей week_day = np.tile(date.weekday, len(units)), w_seas = np.tile(abs(5-date.weekday) % 7, len(units)) )).assign( trend = lambda d: (d["date"] - d["date"].min()).dt.days/70, day = lambda d: (d["date"] - d["date"].min()).dt.days, treat = lambda d: (d["date"] >= d["cohort"]).astype(int) ).assign( y0 = lambda d: 10 + d["trend"] + 0.2*d["unit_fe"] + 0.05*d["time_fe"] + d["w_seas"]/50 ).assign( y1 = lambda d: d["y0"] + np.minimum( 0.1*(np.maximum(0, (d["date"] - d["cohort"]).dt.days)), 1) ).assign( tau = lambda d: d["y1"] - d["y0"], installs = lambda d: np.where(d["treat"] == 1, d["y1"], d["y0"]) ) plt.figure(figsize=(10,4)) [plt.vlines(x=cohort, ymin=9, ymax=15, color=color, ls="dashed") for color, cohort in zip(["C0", "C1"], cohorts)] sns.lineplot( data=(df_heter .groupby(["cohort", "date"])["installs"] .mean() .reset_index()), x="date", y="installs", hue="cohort" ); Мы видим, что количество установок достигает прежнего уровня, но для этого требуется некоторое время (10 дней). Это кажется разумным, верно? Большинство данных, с которыми мы сталкиваемся в реальной жизни, работают именно так: эффектам требуется некоторое время для созревания. Итак, давайте обучим модель TWFE на этих данных и посмотрим, что произойдет.
    2. Смерть: проблемы из-за гетерогенности эффекта  445 formula = f"""installs ~ treat + C(date) + C(unit)""" twfe_model = smf.ols(formula, data=df_heter).fit() print("Оцененный эффект: ", twfe_model.params["treat"]) print("Истинный эффект: ", df_heter.query("treat==1")["tau"].mean()) Оцененный эффект: 0.786770822572485 Истинный эффект: 0.8544117647058823 Во-первых, обратите внимание, что истинное значение ATT больше не равно 1. Это обусловлено тем, что в первые несколько периодов оно будет меньше. Во-вторых, и это самое главное, мы видим, что значение ATT, полученное с помощью TWFE, больше не соответствует истинному значению ATT. Проще говоря, модель TWFE не является объективной. Но почему? У нас соблюдаются предположения о параллельных трендах, отсутствии предвосхищения и все остальные строгие предположения об экзогенности. Так что же происходит? Первый шаг к пониманию происходящего – осознать, что TWFE на самом деле можно разложить на несколько моделей разности разностей 2×2. В нашем примере это будет:  сравнение объектов, рано подвергнутых воздействию, с объектами, которые никогда не подвергались воздействию;  сравнение объектов, поздно подвергнутых воздействию, с объектами, которые никогда не подвергались воздействию;  сравнение объектов, рано подвергнутых воздействию, с объектами, поздно подвергнутыми воздействию (где объекты, поздно подвергнутые воздействию, выступают в качестве контрольных);  сравнение объектов, поздно подвергнутых воздействию, с объектами, рано подвергнутыми воздействию (где объекты, рано подвергнутые воздействию, выступают в качестве контрольных). g_plot_data = (df_heter .groupby(["cohort", "date"])["installs"] .mean() .reset_index() .astype({"cohort":str})) fig, axs = plt.subplots(2, 2, figsize=(15,8), sharex=True, sharey=True) def plot_comp(df, ax, exclude_cohort, name): palette=dict(zip(map(str, cohorts), ["C0", "C1", "C2"])) sns.lineplot( data=df.query(f"cohort != '{exclude_cohort}'"), x="date", y="installs", hue="cohort", palette=palette, legend=None,
446  Сага о разности разностей ax=ax ) sns.lineplot( data=df.query(f"cohort == '{exclude_cohort}'"), x="date", y="installs", hue="cohort", palette=palette, alpha=0.2, legend=None, ax=ax ) ax.set_title(name) plot_comp( g_plot_data, axs[0,0], cohorts[1], "Рано подвергнутые воздействию vs\nНикогда не подвергавшиеся воздействию" ) plot_comp( g_plot_data, axs[0,1], cohorts[0], "Поздно подвергнутые воздействию vs\nНикогда не подвергавшиеся воздействию" ) plot_comp( g_plot_data[g_plot_data["date"]<=cohorts[1]], axs[1,0], cohorts[-1], "Рано подвергнутые воздействию vs\nПоздно подвергнутые воздействию" ) plot_comp( g_plot_data[g_plot_data["date"]>cohorts[0]], axs[1,1], cohorts[-1], "Поздно подвергнутые воздействию vs\nРано подвергнутые воздействию" ) plt.tight_layout();
2. Смерть: проблемы из-за гетерогенности эффекта  447 Первые три сравнения не вызывают опасений, преимущественно в силу того, что объекты, которые используются в качестве контрольных, довольно логично себя ведут. Однако четвертое сравнение, сравнение объектов, поздно подвергнутых воздействию, с объектами, рано подвергнутыми воздействию, вызывает проблемы. Обратите внимание, что в этом сравнении в качестве контроля используются объекты, рано подвергнутые воздействию. Кроме того, обратите внимание, что эта контрольная группа, которая состоит из объектов, рано подвергнутых воздействию, демонстрирует странное поведение. Мы видим резкое возрастание в самом начале. Вспомним, что наш АТТ не является мгновенным, а требует 10 дней для созревания. Интуитивно мы можем понять, что это исказит оценивание контрфактического тренда с помощью DiD, сделав тренд более крутым, чем он должен быть. Чтобы представить это наглядно, давайте построим график оцененных контрфактических значений Y0 для тех, кто поздно подвергся воздействию в этой четвертой группе. late_vs_early = (df_heter [df_heter["date"].astype(str)>="2021-06-01"] [lambda d: d["cohort"].astype(str)<="2021-08-01"]) formula = f"""installs ~ treat + C(date) + C(unit)""" twfe_model = smf.ols(formula, data=late_vs_early).fit() late_vs_early_pred = ( late_vs_early .assign(**{"installs_hat_0": twfe_model.predict( late_vs_early.assign(**{"treat":0}))}) .groupby(["cohort", "date"]) [["installs", "installs_hat_0"]] .mean() .reset_index() ) plt.figure(figsize=(10,4)) plt.title("Поздно подвергнутые воздействию vs\nРано подвергнутые " "воздействию\nКонтрфактические результаты") sns.lineplot( data=late_vs_early_pred, x="date", y="installs", hue="cohort", legend=None ) sns.lineplot( data=(late_vs_early_pred [late_vs_early_pred["cohort"].astype(str) == "2021-07-15"] [lambda d: d["date"].astype(str) >= "2021-07-15"]
448  Сага о разности разностей ), x="date", y="installs_hat_0", alpha=0.7, color="C0", ls="dotted", label="counterfactual" ); Как мы и говорили, контрфактические данные демонстрируют гораздо более крутой тренд, чем он должен быть на самом деле. Модель улавливает быстрый рост в начале раннего воздействия и проецирует этот тренд на позднее воздействие. Можно показать (Goodman-Bacon, 2019), что даже при строгой экзогенности (параллельные тренды, отсутствие предвосхищения и т. д.), если когорты имеют одинаковый размер, оценка TWFE будет сходиться к plimx→∞τ̂ TWFE = VWATT – ∆ATT. Первый член – это взвешенный по дисперсии ATT, полученный в результате множественных сравнений DiD, аналогичных тем, что мы видели ранее. Это то, что нам нужно. Однако есть еще один дополнительный член ∆ATT. Он представляет собой величину изменения ATT со временем, и именно от него зависит смещение нашей оценки. Глядя на этот член, мы видим, что у нас будет смещение в сторону уменьшения, если величина эффекта увеличивается со временем (как в нашем примере), или смещение в сторону увеличения, если величина эффекта уменьшается со временем.
2. Смерть: проблемы из-за гетерогенности эффекта  449 В вышеприведенном примере мы видели, что оценка эффекта, полученная с помощью модели TWFE, была меньше истинного значения ATT. Но ситуация может быть еще более экстремальной. Думаю, стоит привести еще один пример, чтобы увидеть, что это смещение может быть настолько сильным, что даже перевернет сигнал истинного ATT. Рассмотрим очень простой процесс, в котором участвуют только две когорты. Здесь эффект воздействия будет отрицательным и будет уменьшаться на 0.1 каждый день. Кроме того, я убрал все временны́е фиксированные эффекты и тренд, чтобы мы могли увидеть, что происходит на самом деле. date = pd.date_range("2021-05-15", "2021-07-01", freq="D") cohorts = pd.to_datetime(["2021-06-01", "2021-06-15"]) units = range(1, 100+1) np.random.seed(1) df_min = pd.DataFrame(dict( date = np.tile(date, len(units)), unit = np.repeat(units, len(date)), cohort = np.repeat(np.random.choice(cohorts, len(units)), len(date)), unit_fe = np.repeat(np.random.normal(0, 5, size=len(units)), len(date)) )).assign( trend = 0, day = lambda d: (d["date"] - d["date"].min()).dt.days, treat = lambda d: (d["date"] >= d["cohort"]).astype(int) ).assign( y0 = lambda d: 10 - d["trend"] + 0.1*d["unit_fe"] ).assign( y1 = lambda d: d["y0"] - 0.1*( np.maximum(0, (d["date"] - d["cohort"]).dt.days)) ).assign( tau = lambda d: d["y1"] - d["y0"], installs = lambda d: np.where(d["treat"] == 1, d["y1"], d["y0"]) ) plt.figure(figsize=(10,4)) [plt.vlines(x=cohort, ymin=7, ymax=11, color=color, ls="dashed") for color, cohort in zip(["C0", "C1"], cohorts)] sns.lineplot( data=(df_min .groupby(["cohort", "date"])["installs"] .mean() .reset_index()), x="date", y="installs", hue="cohort" );
­ 450  Сага о разности разностей Глядя на график выше, мы ясно видим, что ATT отрицателен, верно? Корректный контрфактический результат должен быть прямой линией на уровне 11. Однако если мы обучим модель TWFE, то получим положительный эффект! formula = f"""installs ~ treat + C(date) + C(unit)""" twfe_model = smf.ols(formula, data=df_min).fit() twfe_model.params["treat"] 0.04999999999998833 И снова, чтобы понять, что происходит, сосредоточьте свое внимание на сравнении, в котором когорта объектов, испытавших воздействие в раннем периоде, служит контролем для когорты объектов, испытавших воздействие в позднем периоде. Помните, что, как и DiD, TWFE корректирует тренд конт рольной группы до уровня тестовой группы, поэтому контрфактический результат должен отражать эту коррекцию. df_pred = df_min.assign( **{"installs_hat_0": twfe_model.predict(df_min.assign(**{"treat":0}))} ) plt.figure(figsize=(10,4)) [plt.vlines(x=cohort, ymin=7, ymax=11, color=color, ls="dashed") for color, cohort in zip(["C0", "C1"], cohorts)] sns.lineplot( data=(df_pred [(df_pred["cohort"].astype(str) > "2021-06-01") & (df_pred["date"].astype(str) >= "2021-06-15")] .groupby(["cohort", "date"])["installs_hat_0"] .mean() .reset_index()), x="date", y="installs_hat_0",
2. Смерть: проблемы из-за гетерогенности эффекта  451 alpha=0.7, ls="dotted", color="C0", label="counterfactual" ) sns.lineplot( data=(df_pred .groupby(["cohort", "date"])["installs"] .mean() .reset_index()), x="date", y="installs", hue="cohort", legend=None ) plt.ylabel("Installs"); Обратите внимание, как контрфактический уровень сдвигается вниз от того положения, где он должен быть. Факт того, что эффект для группы объектов, рано подвергнутых воздействию, снижается, сдвинул этот уровень с 10 до примерно 9.5. Мало того, контрфактический результат также корректируется с учетом нисходящего тренда, которого не должно быть. Из графика ясно, что правильный контрфактический результат должен быть прямой линией на уровне 10, но вместо этого он представляет собой нисходящую наклонную линию, поскольку именно такая линия наблюдается в группе объектов, получивших воздействие в раннем периоде, которая используется в качестве контроля. В итоге контрфактическое значение Y0 фактически оказывается ниже Y1, что приводит к положительной оценке влияния. Это довольно неловко. Просто взглянув на график, мы видим, где должен находиться правильный контрфактический результат (прямая линия на уровне 10). Однако TWFE не может восстановить эту картину в силу того, что использует объекты, получившие воздействие в ранний период, в качестве контроля для объектов, получивших воздействие в поздний период.
     452  Сага о разности разностей Дизайн анализа событий Я знаю, кто-то может подумать, что мы можем легко решить эту проблему с помощью так называемого дизайна анализа событий, когда добавляем по одной дамми-переменной для каждого периода до и после воздействия. В этом случае мы заменяем исходную модель TWFE на где:  D ki,t – это дамми-переменная, которая равна 1, если объект i относится к тестовой группе И t составляет точно k периодов ДО воздействия, и 0 в противном случае;  Di,tl – это дамми-переменная, которая равна 1, если объект i относится к тестовой группе И t составляет точно l периодов ПОСЛЕ воздействия, и 0 в противном случае;  D i,t<–K – это дамми-переменная, которая равна 1, если объект i относится к тестовой группе И t составляет более K периодов ДО воздействия, и 0 в противном случае;  D i,t>L – это дамми-переменная, которая равна 1, если объект i относится к тестовой группе И t составляет более L периодов ПОСЛЕ воздействия, и 0 в противном случае;  γ lag учитывают лаговый (запаздывающий) эффект, то есть эффект, который продолжает действовать даже после воздействия. τ lead учитывают эффект предвосхищения, то есть эффект, который предшествует воздействию. Я знаю, что это выглядит запутанно и сложно, но на самом деле все очень просто, если посмотреть на программную реализацию данной формулы. Все, что нам нужно сделать, – это создать столбец relative_days, измеряющий, насколько далеко объект находится от периода, в котором начинается воздействие. df_min_rel = df_min.assign( relative_days = (df_min["date"] - df_min["cohort"]).dt.days) df_min_rel.head()
2. Смерть: проблемы из-за гетерогенности эффекта  453 Затем мы можем передать этот столбец в качестве категориальной переменной, чтобы наша модель оценила ожидаемое количество установок для каждого периода относительно воздействия. Потом мы можем определить эффект как дополнительное ожидаемое количество установок по сравнению с относительным днем –1, который является последним днем перед воздействием. Мы могли бы подумать, что такая формулировка отразит временную гетерогенность ATT и решит все наши проблемы. К сожалению, это не так. Если мы попробуем ее применить и построим график контрфактических результатов, то увидим, что они далеки от того, где должны интуитивно находиться (далеки от горизонтальной линии на уровне 11). # удаляем свободный член, иначе эффекты будут # относительны относительного дня –30. formula = f"installs ~ -1 + C(relative_days) + C(date) + C(unit)" twfe_model = smf.ols(formula, data=df_min_rel).fit() df_pred = df_min_rel.assign( installs_hat_0=twfe_model.predict(df_min_rel.assign(relative_days=-1)) ) plt.figure(figsize=(10,4)) [plt.vlines(x=cohort, ymin=7, ymax=11, color=color, ls="dashed") for color, cohort in zip(["C0", "C1"], cohorts)] sns.lineplot( data=(df_pred [(df_pred["cohort"].astype(str) > "2021-06-01") & (df_pred["date"].astype(str) >= "2021-06-15")] .groupby(["cohort", "date"])["installs_hat_0"] .mean() .reset_index()), x="date", y="installs_hat_0", alpha=0.7, ls="dotted", color="C0", label="counterfactual" ) sns.lineplot( data=(df_pred .groupby(["cohort", "date"])["installs"] .mean() .reset_index()), x="date", y="installs", hue="cohort", legend=None ) plt.ylabel("Installs");
454  Сага о разности разностей Интересно И под «интересно» я подразумеваю «ужасно» Однако эти контрфактические результаты немного лучше. Мы видим, что они выше результата Y1. Таким образом, по крайней мере, мы получили отрицательную оценку эффекта, как и должно быть. Чтобы убедиться в этом, мы можем построить график оцененных эффектов. Нам сначала нужно извлечь параметр, связанный с каждой дамми-переменной, а затем вычесть из него параметр, связанный с относительным днем –1 (базовый уровень). effects = ( twfe_model.params[twfe_model.params.index.str.contains("relative_days")] .reset_index() .rename(columns={0:"effect"})
3. Просветление: гибкая функциональная форма  455 .assign(relative_day=lambda d: d["index"].str.extract( r'\[(.*)\]').astype(int)) # задаем базовый уровень для периода –1 .assign(effect = lambda d: d["effect"] d.query("relative_day==-1")["effect"].iloc[0]) ) # эффекты effects.plot(x="relative_day", y="effect", figsize=(10,4)) plt.ylabel("Оцененный эффект") plt.xlabel("Время относительно воздействия"); Видим, что график стал немного лучше, потому что по крайней мере оцененный эффект после воздействия 1) в основном является отрицательным и 2) преимущественно уменьшается. Но есть странные пики и паттерн, выглядящий как положительный эффект до воздействия, что, очевидно, не имеет никакого смысла. Проблема здесь та же, о которой мы уже говорили. Поскольку у нас воздействие происходило в разные периоды, объекты, испытавшие воздействие в раннем периоде, используются в качестве контроля для объектов, испытавших воздействие в позднем периоде, это приводит к тому, что модель прогнозирует очень странный контрфактический тренд. В итоге получается, что добавление дамми-переменных для периодов времени относительно воздействия не решает проблему, но что именно решает ее? 3. Просветление: гибкая функциональная форма Есть хорошие и плохие новости. Первая, хорошая новость: мы определили, что проблема связана с функциональной формой, поэтому мы можем ре-
456  Сага о разности разностей шить ее, исправив эту форму. А именно мы неоднократно говорили, что это специфическое смещение в TWFE возникает из-за временнóй гетерогенности эффектов. Оно может происходить, помимо многих других причин, потому что эффекту требуется некоторое время для созревания (например, маркетинговой кампании может потребоваться 10 дней, чтобы полностью раскрыть свой потенциал). Другими словами, функциональная форма традиционной модели TWFE просто является недостаточно гибкой, чтобы уловить эту гетерогенность, что приводит к тому, о чем мы уже говорили. Как и в большинстве случаев, понимание проблемы – это уже большой шаг в направлении поиска решения. В конце предыдущего раздела мы увидели, что простого предположения о том, что эффект может быть разным в каждый период времени по отношению к воздействию (на котором строится дизайн анализа событий), недостаточно. Даже если оно не сработало, намерение было хорошим. Оно сделало модель более гибкой, но не решило проблему. Нам нужно придумать другой способ сделать модель еще более гибкой, чем этот. Для этого давайте вернемся к нашему первоначальному примеру, когда мы пытались смоделировать дополнительное количество установок приложения, которые приносит нам реализация новой функциональной возможности (воздействие). Мы увидели, что простая модель TWFE здесь не работает: Installsit = τD it + γi + θt + eit. Более того, мы знаем, что она не работает, потому что предполагает сильные ограничения. Она заставляет эффект быть одинаковым τit = τ∀i, t, то есть она заставляет время быть гомогенным (однородным). Если проблема заключается в этом, то простое решение заключается в том, чтобы просто разрешить разные эффекты для каждого периода времени и объекта: Эта формула эквивалентна формуле ниже: installs ~ treat:C(unit):C(date) + C(unit) + C(date). К сожалению, мы не можем обучить модель по этой формуле. Ведь количество параметров будет превышать количество наблюдений. Поскольку мы создаем взаимодействие между датой и объектом, у нас будет по одному параметру эффекта воздействия для каждого объекта и периода времени T ∗ N. Но это именно то количество наблюдений, которое у нас есть! OLS не будет работать здесь. Итак, нам нужно уменьшить количество параметров эффекта воздействия в модели. Чтобы добиться этого, мы можем каким-то образом сгруппировать объекты. Если мы немного подумаем, то вспомним про очень естественный способ группировки объектов – по когортам! Мы знаем, что эффект во всей
­ 3. Просветление: гибкая функциональная форма  457 когорте подчиняется с течением времени одной и той же закономерности. Таким образом, естественное улучшение этой непрактичной модели – позволить эффекту меняться по когортам, а не по объектам: где G – общее количество когорт, а g обозначает каждую отдельную когорту. У этой модели уже гораздо более разумное количество параметров эффекта воздействия (T ∗ G), поскольку G обычно намного меньше, чем N. Теперь, наконец, мы можем обучить ее. formula = f"""installs ~ treat:C(cohort):C(date) + C(unit) + C(date)""" # для более красивых графиков в дальнейшем df_heter_str = df_heter.astype({"cohort":str, "date":str}) twfe_model = smf.ols(formula, data=df_heter_str).fit() Чтобы убедиться в работоспособности этой модели, давайте получим контр фактические прогнозы для Y0, принудительно установив для всех нулевое значение treat. Затем мы можем оценить эффект, для этого берем фактический результат воздействия Y1 и вычитаем из него Ŷ0. Посмотрим, совпадает ли наш прогноз с фактическим значением ATT. df_pred = ( df_heter_str .assign(**{"installs_hat_0": twfe_model.predict( df_heter_str.assign(**{"treat":0}))}) .assign(**{"effect_hat": lambda d: d["installs"] - d["installs_hat_0"]}) ) print("Количество параметров:", len(twfe_model.params)) print("Истинный эффект: ", df_pred.query("treat==1")["tau"].mean()) print("Спрогнозированный эффект: ", df_pred.query("treat==1")["effect_hat"].mean()) Количество параметров: 467 Истинный эффект: 0.8544117647058823 Спрогнозированный эффект: 0.8544117647058845 Так и есть! Нам наконец-то удалось создать модель, достаточно гибкую, чтобы учесть временнýю гетерогенность, что позволило получить корректную оценку эффекта воздействия! Еще одна классная вещь, которую мы можем сделать, – это извлечь оцененные эффекты по временным периодам и когортам и визуализировать их. В данном случае, поскольку мы знаем, как были получены данные, нам уже известно, чего ожидать. А именно эффект для каждой когорты должен быть равен 0 до воздействия и 1 через 10 дней после воздействия, от 0 к 1 поднимается прямая линия, охватывающая начало воздействия и 10 дней после него.
­ 458  Сага о разности разностей effects = ( twfe_model.params[twfe_model.params.index.str.contains("treat")] .reset_index() .rename(columns={0:"param"}) .assign(cohort=lambda d: d["index"].str.extract(r'C\(cohort\)\[(.*)\]:')) .assign(date=lambda d: d["index"].str.extract(r':C\(date\)\[(.*)\]')) .assign(date=lambda d: pd.to_datetime(d["date"]), cohort=lambda d: pd.to_datetime(d["cohort"])) ) plt.figure(figsize=(10,4)) sns.lineplot(data=effects, x="date", y="param", hue="cohort") plt.xticks(rotation=45) plt.ylabel("Оцененный эффект"); И снова график соответствует нашим ожиданиям относительно эффектов. Они подчиняются именно тому паттерну, который мы описали выше. Это уже очень хорошо, но мы можем сделать еще лучше. Во-первых, обратите внимание на то, что эта модель имеет огромное количество парамет ров. Поскольку в наших данных 100 объектов и около 92 дней, мы знаем, что 192 параметра из общего количества параметров – это эффекты объектов и периодов времени. Тем не менее еще остается более 250 параметров эффекта воздействия. Если мы предположим нулевой эффект до воздействия (отсутствие предвосхищения), то можем уменьшить количество параметров, исключив даты до воздействия из члена взаимодействия: Кроме того, мы можем исключить из взаимодействия контрольные когорты, поскольку их эффект до воздействия всегда равен нулю:
­ 3. Просветление: гибкая функциональная форма  459 где когорты до g < q определены как контрольные когорты. Однако заметьте, что это сложно реализовать с помощью формул, поэтому сначала нам придется немного заняться конструированием признаков. Мы вручную добавим дамми-переменные когорт, создав один столбец cohort_0601, который будет равен 1, когда переменная cohort равна 2021-06-01, и 0 в противном случае, и второй столбец cohort_0715, который будет равен 1, когда переменная cohort равна 2021-07-15, и 0 в противном случае. Кроме того, мы создадим столбец date_0601 для когорты 2021-06-01, в нем мы запишем все даты до 2021-06-01 в категорию control (контрольную категорию). Затем создадим столбец date_0715 для когорты 2021-07-15, в нем мы запишем в категорию control (контрольную категорию) уже все даты до 2021-07-15. Вот как это выглядит в программном коде. def feature_eng(df): return ( df .assign(date_0601 = np.where(df["date"]>="2021-06-01", df["date"], "control"), date_0715 = np.where(df["date"]>="2021-07-15", df["date"], "control")) .assign(cohort_0601 = (df["cohort"]=="2021-06-01").astype(float), cohort_0715 = (df["cohort"]=="2021-07-15").astype(float)) ) formula = f"""installs ~ treat:cohort_0601:C(date_0601) + treat:cohort_0715:C(date_0715) + C(unit) + C(date)""" twfe_model = smf.ols(formula, data=df_heter_str.pipe(feature_eng)).fit() Если мы теперь захотим получить контрфактические прогнозы, как раньше, то увидим, что оцененный эффект по-прежнему полностью совпадает с истинным. Выигрыш заключается в том, что у нас есть гораздо более прос тая модель, включающая всего около 80 параметров эффекта воздействия (вспомним, что 192 параметра – это фиксированные эффекты периода времени и объекта). df_pred = ( df_heter .assign(**{"installs_hat_0": twfe_model.predict( df_heter_str.pipe(feature_eng).assign(**{"treat":0}))}) .assign(**{"effect_hat": lambda d: d["installs"] d["installs_hat_0"]}) ) print(len(twfe_model.params)) print("Истинный эффект: ",
460  Сага о разности разностей df_pred.query("treat==1")["tau"].mean()) print("Спрогнозированный эффект: ", df_pred.query("treat==1")["effect_hat"].mean()) 271 Истинный эффект: 0.8544117647058823 Спрогнозированный эффект: 0.85441176470588 Теперь мы строим график параметров эффекта воздействия. Мы удалили параметры из контрольной когорты и параметры, которые относятся к датам, предшествующим воздействию на когорту. effects = ( twfe_model.params[twfe_model.params.index.str.contains("treat")] .reset_index() .rename(columns={0:"param"}) .assign(cohort=lambda d: d["index"].str.extract( r':cohort_(.*):'), date_0601=lambda d: d["index"].str.extract( r':C\(date_0601\)\[(.*)\]'), date_0715=lambda d: d["index"].str.extract( r':C\(date_0715\)\[(.*)\]')) .assign(date=lambda d: pd.to_datetime( d["date_0601"].combine_first(d["date_0715"]), errors="coerce")) ) plt.figure(figsize=(10,4)) sns.lineplot(data=effects.dropna(subset=["date"]), x="date", y="param", hue="cohort") plt.xticks(rotation=45); Обратите внимание, что мы можем пойти еще дальше, поскольку эффект для обеих когорт имеет одинаковую форму. Мы можем ограничить модель та-
­ 3. Просветление: гибкая функциональная форма  461 ким образом, чтобы у обеих когорт был одинаковый эффект, изменяющийся только со временем. Для этого нам нужно создать столбец для обозначения дней после воздействия, как в дизайне анализа событий. Это будет выглядеть примерно так: days_after_treat=1(date>cohort)*(date - cohort) Затем мы создаем взаимодействие этого признака с индикатором воздействия: installs ~ treat:C(days_after_treat) + C(unit) + C(date) Однако я думаю, что на этом можно остановиться. Запрет гетерогенности по когортам чаще всего является плохой идеей, поскольку эффект воздействия, как правило, меняется в течение календарного времени, а не только в период после воздействия. Например, может случиться так, что через некоторое время конкуренты скопируют нашу функциональную возможность, и она перестанет привлекать клиентов так сильно, как раньше. В этом случае влияние новой функциональной возможности на количество установок будет уменьшаться со временем. И последнее, что мы должны сделать, помимо демонстрации эффектов, меняющихся со временем, – это построить графики контрфактических прог нозов, чтобы убедиться в том, что они располагаются там, где и должны быть. Я знаю, что это не очень научное подтверждение нашей модели, но поверьте мне, это очень помогает. Итак, вот оно. twfe_model_wrong = smf.ols("installs ~ treat + C(date) + C(unit)", data=df_pred).fit() df_pred = ( df_pred .assign(**{"installs_hat_0_wrong": twfe_model_wrong.predict(df_pred.assign(**{"treat":0}))}) ) plt.figure(figsize=(10,4)) sns.lineplot( data=(df_pred [(df_pred["cohort"].astype(str) > "2021-06-01") & (df_pred["date"].astype(str) >= "2021-06-01")] .groupby(["date"])["installs_hat_0"] .mean() .reset_index()), x="date", y="installs_hat_0", ls="dotted", color="C3", label="counterfactual", )
462  Сага о разности разностей sns.lineplot( data=(df_pred .groupby(["cohort", "date"])["installs"] .mean() .reset_index()), x="date", y="installs", hue="cohort", legend=None ) plt.ylabel("Installs"); Как мы видим, контрфактические прогнозы Y0 попадают как раз туда, куда, по нашему мнению, они должны попасть, то есть расположены довольно близко к контрольной когорте. Это очень успокаивает. Мы знаем, что модель TWFE оценивает эффект воздействия как Y – Ŷ0. То есть мы просто сравниваем результаты когорт, подвергнутых воздействию, с контрфактическими результатами. Поскольку контрфактические результаты корректны, мы можем быть уверены, что эффект воздействия тоже, вероятно, корректен. Это хорошая новость, но не думайте, что я забыл о плохой новости, которую я вам обещал. Дело в том, что хотя мы решили проблему функциональной формы с помощью TWFE, остается еще одна, возможно, более серьезная проблема DiD и TWFE, связанная с их предположением о независимости. При использовании моделей DiD и TWFE мы часто ссылаемся на предположение о параллельных трендах, не задумываясь о том, что именно это предположение означает. К сожалению, предположение о параллельных трендах накладывает гораздо более строгие ограничения и является менее правдоподобным, чем многие думают. Но поскольку эта глава уже и так является слишком большой, я думаю, что можно закончить ее здесь, и мы можем насладиться вкусом небольшой победы, одержанной с помощью метода DiD.
Ключевые идеи  463 Ключевые идеи Думаю, можно с уверенностью сказать, что нам наконец-то удалось не только понять, но и исправить проблему функциональной формы модели TWFE. Мы выяснили корни проблемы (временнáя гетерогенность) и устранили ее, обеспечив большую гибкость. Теперь мы можем выпить и расслабиться, зная, что TWFE снова можно использовать. Или нет?! КОГДА TWFE ПРОСТО РАБОТАЕТ: Нельзя забывать, что метод TWFE (и метод DiD в целом) – это смесь функциональной формы и предположений о независимости. В этой главе мы рассмотрели только проблемы, связанные с функциональной формой, но в нашей комнате по-прежнему находится большой слон – предположение о параллельных трендах. Параллельные тренды – это предположение о независимости, которое выдвигает DiD. Оно хорошо известно. Но мне кажется, что мы не очень понимаем, что означает это предположение. Мы просто ссылаемся на него, как будто упоминание этого предположения уже делает его истинным. К сожалению, предположение о параллельных трендах требует гораздо большего понимания, чем многие думают. В следующих главах мы увидим, почему это так и что можно с этим сделать.
­ ­ ­ ­ 464  Сага о разности разностей Дополнительное чтение Написание этой главы заняло (очень) много времени. В последнее время эконометрическая литература пестрит новыми идеями и взглядами на проб лемы, связанные с DiD, и способами их решения. Мы можем рассматривать эти проблемы под разными углами, что, в свою очередь, приводит к появлению множества подходов, которые мы можем использовать для их решения. Приготовьтесь к тому, что список статей здесь будет длинным (и, вероятно, не очень хорошо организованным). Во-первых, я многое почерпнул из статьи Эндрю Гудмана-Бейкона (Andrew Goodman-Bacon) «Difference-in-Differences with Variation in Treatment Ti ming», https://www.sciencedirect.com/science/article/abs/pii/S0304407621001445. Его диагностика проблемы очень аккуратна и интуитивно понятна. Не говоря уже о том, что она сопровождается очень красивыми картинками, которые дали мне четкое понимание того, как понять, что происходит с DiD. Некоторые из картинок в этой главе – почти полная копия тех, что сделал ГудманБейкон. Вторым важным источником вдохновения стала статья «Difference-inDifferences with Multiple Time Periods» Педро Х. К. Сант’Анны (Pedro H. C. Sant’Anna) и Брентли Каллауэй (Brantly Callaway), https://www.sciencedirect. com/science/article/abs/pii/S0304407620303948. Обратите внимание, что решение этой задачи в работе Каллауэя и Сант’Анны идет по другому пути, нежели тот, который выбрали мы. Однако их решение проливает свет на проблему DiD, делая ее понятной. Помимо статьи, у Педро есть очень хороший пост в блоге, показывающий проблемы с TWFE. Процесс генерации данных, приведенный в этом блоге, в значительной степени вдохновил меня на эксперименты, которые я использовал здесь. Я, по сути, перевел код Педро с R на Python. Педро также был очень любезен, помогая мне с некоторыми вопросами, касающимися предположений DiD. У него есть еще одна очень интересная статья, посвященная проблеме добавления ковариат в модели DiD, которую мы не стали рассматривать здесь, потому что глава и так получилась слишком длинной. Статья называется «Doubly Robust Differencein-Differences Estimators» (https://www.sciencedirect.com/science/article/abs/pii/ S0304407620301901), и я настоятельно рекомендую вам прочитать ее, если вы планируете добавить ковариаты в свою модель. И последняя, но, безусловно, не менее важная коррекция функциональной формы, которую мы использовали здесь, была вдохновлена статьями «Estimating dynamic treatment effects in event studies with heterogeneous treatment effects» Лиян Сунь и Сары Абрахам (Liyang Sun and Sarah Abraham), https://arxiv.org/ abs/1804.05785, и «Two-Way Fixed Effects, the Two-Way Mundlak Regression, and Difference-in-Differences Estimators» Джеффри Вулдриджа (Jeffrey Wooldridge), https://www.researchgate.net/publication/354015780_Two-Way_Fixed_Effects_the_ Two-Way_Mundlak_Regression_and_Difference-in-Differences_Estimators. Хотя мне очень понравилось то, что сделали Каллауэй и Сант’Анна в своей работе, их
Дополнительное чтение  465 решение немного сложнее в реализации. В отличие от них, решение Лиян Сунь, Сары Абрахам и Джеффри Вулдриджа не требует ничего, кроме продуманного подбора взаимодействий для модели регрессии TWFE, что делает его очень простым в реализации, предполагая использование лишь библиотеки statsmodels и некоторых формул. Помимо вышеупомянутых статей, я также использовал презентации DiD Study Group, организованной Тейлором Райтом (Taylor Wright) на YouTube, https://www.youtube.com/playlist?list=PLVObvb_htcuBt8mV9yNagt7hK9FL5KXeE. Гораздо легче понять вышеупомянутые статьи, если сначала посмотреть выступления авторов. Я также очень благодарен профессору Ицину Ксу (Yiqing Xu) за то, что он связал все это воедино в своем потрясающем цикле лекций Causal Inference with Panel Data, также доступном на YouTube, https://www. youtube.com/playlist?list=PLo0lw6BstMGZQqx_r1GnOETkFYihCgve9. Наконец, не забывайте, что я тоже учусь, поэтому если вы найдете что-то нелепое, задайте вопрос, и я постараюсь ответить на него по мере своих сил. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
­ Глава 25 Синтетическая разность разностей В предыдущих главах мы рассмотрели методы разности разностей и синтетического контроля для определения эффекта воздействия в панельных данных (когда у нас есть данные по нескольким объектам i, за которыми мы наблюдаем в течение нескольких периодов времени t). Оказалось, что мы можем объединить оба подхода в рамках одного метода оценивания. Эта новая процедура оценки под названием «синтетическая разность разностей» (Synthetic Difference-in-Differences, SDiD, СРР) позволяет использовать преимущества обоих методов, одновременно повышая точность (уменьшая ошибку) оценки эффекта воздействия. Мы рассмотрим синтетическую разность разностей преимущественно в контексте блочного назначения воздействия (block treatment assignment). Это означает, что мы наблюдаем несколько объектов в разные периоды времени и в один и тот же период времени одни объекты подвергаются воздействию, а другие объекты не подвергаются воздействию. Мы можем представить это в виде матрицы назначений воздействия D, где столбцы матрицы – это объекты, а строки матрицы – это периоды времени. Давайте продолжим пример с оценкой влияния Положения 99 на потреб ление сигарет в Калифорнии. В этом случае у нас есть только один тестовый объект – Калифорния, которая поверглась воздействию (приняла Положение 99) в какой-то момент времени (если быть точным, в ноябре 1988 года). Если мы скажем, что Калифорния – это последний столбец матрицы, то получим примерно следующее:
­ Синтетическая разность разностей  467 Обратите внимание, что здесь мы разбираем только ситуацию, когда все тестовые объекты подвергаются воздействию в один и тот же момент времени. В конце главы мы обсудим, как работать с постепенным или пошаговым назначением воздействия (staggered adoption treatment assignment), когда воздействие постепенно распространяется на объекты, в результате чего объекты получают воздействие в разные моменты времени. Единственное требование такого дизайна: как только объект подвергся воздействию, он не может вернуться к состоянию до воздействия. Возвращаясь к простому случаю, когда все объекты подвергаются воздействию одновременно, мы можем упростить матрицу назначений воздействий до четырех блоков, каждый из которых представлен другой матрицей. Двигаясь вниз по матрице, мы перемещаемся вперед во времени. Тестовые блоки мы кладем в правую часть матрицы. В результате первый блок нашей матрицы (вверху слева) соответствует контрольным объектам до периода воздействия, второй блок (вверху справа) соответствует тестовым объектам до периода воздействия, третий блок (внизу слева) содержит контрольные объекты после периода воздействия, а четвертый блок (внизу справа) – тес товые объекты после периода воздействия. Индикатор воздействия равен нулю везде, кроме блока с тестовыми объектами после периода воздействия: Эта матрица назначений воздействия приведет к следующей матрице результатов: Снова обратите внимание на то, что период после воздействия находится внизу, а тестовые объекты – справа. В нашем примере, когда мы оцениваем эффект Положения 99, результатом Y являются продажи сигарет. Мы используем до и после для обозначения периода до и после воздействия соответственно, а также контр и тест для обозначения контрольного и тестового объектов соответственно. Мы будем использовать вышеприведенное матричное представление всякий раз, говоря об оценке синтетических контрольных весов, но есть и другое представление данных, которое полезно, особенно если мы говорим о методе разности разностей. В этом представлении у нас есть таблица с 5 столбцами:
468  Синтетическая разность разностей столбец с объектами, столбец с периодами времени, столбец результатов и два булевых столбца, обозначающих принадлежность к тестовому объекту (факт воздействия) и принадлежность к периоду после воздействия. Количество строк в этой таблице равно количеству объектов N, умноженному на количество периодов T. Ниже вы можете увидеть, как эта таблица выглядит для нашего примера с потреблением сигарет. import numpy as np import pandas as pd from toolz import curry, partial import seaborn as sns from matplotlib import pyplot as plt import statsmodels.formula.api as smf import cvxpy as cp import warnings warnings.filterwarnings('ignore') from matplotlib import style style.use("ggplot") pd.set_option('display.max_columns', 10) data = ( pd.read_csv("data/smoking.csv")[ ["state", "year", "cigsale", "california", "after_treatment"] ] .rename(columns={"california": "treated"}) .replace({"state": {3: "california"}}) ) data.head() data[(data['state'] == 'california') & (data['year'].between(1986, 1990))]
­ ­ Синтетическая разность разностей  469 Если мы хотим перейти от этой таблицы к матричному представлению, о котором говорили ранее, все, что нам нужно сделать, – это повернуть таб лицу по периоду времени (году) и объекту (штату). Мы будем переходить от одного представления к другому, поскольку одно представление удобнее для метода разности разностей (Difference-in-Differences, DiD, РР), а другое – для метода синтетического контроля (Syntetic Control, SC, CK). data_piv = data.pivot("year", "state", "cigsale") data_piv = data_piv.rename( columns={c: f"state_{c}" for c in data_piv.columns if c != "california"} ) data_piv.head()[["state_1", "state_2", "state_4", "state_38", "state_39", "california"]].round() Что касается потенциальных результатов, мы можем вернуться к матрице результатов, чтобы проанализировать поставленную здесь задачу анализа причинно-следственных связей. Поскольку воздействие распространяется на тестовый объект (объект, подвергаемый воздействию) только после периода воздействия, мы наблюдаем потенциальный результат Y0 в матрице результатов повсюду, кроме правого нижнего блока: Наша цель – оценить ATT = Y(1)после, тест – Y(0)после, тест . Для этого нам нужно каким-то образом оценить недостающий потенциальный результат Y(0)после, тест. Другими словами, нам нужно выяснить, что произошло бы с тес товым объектом в период после воздействия, если бы он не был подвергнут воздействию. Учитывая это, начать стоит с рассмотрения методов разности разностей и синтетического контроля. Сначала кажется, что эти методы поразному оценивают этот недостающий потенциальный результат. Их объединение кажется странным. Однако у обоих методов больше общего, чем может показаться.
470  Синтетическая разность разностей Ревизия метода разности разностей В главе, посвященной методу разности разностей, мы получили эффект воздействия, оценив следующую линейную модель: Yit = β0 + β1Postt + β2Treatedi + β3Treatedi Postt + eit, где Post – дамми-переменная периода времени, указывающая на то, что данный период наступил после воздействия, а Treated – дамми-переменная, указывающая на принадлежность объекта к тестовой группе. Если мы оценим эту модель применительно к Калифорнии, то получим оцененное значение ATT –27.34. Это означает, что благодаря Положению 99 потребление сигарет на душу населения снизилось на 27 пачек. did_model = smf.ols("cigsale ~ after_treatment*treated", data=data).fit() att = did_model.params["after_treatment[T.True]:treated[T.True]"] print("ATT по методу разности разностей: ", att.round(3)) ATT по методу разности разностей: -27.349 pre_year = data.query("~after_treatment")["year"].mean() post_year = data.query("after_treatment")["year"].mean() pre_control_y = did_model.params["Intercept"] post_control_y = did_model.params["Intercept"] + did_model.params[ "after_treatment[T.True]"] pre_treat_y = did_model.params["Intercept"] + did_model.params["treated[T.True]"] post_treat_y0 = post_control_y + did_model.params["treated[T.True]"] post_treat_y1 = post_treat_y0 + did_model.params[ "after_treatment[T.True]:treated[T.True]"] plt.plot([pre_year, post_year], [pre_control_y, post_control_y], color="C0", label="Контроль") plt.plot([pre_year, post_year], [pre_treat_y, post_treat_y0], color="C1", ls="dashed") plt.plot([pre_year, post_year], [pre_treat_y, post_treat_y1], color="C1", label="Калифорния") plt.vlines(x=1988, ymin=40, ymax=140, linestyle=":", lw=2, label="Положение 99", color="black") plt.title("Оценивание по методу разности разностей") plt.ylabel("Продажи сигарет") plt.legend();
Ревизия метода разности разностей  471 Однако эту оценку следует воспринимать с долей скеписиса. Мы знаем: метод разности разностей требует, чтобы тренд в контрольной группе был аналогичен тренду в группе объектов, получавших воздействие (тестовой группе), в предположении, что они не подверглись воздействию. Формально E[Y(0)после, контр – Y(0)до, контр] = E[Y(0)после, тест – Y(0)до, тест]. Это предположение не поддается проверке, но, взглянув на тренд Калифорнии (объекта, подвергнутого воздействию) и других штатов до воздействия, мы можем понять, насколько это предположение правдоподобно. В частности, мы видим, что тренд продаж сигарет (cigsale) в Калифорнии не параллелен трендам в других штатах, по крайней мере в периоды до воздействия. Продажи сигарет в Калифорнии снижаются быстрее, чем в среднем по контрольным штатам, даже до начала воздействия. Если этот тренд сохранится и после периода, предшествующего воздействию, то оценка по методу разности разностей будет смещена в сторону уменьшения, это означает, что истинный эффект на самом деле менее экстремален, чем тот, который мы оценили выше. plt.figure(figsize=(10,5)) plt.plot(data_piv.drop(columns=["california"]), color="C1", alpha=0.3) plt.plot(data_piv.drop(columns=["california"]).mean(axis=1), lw=3, color="C1", ls="dashed", label="Усредн. контроль") plt.plot(data_piv["california"], color="C0", label="Калифорния") plt.vlines(x=1988, ymin=40, ymax=300, linestyle=":", lw=2, label="Положение 99", color="black") plt.legend() plt.ylabel("Продажи сигарет") plt.title("Непараллельные тренды");
  ­ 472  Синтетическая разность разностей Проблема непараллельных трендов – это как раз тот момент, когда в модели синтетической разности разностей вступает в игру синтетический контроль. Но мы забегаем вперед. Независимо от того, что модель разности разностей является валидной моделью для вышеприведенных данных, интересен тот факт, что мы можем переписать ее, используя формулу модели с временными и индивидуальными фиксированными эффектами. Чтобы представить модель разности разностей в таком виде, мы берем усредненные значения объектов и периодов времени наряду с индикатором воздействия В этой формуле эффекты объектов отражают разницу констант (базовых уровней) для каждого объекта, а эффекты периодов времени отражают общий тренд для тестовых и контрольных объектов. Чтобы реализовать ее, мы можем либо добавить в модель дамми-переменные объектов и периодов времени, либо центрировать данные. В случае центрирования:  мы вычитаем из переменной результата значение результата, усредненное по объектам, и значение результата, усредненное по периодам времени;  мы вычитаем из переменной воздействия значение воздействия, усредненное по объектам, и значение воздействия, усредненное по периодам времени: – – Yit = Yit – Yi – Yt, – – Dit = D it – Di – Dt, – – где Yi – это среднее значение по всем периодам времени для объекта i и Yt – это среднее значение по всем объектам для периода времени t:
Ревизия метода синтетического контроля  473 После центрирования простая регрессия результата на индикатор воздействия (treat*post) даст нам оценку по методу разности разностей. @curry def demean(df, col_to_demean): return df.assign(**{col_to_demean: ( df[col_to_demean] - df.groupby("state")[col_to_demean].transform("mean") - df.groupby("year")[col_to_demean].transform("mean"))}) formula = f"""cigsale ~ treat""" mod = smf.ols(formula, data=data .assign(treat = data["after_treatment"]*data["treated"]) .pipe(demean(col_to_demean="treat")) .pipe(demean(col_to_demean="cigsale"))) mod.fit().summary().tables[1] Как видите, мы получаем точно такую же оценку параметра, что и ранее. В конце концов, оба подхода – это просто разные способы взглянуть на одну и ту же модель разности разностей. Однако эта формулировка гораздо интереснее для решения нашей задачи, потому что позволяет увидеть, как метод разности разностей на самом деле очень похож на синтетический контроль. Внимательно посмотрите на вышеприведенную формулу TWFE. Обратите внимание, что это регрессионная задача с эффектами объектов и периодов времени. Но обратите внимание, что при оптимизации целевой функции не используются веса весов. В этом и заключается главное отличие метода разности разностей от метода синтетического контроля, которое мы вскоре увидим. Ревизия метода синтетического контроля В канонической модели синтетического контроля мы находим веса объектов (штатов), которые минимизируют разницу между результатом для тестового
­ 474  Синтетическая разность разностей объекта до воздействия и средневзвешенным значением результатов для контрольных объектов до воздействия (в условиях отсутствия ковариат). Кроме того, мы задаем ограничения на весовые коэффициенты, дабы они были положительными и в сумме равнялись единице. Чтобы найти эти веса, мы решаем следующую оптимизационную задачу: при условии что ∑wi = 1 и wi > 0 ∀ i, где результат Yдо, контр – это матрица размером Тдо × Nконтр, в которой столбцы – объекты, а строки – периоды времени. wконтр – это вектор-столбец размером Nконтр с одной записью для каждого объекта. Наконец, – y до, тест – это вектор-столбец размером Tдо × 1, в котором каждая запись представляет собой усредненный по временным периодам результат для тестовых объектов до воздействия. Вот почему мы иногда называем синтетический контроль горизонтальной регрессией. В большинстве задач регрессии объектами являются строки матрицы, а здесь объектами становятся столбцы. Следовательно, мы регрессируем усредненный результат тестовых объектов на контрольные объекты. Как только мы найдем веса, которые решают вышеуказанную проблему, мы можем умножить их на результаты контрольных объектов для всех периодов времени и получим синтетический контроль для тестового объекта: yск = Yконтрŵ ск. Идея заключается в том, что yпост, ск является хорошей оценкой нашего недостающего потенциального результата Y(0)после, тест. Если это так, то ATT – это просто усредненный результат для тестовых объектов в период после воздействия минус усредненный результат для синтетического контроля тоже в период после воздействия: τ̂ = – y после, тест – – y после, ск. from sc import SyntheticControl sc_model = SyntheticControl() y_co_pre = data.query("~after_treatment").query( "~treated").pivot("year", "state", "cigsale") y_tr_pre = data.query( "~after_treatment").query("treated")["cigsale"] sc_model.fit(y_co_pre, y_tr_pre) sc_weights = pd.Series( sc_model.w_, index=y_co_pre.columns, name="sc_w" ) sc = data.query("~treated").pivot( "year", "state", "cigsale").dot(sc_weights)
Ревизия метода синтетического контроля  475 att = data.query("treated")["cigsale"][ sc.index > 1988].mean() - sc[sc.index > 1988].mean() print("SC ATT: ", round(att, 4)) SC ATT: -19.5136 Эта оценка намного меньше той, которую мы получили с помощью метода разности разностей. Синтетический контроль гораздо лучше учитывает непараллельные тренды в периоде до воздействия, поэтому он не подвержен такому же смещению, что и метод разности разностей. Скорее, процесс создания синтетического контроля обеспечивает параллельность трендов, по крайней мере в период до воздействия. В результате получаемая нами оценка гораздо меньше и более правдоподобна. Мы можем визуализировать этот процесс оценивания, построив график фактических результатов для Калифорнии наряду с результатами синтетического контроля. Кроме того, пунктирными линиями показаны средние значения после вмешательства как для Калифорнии, так и для синтетического контроля. Разница между этими линиями представляет собой оценку ATT. plt.plot(sc, label="Синтетический контроль") plt.plot(sc.index, data.query("treated")["cigsale"], label="Калифорния", color="C1") calif_avg = data.query("treated")["cigsale"][sc.index > 1988].mean() sc_avg = sc[sc.index > 1988].mean() plt.hlines(calif_avg, 1988, 2000, color="C1", ls="dashed") plt.hlines(sc_avg, 1988, 2000, color="C0", ls="dashed") plt.title("Оценивание SC") plt.ylabel("Продажи сигарет") plt.vlines(x=1988, ymin=40, ymax=140, linestyle=":", lw=2, label="Положение 99", color="black") plt.legend();
476  Синтетическая разность разностей Интересно отметить, что дополнительно мы можем переписать формулу модели синтетического контроля так, что она будет довольно схожа с формулой модели с временными и индивидуальными фиксированными эффектами (Two Way Fixed Effect – TWFE), которую мы использовали в рамках метода разности разностей: где веса ŵiск для контрольных объектов оцениваются, исходя из решения ранее рассмотренной оптимизационной задачи. Для тестового объекта (объекта, подвергнутого воздействию) веса будут просто равны 1/Nтест (используем равномерное взвешивание). Обратите внимание здесь на различие между синтетическим контролем и методом разности разностей. Во-первых, синтетический контроль добавляет в уравнение веса объектов ŵiск. Во-вторых, у нас есть фиксированные эффекты периодов времени βt, но отсутствуют фиксированные эффекты объектов αi, а также общая константа μ. Чтобы убедиться в фактической эквивалентности двух формул, приведем соответствующий программный код, который дает точно такую же оценку ATT. @curry def demean_time(df, col_to_demean): return df.assign(**{col_to_demean: ( df[col_to_demean] - df.groupby("year")[col_to_demean].transform("mean"))}) data_w_cs_weights = data.set_index( "state").join(sc_weights).fillna(1/len(sc_weights)) formula = f"""cigsale ~ -1 + treat""" mod = smf.wls( formula, data=data_w_cs_weights .assign(treat=data_w_cs_weights["after_treatment"] * data_w_cs_weights["treated"]) .pipe(demean_time(col_to_demean="treat")) .pipe(demean_time(col_to_demean="cigsale")), weights=data_w_cs_weights["sc_w"]+1e-10) mod.fit().summary().tables[1] Мы только что увидели, что эти два подхода, синтетический контроль (SC) и разность разностей (DiD), на самом деле тесно связаны. Теперь мы
Синтетическая разность разностей  477 можем поговорить о синтетической разности разностей. Как вы, наверное, догадались, мы просто добавим веса в формулу модели разности разностей или фиксированные эффекты объектов в формулу модели синтетического контроля. РАЗНОСТЬ РАЗНОСТЕЙ СИНТЕТИЧЕСКИЙ КОНТРОЛЬ Архангельский и коллеги Ты просто принял обе таблетки? Синтетическая разность разностей Прежде чем мы перейдем непосредственно к формуле синтетической разности разностей, позвольте мне воспроизвести ранее приводившиеся формулы синтетического контроля и разности разностей, что облегчит сравнение: Далее, как я и обещал, мы можем легко объединить два вышеприведенных уравнения в одно, которое будет содержать элементы обоих: Как видите, мы вернули фиксированные эффекты объектов αi. Кроме того, мы оставили веса объектов ŵi. Но появилось кое-что новенькое, а именно
478  Синтетическая разность разностей веса периодов времени λ̂ t. Не волнуйтесь, в них нет ничего сложного. Вспомним, как веса объектов wi минимизировали разницу между результатами контрольных объектов и усредненным результатом тестовых объектов? Другими словами, мы используем их для сопоставления предварительных трендов тестовой и контрольной групп. Веса периодов времени делают то же самое, но для периодов. Иными словами, они минимизируют разницу между результатом контрольной группы в период до воздействия и результатом контрольной группы в период после воздействия: при условии что ∑ λ i = 1 и λt > 0 ∀ t. Вновь Yдо, контр – это матрица размером Тдо × Nконтр, в которой строки – перио­ ды времени, столбцы – объекты. Однако теперь – y после, контр – это вектор-строка размером 1 × Nконтр, в которой каждый элемент представляет собой усредненный по времени результат для соответствующего контрольного объекта в период после воздействия. Наконец, λдо – это вектор-строка размером 1 × Tдо, содержащая по одному элементу для каждого периода до воздействия. Здесь отметим, что веса объектов w умножались на матрицу результатов Yдо, контрwконтр. Это означает, что мы регрессировали усредненный результат для каждого объекта тестовой группы на результататы объектов контрольной группы. Теперь мы ставим эту проблему с ног на голову, регрессируя результат, усредненный по всем периодам времени после воздействия для объекта контрольной группы, на результат того же самого контрольного объекта, но уже полученный в период до воздействия. Что касается весов периодов времени для периодов после воздействия, мы просто задаем их равными единице, деленной на количество периодов после воздействия 1/Tпосле (снова используем равномерное взвешивание). Обратите внимание, что у нас еще есть константа λ0. Мы включаем ее, поскольку допус­ каем, что результат в период после воздействия может быть выше или ниже результатов во всех периодах до воздействия, так часто бывает во многих задачах с явным положительным или отрицательным трендом. Если все это кажется немного абстрактным, возможно, программный код поможет вам понять, что происходит. def fit_time_weights(data, outcome_col, year_col, state_col, treat_col, post_col): control = data.query(f"~{treat_col}") # поворачиваем данные в матричное представление (T_до, N_контр) y_pre = (control .query(f"~{post_col}") .pivot(year_col, state_col, outcome_col)) # группируем период времени после воздействия по объектам, # чтобы получить вектор (1, N_контр)
Синтетическая разность разностей  479 y_post_mean = (control .query(f"{post_col}") .groupby(state_col) [outcome_col] .mean() .values) # добавляем в верхнюю часть матрицы вектор (1, N_контр), # состоящий из единиц, в качестве константы X = np.concatenate([np.ones((1, y_pre.shape[1])), y_pre.values], axis=0) # оцениваем веса периодов времени w = cp.Variable(X.shape[0]) objective = cp.Minimize(cp.sum_squares(w@X - y_post_mean)) constraints = [cp.sum(w[1:]) == 1, w[1:] >= 0] problem = cp.Problem(objective, constraints) problem.solve(verbose=False) # print("Intercept: ", w.value[0]) return pd.Series(w.value[1:], # удаляем константу name="time_weights", index=y_pre.index) Первое, что мы делаем в этом программном коде, – это оставляем только контрольную группу. Затем мы поворачиваем данные, охватывающие период до воздействия, так, чтобы получить матрицу Yдо, контр. Потом мы группируем данные после воздействия, чтобы получить средний результат для каждого контрольного объекта в период после воздействия. Далее в качестве константы мы добавляем строку, заполненную единицами, в верхнюю часть матрицы Yдо, контр. Наконец, мы регрессируем – y после, контр на периоды до воздействия (строки матрицы Yдо, контр), чтобы получить веса периодов времени λt. Обратите внимание, мы добавляем ограничения так, чтобы веса в сумме составляли 1 и были неотрицательными. Наконец, мы отбрасываем константу и сохраняем веса периодов времени в виде объекта Series библиотеки pandas. Ниже представлен результат, который мы получаем при выполнении вышеуказанного программного кода. Мы находим веса периодов времени, выясняя, как Положение 99 повлияло на продажи сигарет. Обратите внимание, что все периоды, кроме 1986, 1987 и 1988, получают нулевые веса. Это означает, что взвешенное среднее лишь последних трех периодов достаточно, чтобы сбалансировать периоды до и после воздействия. time_weights = fit_time_weights(data, outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") time_weights.round(3).tail() year
480  Синтетическая разность разностей 1984 -0.000 1985 -0.000 1986 0.366 1987 0.206 1988 0.427 Name: time_weights, dtype: float64 Чтобы чуть лучше понять роль этих весов, мы можем визуализировать выражение λ̂доYдо, контр + λ̂ 0 в виде горизонтальной линии, которая приходится на период до воздействия и не обнуляется. Рядом с ней мы визуализируем усредненный результат, полученный в период после воздействия. Обратите внимание, как они идеально выравниваются. Мы также показываем оцененные веса периодов времени в виде красных столбиков на оси x. fig = plt.figure() ax = fig.add_subplot(111) ax.plot(data.query("~treated").query( "~after_treatment").groupby("year")["cigsale"].mean()) ax.plot(data.query("~treated").query( "after_treatment").groupby("year")["cigsale"].mean()) intercept = -15.023877689807628 ax.hlines( ( data.query("~treated").query("~after_treatment").groupby( "year")["cigsale"].mean() * time_weights ).sum() - 15, 1986, 1988, color="C0", ls="dashed", label=""" $\lambda_{до} Y_{до, контр} + \lambda_0$""") ax.hlines( data.query("~treated").query("after_treatment").groupby( "year")["cigsale"].mean().mean(), 1988, 2000, color="C1", ls="dashed", label="""Средн. $Y_{после, контр}$""") ax.vlines(x=1988, ymin=90, ymax=140, linestyle=":", lw=2, label="Положение 99", color="black") plt.legend() plt.title("Балансировка по периодам времени") plt.ylabel("Продажи сигарет"); ax2 = ax.twinx() ax2.bar(time_weights.index, time_weights, label="$\lambda$") ax2.set_ylim(0,10) ax2.set_ylabel("Веса периодов времени");
Синтетическая разность разностей  481 Теперь, когда мы выяснили, что из себя представляют веса периодов времени λt в модели синтетической разности разностей и как происходит их оценивание, давайте обратим внимание на веса объектов wi. И нет, к сожалению, они не эквивалентны весам, которые мы использовали в традиционном методе синтетического контроля. Первое отличие между ними заключается в том, что мы допускаем использование константы w0. Мы делаем это, потому что нам уже не нужно, чтобы тестовый объект и синтетический контроль имели одинаковый уровень. Поскольку мы вводим модель разности разностей, нам лишь нужно, чтобы синтетический контроль и контрольный объект демонстрировали параллельные тренды. Следующее различие заключается в том, что мы добавляем L2-штраф к весам. Это помогает равномернее распределить ненулевые веса по контрольным объектам, вместо того чтобы лишь некоторые из них вносили вклад в синтетический контроль. L2-штраф гарантирует отсутствие очень больших весов, что заставляет нас использовать большее количество объектов. при условии что ∑wi = 1 и wi > 0 ∀ i. Здесь еще присутствует член ζ 2, который имеет теоретическое обоснование, но у него сложное объяснение, которое я, к сожалению, приводить здесь не буду. В разделе «Дополнительное чтение» вы найдете оригинальную статью, в которой дано объяснение ζ 2. Мы определяем его так: ζ = (Nтест ∗ Tпосле)1/4σ(∆it), где ∆it – это первая разность результатов Yit – Yi(t–1) и σ(∆it) – стандартное отклонение этой разности. Ниже приведен программный код для вычисления ζ.
482  Синтетическая разность разностей def calculate_regularization(data, outcome_col, year_col, state_col, treat_col, post_col): n_treated_post = data.query(post_col).query(treat_col).shape[0] first_diff_std = (data .query(f"~{post_col}") .query(f"~{treat_col}") .sort_values(year_col) .groupby(state_col) [outcome_col] .diff() .std()) return n_treated_post**(1/4) * first_diff_std Что касается весов объектов, то в них нет ничего особенно нового. Мы можем повторно использовать большую часть кода из функции для оценивания весов временных периодов fit_time_weights(). Нам только нужно быть осторожнее с размерностями, поскольку задача теперь перевернута с ног на голову. def fit_unit_weights(data, outcome_col, year_col, state_col, treat_col, post_col): zeta = calculate_regularization(data, outcome_col, year_col, state_col, treat_col, post_col) pre_data = data.query(f"~{post_col}") # поворачиваем данные в матричное представление (T_до, N_контр) y_pre_control = (pre_data .query(f"~{treat_col}") .pivot(year_col, state_col, outcome_col)) # группируем тестовые объекты по временным # периодам, чтобы получить вектор (T_до, 1) y_pre_treat_mean = (pre_data .query(f"{treat_col}") .groupby(year_col) [outcome_col] .mean()) # добавляем столбец (T_до, 1) в начало матрицы (T_до, N_контр), # чтобы он служил в качестве константы T_pre = y_pre_control.shape[0] X = np.concatenate([np.ones((T_pre, 1)), y_pre_control.values], axis=1) # оцениваем веса объектов. Обратите внимание на L2-штраф w = cp.Variable(X.shape[1]) objective = cp.Minimize(cp.sum_squares(X@w - y_pre_treat_mean.values) + T_pre*zeta**2 * cp.sum_squares(w[1:])) constraints = [cp.sum(w[1:]) == 1, w[1:] >= 0] problem = cp.Problem(objective, constraints)
Синтетическая разность разностей  483 problem.solve(verbose=False) # print("Intercept:", w.value[0]) return pd.Series(w.value[1:], # удаляем константу name="unit_weights", index=y_pre_control.columns) Сначала мы вычисляем ζ с помощью ранее написанной функции fit_unit_ weights() и отбираем лишь данные, приходящиеся на период до воздействия. Затем мы преобразуем данные, приходящиеся на период до воздействия, чтобы получить матрицу результатов – y до, тест. Потом мы добавляем столбец, состоящий из единиц, в начало матрицы – y до, тест. Этот столбец позволит нам оценить константу. С учетом всего этого мы задаем оптимизируемую целевую функцию, которая включает наложение L2-штрафа на веса. Наконец, мы отбрасываем константу и сохраняем оцененные веса в виде объекта Series библиотеки pandas. Давайте применим этот программный код для оценивания весов объектов, выяснив, как Положение 99 повлияло на продажи сигарет. Ниже приведен результат, который мы получим для первых 5 штатов: unit_weights = fit_unit_weights(data, outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") unit_weights.round(3).head() state 1 -0.000 2 -0.000 4 0.057 5 0.078 6 0.070 Name: unit_weights, dtype: float64 Эти веса объектов также определяют синтетический контроль, который мы можем визуализировать наряду с результатом для Калифорнии. Кроме того, мы построим график для модели традиционного синтетического контроля, которую оценивали ранее, наряду с моделью, которую построили только что, включая константу. Это даст нам некоторое представление о том, что происходит, и о разнице между тем, что мы только что сделали, и традиционным синтетическим контролем. intercept = -24.75035353644767 sc_did = data_piv.drop(columns="california").values @ unit_weights.values fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18,5)) ax1.plot(data_piv.index, sc_did,
­ 484  Синтетическая разность разностей label="Синтетический контроль (SDID)", color="C0", alpha=.8) ax1.plot(data_piv["california"], label="Калифорния", color="C1") ax1.vlines(x=1988, ymin=40, ymax=160, linestyle=":", lw=2, label="Положение 99", color="black") ax1.legend() ax1.set_title("Синтетический контроль (синтетическая разность разностей)") ax1.set_ylabel("Продажи сигарет"); ax2.plot(data_piv.index, sc_did+intercept, label="Синтетический контроль (синтетическая разность разностей) + $w_0$", color="C0", alpha=.8) ax2.plot(data_piv.index, sc, label="Традиционный синтетический контроль", color="C0", ls="dashed") ax2.plot(data_piv["california"], label="Калифорния", color="C1") ax2.vlines(x=1988, ymin=40, ymax=160, linestyle=":", lw=2, label="Положение 99", color="black") ax2.legend() ax2.set_title("Синтетический контроль (синтетическая разность разностей) и\n" "традиционный синтетический контроль"); На первом графике мы видим: очевидное различие заключается в том, что этот новый синтетический контроль уже не находится на том же уровне, что и Калифорния. Это потому, что мы включили константу, которая позволяет контрольному объекту быть на произвольно разном уровне по сравнению со своим синтетическим контролем. Этот новый синтетический контроль построен таким образом, чтобы иметь тот же тренд до воздействия, что и тес товый объект, но не обязательно на том же самом уровне. На втором графике мы сдвигаем этот новый синтетический контроль, добавляя обратно константу, которую мы ранее удалили. Он оказывается на уровне тестового объекта – Калифорнии. Для сравнения мы визуализировали ранее построенную модель традиционного синтетического контроля в виде
­ Синтетическая разность разностей  485 красной пунктирной линии. Обратите внимание, что линии синтетических контролей не одинаковы. Это различие обусловлено тем, что мы разрешили использование константы, а также из-за L2-штрафа, который сдвигает веса к нулю. Теперь, когда у нас есть веса периодов времени λ̂t и веса объектов ŵi, мы можем перейти к запуску DiD-составляющей модели синтетической разности разностей. Для работы с этой составляющей мы берем данные в формате таблицы, в которой каждая строка будет комбинацией периода T и объекта N. У нас будут столбцы, соответствующие штату, году, результату (продажам сигарет), индикатору периода после воздействия и индикатору тестового объекта. К этой таблице мы добавим веса временных периодов и веса объектов. Поскольку вес временного периода находится в серии с индексом периода времени, а вес объекта – в другой серии с индексом объекта, мы просто объединим все вместе. def join_weights(data, unit_w, time_w, year_col, state_col, treat_col, post_col): return ( data .set_index([year_col, state_col]) .join(time_w) .join(unit_w) .reset_index() .fillna( { time_w.name: 1 / len( pd.unique(data.query(f"{post_col}")[year_col]) ), unit_w.name: 1 / len( pd.unique(data.query(f"{treat_col}")[state_col]) ) } ) .assign(**{"weights": lambda d: (d[time_w.name] * d[unit_w.name]).round(10)}) .astype({treat_col: int, post_col: int})) Этот процесс объединения приведет к тому, что веса объектов в тестовой группе и веса периодов времени в период после воздействия получат пропус ки. К счастью, поскольку мы используем равномерное взвешивание в обоих случаях, заполнить эти пропуски довольно просто. Пропущенные значения весов периодов времени мы заполним средним значением дамми-переменной – индикатором периода после воздействия, которое будет равно 1/Tпосле. Пропущенные значения весов объектов мы заполним средним значением дамми-переменной – индикатором тестового объекта, которое будет равно 1/Nтест. Наконец, мы перемножаем эти веса. Вот результат, который мы получаем, применив функцию join_weights() к нашим данным:
486  Синтетическая разность разностей did_data = join_weights(data, unit_weights, time_weights, year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") did_data.head() data["after_treatment"].mean() 0.3870967741935484 1/len(data.query("after_treatment==1")["year"].unique()) 0.08333333333333333 Наконец, нам остается только оценить модель разности разностей с весами, которые мы только что задали. Оценка параметра, связанного со взаимодействием двух дамми-переменных, т. е. взаимодействием индикатора периода после воздействия и индикатора тестового объекта, будет оценкой модели синтетической разности разностей для ATT. did_model = smf.wls("cigsale ~ after_treatment*treated", data=did_data, weights=did_data["weights"]+1e-10).fit() did_model.summary().tables[1] Эта оценка намного меньше той, которую мы получили с помощью модели обычной разности разностей, но это неудивительно. Как мы уже обсуждали, оценка модели разности разностей, вероятно, смещена в данном случае, поскольку у нас есть веские причины сомневаться в предположении о параллельных трендах. Ответ на вопрос, почему оценка, получен-
Синтетическая разность разностей  487 ная с помощью модели синтетической разности разностей, меньше оценки, полученной с помощью модели традиционного синтетического контроля, вероятно, менее очевиден. Если мы вернемся и посмотрим на график синтетического контроля, мы увидим, что продажи сигарет в Калифорнии начали падать ниже уровня продаж в синтетическом контроле еще до принятия Положения 99. Это, вероятно, связано с тем, что традиционный синтетический контроль должен сопоставлять тестовые и контрольные объекты в течение всего периода до воздействия, что приводит к пропуску того или иного года. В модели синтетической разности разностей эта проблема менее актуальна, поскольку веса временных периодов позволяют нам сосредоточиться только на тех периодах, которые больше всего схожи с периодом после воздействия. В данном случае это как раз те три года, которые предшествовали принятию Положения 99. Чтобы понять, что делает модель синтетической разности разностей, мы можем визуализировать линии разности разностей для тестовой группы (Калифорния) и синтетического контроля на основе синтетической разности разностей. Обратите внимание, как мы проецируем тренд, который видим в синтетическом контроле, на тестовый объект, чтобы получить контрфактическое значение Y(0)тест, после. Разница между пунктирной фиолетовой линией и нижней сплошной фиолетовой линией – это ATT. Мы отрисовываем эти линии, начиная с 1987 года, чтобы показать, как веса временных периодов обнуляют все периоды, кроме 1986, 1987 и 1988. Веса временных периодов также показаны на маленьком графике внизу. avg_pre_period = (time_weights * time_weights.index).sum() avg_post_period = 1989 + (2000 - 1989) / 2 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15,8), sharex=True, gridspec_kw={'height_ratios': [3, 1]}) ax1.plot(data_piv.index, sc_did, label="Калифорния") ax1.plot(data_piv.index, data_piv["california"], label="Синтетический контроль (синтетическая\nразность разностей)") ax1.vlines(1989, data_piv["california"].min(), sc_did.max(), color="black", ls="dotted", label="Полож. 99") pre_sc = did_model.params["Intercept"] post_sc = pre_sc + did_model.params["after_treatment"] pre_treat = pre_sc + did_model.params["treated"] post_treat = post_sc + did_model.params[
488  Синтетическая разность разностей "treated"] + did_model.params["after_treatment:treated"] sc_did_y0 = pre_treat + (post_sc - pre_sc) ax1.plot([avg_pre_period, avg_post_period], [pre_sc, post_sc], color="C2") ax1.plot([avg_pre_period, avg_post_period], [pre_treat, post_treat], color="C2", ls="dashed") ax1.plot([avg_pre_period, avg_post_period], [pre_treat, sc_did_y0], color="C2") ax1.annotate('ATT', xy=(1995, 69), xytext=(1996, 66.5), fontsize=12, ha='center', va='bottom', bbox=dict(boxstyle='square', fc='white', color='k'), arrowprops=dict(arrowstyle='-[, widthB=1.5, lengthB=0.5', lw=2.0, color='k')) ax1.legend() ax1.set_title("Синтетическая разность разностей") ax1.set_ylabel("Продажи сигарет") ax2.bar(time_weights.index, time_weights) ax2.vlines(1989, 0, 1, color="black", ls="dotted") ax2.set_ylabel("Веса периодов времени") ax2.set_xlabel("Года"); Вышеприведенная модель оценивает ATT, который представляет собой эффект (влияние) Положения 99 на продажи сигарет в Калифорнии, усредненный по всем временным периодам после воздействия. Однако из вышеприведенного графика видно, что эффект увеличивается с течением времени. Что, если мы хотим учесть это? К счастью, это очень просто сделать.
Временная гетерогенность эффекта и постепенная адаптация  489 Прежде чем двигаться дальше, просто предупредим касательно вышеприведенной модели. Вам не следует доверять стандартным ошибкам или полагаться на доверительные интервалы регрессии, которую мы только что обучили. Они не отражают дисперсию при оценивании весов. Мы кратко обсудим, как правильно делать выводы, но сначала давайте посмотрим, как учитывать гетерогенность эффекта во времени (временную гетерогенность эффекта). Временная гетерогенность эффекта и постепенная адаптация К счастью, оценить один эффект для каждого временного периода с помощью модели синтетической разности разностей невероятно просто. Все, что нам нужно сделать, – это запустить модель несколько раз, по одному разу для каждого временного периода. Если говорить более точно, допустим, у нас есть следующая матрица распределения воздействия, в которой всего 4 временных периода и 3 объекта. Последний объект является тестовым (подвергнутым воздействию). Обучение модели синтетической разности разностей на вышеприведенной матрице даст нам значение ATT, усредненное по периодам 3 и 4. Чтобы оценить эффект для каждого периода индивидуально, просто разделим задачу на две части: по одной – для каждого периода после воздействия. Сначала мы обучаем модель синтетической разности разностей на данных, в которых мы оставляем только период после воздействия 3, а затем обучаем модель на данных, в которых оставляем только период после воздействия 4. Иными словами, мы запускаем модель синтетической разности разностей на каждой из следующих матриц по отдельности:
490  Синтетическая разность разностей Лучше всего сначала объединить все этапы, связанные с обучением модели синтетической разности разностей, в одну функцию. Речь идет об оценивании весов объектов и весов временных периодов и обучении модели разности разностей. def synthetic_diff_in_diff(data, outcome_col, year_col, state_col, treat_col, post_col): # находим веса объектов unit_weights = fit_unit_weights(data, outcome_col=outcome_col, year_col=year_col, state_col=state_col, treat_col=treat_col, post_col=post_col) # находим веса периодов времени time_weights = fit_time_weights(data, outcome_col=outcome_col, year_col=year_col, state_col=state_col, treat_col=treat_col, post_col=post_col) # добавляем веса к данным для модели разности разностей did_data = join_weights(data, unit_weights, time_weights, year_col=year_col, state_col=state_col, treat_col=treat_col, post_col=post_col) # запускаем модель разности разностей formula = f"{outcome_col} ~ {post_col}*{treat_col}" did_model = smf.wls(formula, data=did_data, weights=did_data["weights"]+1e-10).fit() return did_model.params[f"{post_col}:{treat_col}"] synthetic_diff_in_diff(data, outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") -15.60539723458704 Теперь, когда у нас есть возможность легко запустить модель синтетической разности разностей, мы можем запустить ее несколько раз, удаляя все периоды после воздействия, кроме периода, для которого хотим оценить эффект. effects = {year: synthetic_diff_in_diff( data.query(f"~after_treatment|(year=={year})"),
Временная гетерогенность эффекта и постепенная адаптация  491 outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") for year in range(1989, 2001)} effects = pd.Series(effects) plt.plot(effects); plt.ylabel("Влияние (эффект) на продажи сигарет") plt.title("Оценка эффекта (синтетическая\nразность разностей) по годам"); Как и ожидалось, эффект становится сильнее с течением времени. Он начинается с маленького значения, но постепенно увеличивается, и похоже, что в 2000 году будет уменьшение потребления на 25 пачек сигарет на душу населения. Удобно, что запуск нескольких моделей синтетической разности разностей также будет актуален для случая постепенной адаптации. В случае постепенной адаптации у нас есть несколько тестовых объектов, которые подвергаются воздействию в разные временные периоды. Давайте вернемся к нашей очень простой матрице назначения воздействия, с 3 объектами и 4 временными периодами. Допустим, объект 1 никогда не подвергается воздействию, объект 2 подвергается воздействию в периоде 4, а объект 3 подвергается воздействию в периоде 3. Это приведет к следующей матрице:
­ 492  Синтетическая разность разностей Обратите внимание, что модель синтетической разности разностей не может обработать эту матрицу, потому что у нас нет четкого определения периода до воздействия (время, предшествующее периоду 4, для объекта 2 или время, предшествующее периоду 3, для объекта 3) или четкого определения контрольного объекта (объект 2 может быть контрольным для воздействия начиная с периода 3). Ключевой момент в решении этой проблемы – понимание того, что мы можем удалить столбцы (объекты) или строки (периоды времени) в этой матрице, чтобы вернуться к дизайну блочного назначения. Например, мы можем создать из вышеприведенной матрицы две матрицы, в первой мы удалим 3-й временной период, а во второй мы удалим 4-й временной период: В результате мы получим две матрицы, а это значит, что мы можем запус тить модель синтетической разности разностей на обеих матрицах. Результатом будут две оценки ATT, которые затем можно объединить с помощью взвешенного среднего, где вес для каждой оценки – это доля тестовых объектов в соответствующей матрице. В первом блоке 2 объекта из трех подверглись воздействию, во втором блоке один объект из трех подвергся воздействию. В нашем примере вес для ATT1 будет равен 2/3, а вес для ATT2 будет равен 1/3. Как вариант мы также можем получить два блочных дизайна, удалив столбцы, что приведет к следующим матрицам: , где D1 соответствует объектам 1 и 2, а D2 – объектам 1 и 3. Поскольку мы уже видели, как строить модель синтетической разности разностей для разных временных периодов, давайте рассмотрим подход, в котором мы фильтруем объекты. Поскольку наши данные о влиянии По-
Временная гетерогенность эффекта и постепенная адаптация  493 ложения 99 на продажи сигарет изначально не предполагают дизайна постепенной адаптации, давайте просто симулируем его. Мы создадим 3 новых штата на основе наших данных и будем притворяться, что они приняли закон, аналогичный Положению 99, но в 1993 году. Возможно, власти этих штатов были впечатлены результатами Калифорнии и захотели принять тот же самый закон. Как только они это сделают, принятый закон будет уменьшать потребление сигарет на 3 % ежегодно. Мы можем визуализировать среднее потребление сигарет для этих новых штатов, чтобы лучше понять, что происходит. Черной пунктирной чертой отмечен год принятия этими штатами антитабачного закона. np.random.seed(1) n = 3 random_states = list(np.random.choice(data['state'].unique(), n)) tr_state = (data[data['state'].isin(random_states)] .assign(**{ "treated": True, "state": lambda d: "new_" + d["state"].astype(str), "after_treatment": lambda d: d["year"] > 1992 }) # эффект 3 % / год .assign(**{"cigsale": lambda d: np.random.normal( d["cigsale"] d["cigsale"]*(0.03*(d["year"] - 1992))*d["after_treatment"], 1)})) new_data = pd.concat([data, tr_state]).assign( **{"after_treatment": lambda d: np.where(d["treated"], d["after_treatment"], False)}) new_data_piv = new_data.pivot("year", "state", "cigsale") new_tr_states = list(filter(lambda c: str(c).startswith("new"), new_data_piv.columns)) plt.figure(figsize=(10,5)) plt.plot(new_data_piv.drop(columns=["california"]+new_tr_states), color="C1", alpha=0.3) plt.plot(new_data_piv.drop(columns=["california"]+new_tr_states).mean(axis=1), lw=3, color="C1", ls="dashed", label="Усредн. контроль") plt.plot(new_data_piv["california"], color="C0", label="Калифорния") plt.plot(new_data_piv[new_tr_states].mean(axis=1), color="C4", label="Новый тест. штат") plt.vlines(x=1988, ymin=40, ymax=300, linestyle=":", lw=2, label="Положение 99", color="black") plt.vlines(x=1992, ymin=40, ymax=300, linestyle="dashed", lw=2, label="Новый штат тест.", color="black")
494  Синтетическая разность разностей plt.legend() plt.ylabel("Продажи сигарет") plt.title("Две группы, испытавшие воздействие"); Наконец-то у нас появились данные о постепенной адаптации. Теперь нам нужно понять, как удалить некоторые штаты, чтобы разбить проблему на несколько случаев блочного назначения. Во-первых, мы можем сгруппировать штаты по времени принятия закона. Следующий программный код именно это и делает. assignment_blocks = (new_data.query("treated & after_treatment") .groupby("state")["year"].min() .reset_index() .groupby("year")["state"].apply(list).to_dict()) assignment_blocks {1989: ['california'], 1993: ['new_13', 'new_38', 'new_9']} Как видите, мы получили две группы штатов. Первая группа состоит только из Калифорнии, которая начала подвергаться воздействию с 1989 года, а вторая группа состоит из трех новых штатов, которые все были подвергнуты воздействию начиная с 1993 года. Теперь нам нужно запустить модель синтетической разности разностей для каждой из этих групп. Мы можем легко сделать это, оставив только контрольные объекты плюс одну из этих групп. Однако здесь есть подвох. Столбец after_treatment будет иметь разное значение в зависимости от того, какую группу мы анализируем. Если мы анализируем группу, содержащую только Калифорнию, значение after_treatment должно быть year >= 1989; если мы рассматриваем группу с новыми штатами, это значение должно быть year >= 1993. К счастью, это довольно легко учесть. Все, что нам нужно сделать, – это воссоздать значение after_treatment в каждой итерации.
­ ­ Оценивание плацебо-дисперсии  495 staggered_effects = {year: synthetic_diff_in_diff( new_data[(~new_data['treated']) | (new_data['state'].isin(states))] .assign(**{"after_treatment": lambda d: d["year"] >= year}), outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") for year, states in assignment_blocks.items()} staggered_effects {1989: -15.60539723458704, 1993: -17.249435402003723} Как неудивительно, но оценка ATT для первой группы, состоящей только из Калифорнии, точно такая же, что и оценка, которую мы видели раньше. Вторая оценка ATT – это оценка, которую мы получаем для новой группы штатов. Нам нужно объединить их в одну общую оценку ATT. Это можно сделать с помощью взвешенного среднего, о котором мы рассказали ранее. Сначала мы вычисляем количество элементов, испытавших воздействие (after_treatment & treated), в каждом блоке. Затем мы объединяем оценки ATT, используя эти веса. weights = {year: sum((new_data["year"] >= year) & (new_data["state"].isin(states))) for year, states in assignment_blocks.items()} att = sum([effect*weights[year]/sum(weights.values()) for year, effect in staggered_effects.items()]) print("веса: ", weights) print("ATT: ", att) Здесь у нас в общей сложности 36 случаев воздействия: обычные 12 перио дов после воздействия (1989–2000 гг.) для Калифорнии плюс 8 периодов пос ле воздействия (1993–2000 гг.) для каждого из трех новых тестовых штатов, которые мы задали. Исходя из этого, вес для первой оценки ATT составляет 12/36, а вес для второй оценки ATT составляет 24/36, что в совокупности дает вышеприведенный результат. Оценивание плацебо-дисперсии Эта глава становится немного слишком длинной, но есть одно обещание, которое мы до сих пор не выполнили. Помните, как мы говорили в самом начале, что метод синтетической разности разностей демонстрирует лучшее качество (меньшую погрешность) по сравнению с методом синтетического контроля? Причина в том, что фиксированные эффекты периодов времени и объектов в методе синтетической разности разностей в значительной мере улавливают дисперсию результата, что, в свою очередь, снижает дисперсию оценки.
496  Синтетическая разность разностей Конечно, я не буду просить вас принять мое слово на веру, поэтому далее мы покажем, как можно построить доверительный интервал для оценки, полученной с помощью модели синтетической разности разностей. Оказывается, существует множество решений этой проблемы, но только одно подходит для случая с одним тестовым объектом – нашего случая, когда только Калифорния была подвергнута воздействию. Идея заключается в проведении серии плацебо-тестов, в ходе которых мы притворяемся, что объект из контрольного пула подвергся воздействию (является тестовым), когда это на самом деле не так. Затем мы используем метод синтетической разности разностей для оценки ATT этого плацебо-теста и сохраняем его результат. Мы выполняем этот шаг несколько раз, выбирая каждый раз контрольный объект. В конце у нас будет массив значений ATT плацебо-тестов. Дисперсия этого массива – это плацебо-дисперсия оценки эффекта, полученной по методу синтетической разности разностей, которую мы можем использовать для построения доверительного интервала. Чтобы реализовать вышесказанное, первое, что нам нужно, – это функция, которая создает плацебо-данные. Эта функция удаляет тестовые объекты, выбирает один контрольный объект и изменяет значение столбца treated для этого контрольного объекта с 0 на 1. def make_random_placebo(data, state_col, treat_col): control = data.query(f"~{treat_col}") states = control[state_col].unique() placebo_state = np.random.choice(states) return control.assign(**{treat_col: control[state_col] == placebo_state}) np.random.seed(1) placebo_data = make_random_placebo(data, state_col="state", treat_col="treated") placebo_data.query("treated").tail()
Оценивание плацебо-дисперсии  497 В вышеприведенном примере мы отобрали штат 39 и теперь делаем вид, что он является тестовым объектом. Обратите внимание, что значение столбца treated сменилось на значение True. Следующее, что нам нужно, – это вычислить оценку по методу синтетической разности разностей с помощью плацебо-данных и повторить это несколько раз. Следующая функция как раз делает это. Она запускает функцию synthetic_diff_in_diff(), чтобы получить оценку по методу синтетической разности разностей, но вместо обычных данных мы передаем ей результат вызова функции make_random_placebo(). Мы делаем это несколько раз, чтобы получить массив оценок по методу синтетической разности разностей и, наконец, вычислить квадратный корень из дисперсии этого массива, который является обычным стандартным отклонением. from joblib import Parallel, delayed # для параллельной обработки def estimate_se(data, outcome_col, year_col, state_col, treat_col, post_col, bootstrap_rounds=400, seed=0, njobs=4): np.random.seed(seed=seed) sdid_fn = partial(synthetic_diff_in_diff, outcome_col=outcome_col, year_col=year_col, state_col=state_col, treat_col=treat_col, post_col=post_col) effects = Parallel(n_jobs=njobs)(delayed(sdid_fn)(make_random_placebo( data, state_col=state_col, treat_col=treat_col)) for _ in range(bootstrap_rounds)) return np.std(effects, axis=0) effect = synthetic_diff_in_diff(data, outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") se = estimate_se(data, outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") Стандартное отклонение можно использовать для построения доверительных интервалов, как мы описали в вышеприведенной формуле.
498  Синтетическая разность разностей print(f"Эффект: {effect}") print(f"Стандартная ошибка: {se}") print(f"90%-ный ДИ: ({effect-1.65*se}, {effect+1.65*se})") Эффект: -15.60539723458704 Стандартная ошибка: 9.912089736240306 90%-ный ДИ: (-31.96034529938354, 0.7495508302094631) Обратите внимание, что оценка ATT не является значимой в данном случае, но еще более интересно сравнить стандартную ошибку оценки, полученной по методу синтетической разности разностей, с оценкой, которую мы получаем с помощью традиционного синтетического контроля. def synthetic_control(data, outcome_col, year_col, state_col, treat_col, post_col): x_pre_control = (data .query(f"~{treat_col}") .query(f"~{post_col}") .pivot(year_col, state_col, outcome_col) .values) y_pre_treat_mean = (data .query(f"~{post_col}") .query(f"{treat_col}") .groupby(year_col) [outcome_col] .mean()) w = cp.Variable(x_pre_control.shape[1]) objective = cp.Minimize(cp.sum_squares(x_pre_control@w y_pre_treat_mean.values)) constraints = [cp.sum(w) == 1, w >= 0] problem = cp.Problem(objective, constraints) problem.solve(verbose=False) sc = (data .query(f"~{treat_col}") .pivot(year_col, state_col, outcome_col) .values) @ w.value y1 = data.query(f"{treat_col}").query(f"{post_col}")[outcome_col] att = np.mean(y1 - sc[-len(y1):]) return att def estimate_se_sc(data, outcome_col, year_col, state_col, treat_col, post_col, bootstrap_rounds=400, seed=0): np.random.seed(seed=seed) effects = [synthetic_control(make_random_placebo(data, state_col=state_col, treat_col=treat_col),
­ Оценивание плацебо-дисперсии  499 outcome_col=outcome_col, year_col=year_col, state_col=state_col, treat_col=treat_col, post_col=post_col) for _ in range(bootstrap_rounds)] return np.std(effects, axis=0) effect_sc = synthetic_control(data, outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") se_sc = estimate_se_sc(data, outcome_col="cigsale", year_col="year", state_col="state", treat_col="treated", post_col="after_treatment") print(f"Эффект: {effect_sc}") print(f"Стандартная ошибка: {se_sc}") print(f"90%-ный ДИ: ({effect_sc-1.65*se_sc}, {effect_sc+1.65*se_sc})") Эффект: -19.513629763998537 Стандартная ошибка: 11.241934948975807 90%-ный ДИ: (-38.06282242980862, -0.9644370981884585) Сейчас обратите внимание, что стандартная ошибка, полученная с помощью синтетического контроля, выше стандартной ошибки, полученной с помощью синтетической разности разностей. Это происходит потому, что синтетическая разность разностей улавливает значительную часть дисперсии результата благодаря фиксированным эффектам периодов времени и объектов. Таким образом, мы выполняем обещание, данное ранее. Однако прежде чем закончить, стоит упомянуть, что мы также можем использовать ту же самую процедуру для оценки дисперсии, чтобы построить доверительный интервал оцененного эффекта для каждого периода после воздейст вия. Все, что нам нужно сделать, – это запустить вышеприведенный код по одному разу для каждого временного периода. Просто имейте в виду, что выполнение этого кода может занять некоторое время, даже с учетом распараллеливания, которое мы здесь реализовали. standard_errors = {year: estimate_se( data.query(f"~after_treatment|(year=={year})"), outcome_col="cigsale", year_col="year", state_col="state",
500  Синтетическая разность разностей treat_col="treated", post_col="after_treatment") for year in range(1989, 2001)} standard_errors = pd.Series(standard_errors) plt.figure(figsize=(15,6)) plt.plot(effects, color="C0") plt.fill_between(effects.index, effects-1.65*standard_errors, effects+1.65*standard_errors, alpha=0.2, color="C0") plt.ylabel("Влияние (эффект) на продажи сигарет") plt.xlabel("Год") plt.title("90%-ный ДИ (синтетическая разность разностей)") plt.xticks(rotation=45); Ключевые идеи Синтетическая разность разностей (Synthetic-Diff-in-Diff, SDiD, СРР) черпает вдохновение как из метода разности разностей (Diff-in-Diff, DiD, РР), так и из метода синтетического контроля (Synthetic Control, SC, СК), используя преимущества обоих методов. Как и синтетический контроль, синтетическая разность разностей по-прежнему может работать с несколькими периодами, когда тренды в период, предшествующий воздействию, не являются параллельными. Однако, в отличие от синтетического контроля, синтетическая разность разностей оценивает веса объектов для построения контрольного объекта, результат которого параллелен только результату тестовой группы (при этом он не обязан совпадать с его уровнем). Из метода разности
­ Дополнительное чтение  501 разностей модель синтетической разности разностей берет фиксированные эффекты периодов времени и объектов, что помогает объяснить значительную часть дисперсии результата, а это, в свою очередь, снижает дисперсию оценки, полученной с помощью синтетической разности разностей. Синтетическая разность разностей также привносит некоторые новые идеи. Вопервых, в ходе оптимизации весов объектов применяется дополнительный L2-штраф, что делает веса более равномерно распределенными по контрольным объектам. Во-вторых, синтетическая разность разностей позволяет использовать константу (и, следовательно, экстраполяцию) при поиске таких весов. В-третьих, синтетическая разность разностей вводит использование весов для периодов времени, которые отсутствуют как в методе разности разностей, так и в методе синтетического контроля. По этой причине я бы не сказал, что модель синтетической разности разностей просто объединяет синтетический контроль и разность разностей. Скорее, она создает что-то новое, вдохновленное этими двумя подходами. Я также не могу сказать, что синтетическая разность разностей лучше или хуже, чем традиционный синтетический контроль. У каждого из них – разные свойства, которые могут быть уместными или нет, в зависимости от ситуации. Например, вы можете оказаться в ситуации, когда допускать экстраполяцию, полагаясь на синтетическую разность разностей, опасно. В этом случае синтетический контроль может стать хорошей альтернативой. Дополнительное чтение Эта глава по сути является пояснением к статье «Synthetic Difference in Diffe rences» (2019) (https://www.aeaweb.org/articles?id=10.1257/%20aer.20190159), написанной Дмитрием Архангельским (Dmitry Arkhangelsky), Сьюзан Эти (Susan Athey), Дэвидом А. Хиршбергом (David A. Hirshberg), Гвидо В. Имбенсом (Guido W. Imbens) и Стефаном Вагером (Stefan Wager). Кроме того, я хотел бы поблагодарить Масу Асами (Masa Asami) за его реализацию синтетической разности разностей на Python, pysynthdid (https://github.com/MasaAsami/pysynthdid). Его программный код помог мне убедиться в отсутствии ошибок в моем коде, что было очень полезно.
Приложение 1 Устранение смещения с помощью ортогонализации Ранее мы разобрались, как можно оценить качество причинно-следственной модели. Само по себе это огромный подвиг. Причинно-следственные модели оценивают эластичность , которая является ненаблюдаемой величиной. Поскольку мы не могли наблюдать фактические значения показателя, который наша модель должна была оценить, нам пришлось очень творчески подойти к оценке ее качества. Метод, показанный в предыдущей главе, в значительной степени опирался на данные, в которых воздействие было назначено случайным образом. Идея в виде коэффициента заключалась в том, чтобы оценить эластичность линейной регрессии одной переменной y ~ t. Однако это работает только в том случае, если воздействие назначено случайно. В противном случае мы получим проблемы из-за смещения, вызванного опущенной переменной. Чтобы обойти эту проблему, нам нужно, чтобы данные выглядели так, как будто воздействие назначено случайно. Я бы сказал, что существует два основных способа сделать это. Один из них – использование оценки склонности, а другой – ортогонализация. О последнем мы расскажем в этом приложении. И последнее слово предостережения, прежде чем мы продолжим. Я бы сказал, что, вероятно, самый безопасный способ получить неслучайные данные – это пойти и провести какой-нибудь эксперимент по сбору случайных данных. Я сам не очень доверяю методам устранения смещения, потому что
­ Перерождение линейной регрессии  503 никогда нельзя знать, учли ли вы все факторы, влияющие на случайность. Однако ортогонализацию все же стоит изучить. Это невероятно мощная техника, которая станет основой многих будущих причинно-следственных моделей. Перерождение линейной регрессии Идея ортогонализации опирается на теорему, разработанную в 1933 году тремя эконометристами – Рагнаром Фришем, Фредериком В. Во и Майклом К. Ловеллом. Проще говоря, она гласит, что любую модель множественной линейной регрессии можно разложить на три этапа, или модели. Допустим, ваши признаки записаны в матрице X. Теперь вы разбиваете эту матрицу таким образом, что получаете первый набор признаков X1 и второй набор признаков X2. На первом этапе мы берем первый набор признаков и оцениваем следующую линейную регрессионную модель yi = θ0 + θ1X1i + ei, где θ1 – вектор параметров. Затем мы берем остатки этой модели y* = yi – (θˆ0 + θˆ1 X1i). На втором этапе мы снова берем первый набор признаков, но теперь обучаем модель, в которой оцениваем второй набор признаков X2i = γ0 + γ1 X1i + ei. Здесь мы используем первый набор признаков для прогнозирования второго набора признаков. Наконец, мы вновь получаем остатки для этого второго этапа: X 2i* = X2i – (γ̂0 + γ̂1 X1i ). В итоге мы берем остатки, полученные на первом и втором этапах, и оцениваем следующую модель: yi* = β0 + β2 X 2i* + ei. Теорема Фриша–Во–Ловелла утверждает, что оценка параметра βˆ2, полученная в результате оценивания этой модели, эквивалентна оценке параметра, которую мы получим при обучении полной регрессии со всеми признаками: yi = β0 + β1 X 1i + β2 X 2i + ei.
504  Устранение смещения с помощью ортогонализации ХОРОШО. Давайте разберемся с этим немного подробнее. Мы знаем, что регрессия – это особая модель. Каждый ее параметр имеет интерпретацию частной производной: насколько увеличится значение результата Y, если я увеличу признак на единицу своего измерения, притом что остальные признаки остаются неизменными. Это очень удобно для анализа причинно-следственных связей, потому что позволяет контролировать переменные в ходе анализа, даже если эти переменные не были зафиксированы во время сбора данных. Мы также знаем, что если мы опускаем переменные в регрессии, то получаем смещение. Точнее, мы получаем смещение, вызванное опущенной переменной (или смещение, вызванное спутывающим фактором). Тем не менее теорема Фриша–Во–Ловелла говорит о том, что я могу разбить свою регрессионную модель на две части, ни одна из которых не будет содержать полного набора признаков, и все равно получу ту же оценку, которую я получил бы, обучив регрессию со всеми признаками. Помимо этого, теорема еще дает некоторое представление о том, что именно делает линейная регрессия. Чтобы получить коэффициент одной переменной Xk, регрессия сначала использует все остальные переменные для прогнозирования Xk и берет остатки. Это позволяет «очистить» Xk от любого влияния этих переменных. Таким образом, когда мы пытаемся понять влияние переменной Xk на Y, оно будет свободно от смещения, вызванного опущенной переменной. Во-вторых, регрессия использует все остальные переменные для прогнозирования Y и берет остатки. Это «очищает» Y от любого влияния этих переменных, уменьшая дисперсию Y так, чтобы было легче понять, как Xk влияет на Y. Я знаю, что вам, возможно, трудно оценить, насколько это здорово. Но вспомните, что делает линейная регрессия. Она оценивает влияние X2 на y, учитывая при этом X1. Это невероятно мощный инструмент для анализа причинно-следственных связей. Это говорит о том, что я могу построить модель, предсказывающую воздействие t с помощью моих признаков X, модель, предсказывающую результат y с помощью тех же самых признаков, взять
Интуиция, лежащая в основе ортогонализации  505 остатки обеих моделей и обучить модель, которая оценивает, как остаток от t влияет на остаток от y. Эта последняя модель скажет мне, как t влияет на y, при этом контролируя X. Другими словами, первые две модели контролируют спутывающие переменные. Они генерируют данные, которые так же хороши, как и случайные. Это и есть устранение смещения из данных. Это то, что мы используем в итоговой модели, которая оценивает эластичность. Существует (не очень сложное) математическое доказательство того, почему это так, но я думаю, что интуиция, лежащая в основе данной теоремы, настолько проста, что мы можем перейти непосредственно к ней. Интуиция, лежащая в основе ортогонализации Давайте снова возьмем наши данные о ценах. Но теперь мы возьмем только ту выборку, в которой цены были назначены неслучайно. Снова разделим данные на обучающий и тестовый наборы. Поскольку мы будем использовать тестовый набор для оценки нашей причинно-следственной модели, давайте выясним, как можно воспользоваться ортогонализацией для устранения смещения. import pandas as pd import numpy as np from matplotlib import pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split import statsmodels.formula.api as smf import statsmodels.api as sm from nb21 import cumulative_elast_curve_ci, elast, cumulative_gain_ci prices = pd.read_csv("data/ice_cream_sales.csv") train, test = train_test_split(prices, test_size=0.5) train.shape, test.shape ((5000, 5), (5000, 5)) Если мы выведем корреляции для тестового набора, то увидим, что цена положительно коррелирует с продажами, а это значит, что продажи должны расти, когда мы повышаем цены. Это очевидная чепуха. Люди не покупают больше, если мороженое стоит дорого. Вероятно, у нас есть какое-то смещение в данных. test.corr()
506  Устранение смещения с помощью ортогонализации Если мы визуализируем наши данные, то увидим, почему так происходит. В выходные дни (суббота и воскресенье) цена выше, но и продажи тоже выше. Мы видим, что это действительно так, потому что облако точек, соответствующее выходным дням, находится в правой верхней части графика. Вероятно, выходные дни играют важную роль в смещении. По выходным продается больше мороженого, потому что спрос на него выше. В ответ на этот спрос цены растут. Так что дело не в том, что рост цен вызывает рост продаж. Просто и продажи, и цены вырастают по выходным. np.random.seed(123) sns.scatterplot(data=test.sample(1000), x="price", y="sales", hue="weekday"); Для анализа этого набора данных нам понадобятся две модели. Первая модель, назовем ее Mt(X), прогнозирует воздействие (цену в нашем случае) с помощью спутывающих переменных. Это один из этапов, который мы рассмотрели выше, согласно теореме Фриша–Во–Ловелла. m_t = smf.ols("price ~ cost + C(weekday) + temp", data=test).fit() debiased_test = test.assign( **{"price-Mt(X)":test["price"] - m_t.predict(test)} )
Интуиция, лежащая в основе ортогонализации  507 Обучив эту модель, мы вычисляем остатки: t̂ i = ti – Mt (Xi ). Вы можете рассматривать эти остатки как вариант воздействия, который является несмещенным или, что еще лучше, который невозможно предсказать на основе спутывающих переменных X. Поскольку спутывающие переменные уже использовались для прогнозирования t, остатки по определению невозможно спрогнозировать с помощью X. Можно еще сказать, что смещение было объяснено моделью Mt(Xi ), порождающей t̂ i, которая ведет себя так же, как случайно назначенное воздействие. Конечно, это работает только в том случае, если в X входят все спутывающие факторы, которые влияют и на T, и на Y. Мы также можем визуализировать эти данные, чтобы увидеть, как они выглядят. np.random.seed(123) sns.scatterplot(data=debiased_test.sample(1000), x="price-Mt(X)", y="sales", hue="weekday") plt.vlines(0, debiased_test["sales"].min(), debiased_test["sales"].max(), linestyles='--', color="black"); Мы видим, что точки, соответствующие выходным дням, больше не находятся в правом верхнем углу. Они сдвинуты в центр. Более того, мы больше не можем выявить разные уровни цены (воздействия), пользуясь делением на будние и выходные дни. Можно сказать, что остаток price – Mt(X), отло-
508  Устранение смещения с помощью ортогонализации женный по оси x, представляет собой «случайный» или лишенный смещения вариант исходного воздействия. Одного этого достаточно, чтобы устранить смещение из набора данных. Это новое воздействие, которое мы создали, так же хорошо, как и случайно назначенное воздействие. Но мы можем предпринять еще одну процедуру, чтобы сделать набор данных еще лучше. А именно мы можем вычислить остатки для результата ŷi = yi – My(Xi ). Это еще один этап из теоремы Фриша–Вога–Ловелла. Он не делает набор данных менее смещенным, но облегчает оценку эластичности за счет уменьшения дисперсии в y. И снова вы можете представить ŷi как вариант yi, которую невозможно спрогнозировать с помощью X или у которой объяснены все дисперсии, обусловленные X. Подумайте об этом. Мы уже использовали X, чтобы спрогнозировать y с помощью модели My(Xi ). А ŷi – это ошибка данного прогноза. Поэтому по определению ее невозможно предсказать с помощью X. Вся информация в X, необходимая для прогнозирования y, уже использована. Если это так, то единственное, что остается для объяснения остатка ŷi, – это то, что мы не использовали для его вычисления (не включили в X), а это может быть только воздействие (опять же предполагая отсутствие неизмеряемых спутывающих факторов). m_y = smf.ols("sales ~ cost + C(weekday) + temp", data=test).fit() debiased_test = test.assign( **{"price-Mt(X)":test["price"] - m_t.predict(test), "sales-My(X)":test["sales"] - m_y.predict(test)} ) После того как мы выполним оба преобразования, выяснится, что будние дни не только не предсказывают остатки цен, но и не могут предсказать остатки продаж ŷ. Единственное, что остается для предсказания этих остатков, – это воздействие. Также обратите внимание на кое-что интересное. На графике выше было трудно определить направление ценовой эластичности. Казалось, что продажи снижаются по мере роста цен, но дисперсия продаж была настолько велика, что трудно было утверждать это наверняка. Теперь, когда мы строим график двух остатков ti – Mt (Xi ) и yi – My(Xi ), становится совершенно ясно, что продажи действительно вызывают снижение цен. np.random.seed(123) sns.scatterplot(data=debiased_test.sample(1000), x="price-Mt(X)", y="sales-My(X)", hue="weekday") plt.vlines(0, debiased_test["sales-My(X)"].min(),
­ Интуиция, лежащая в основе ортогонализации  509 debiased_test["sales-My(X)"].max(), linestyles='--', color="black"); Небольшим недостатком данных, из которых удалено смещение, является иной масштаб значений. В результате трудно понять, что эти значения означают (что такое ценовой остаток –3?). Тем не менее я считаю, что это небольшая цена, которую мы платим за удобство получения случайных данных на основе данных, которые изначально не были случайными. Подводя итог, можно сказать, что, предсказывая воздействие, мы построи ли модель, которая работает как несмещенный вариант воздействия; предсказывая результат, мы построили модель, являющуюся вариантом результата, который можно объяснить только в том случае, если мы используем воздействие. Эти данные, в которых мы заменили y на ŷ и t на t̂, и есть те самые несмещенные данные, которые мы хотели получить. Мы можем использовать их для оценки нашей причинно-следственной модели точно так же, как мы делали это ранее, используя случайные данные. Чтобы убедиться в этом, давайте еще раз построим причинно-следственную модель для ценовой эластичности, используя обучающие данные. m3 = smf.ols( f"sales ~ price*cost + price*C(weekday) + price*temp", data=train ).fit() Затем мы получим прогнозы эластичности для тестового набора. def predict_elast(model, price_df, h=0.01): return (model.predict(price_df.assign(price=price_df["price"]+h)) - model.predict(price_df)) / h
510  Устранение смещения с помощью ортогонализации debiased_test_pred = debiased_test.assign(**{ "m3_pred": predict_elast(m3, debiased_test), }) debiased_test_pred.head() Теперь, когда нужно построить график накопленной эластичности, мы по-прежнему упорядочиваем набор данных по спрогнозированной эластичности, но теперь для получения этой эластичности мы используем варианты воздействия и результата, из которых удалено смещение. Это эквивалентно оцениванию β1 в следующей регрессионной модели: ŷi = β0 + β1t̂ i + ei, в которой остатки выглядят так, как мы описывали ранее. plt.figure(figsize=(10,6)) cumm_elast = cumulative_elast_curve_ci( debiased_test_pred, "m3_pred", "sales-My(X)", "price-Mt(X)", min_periods=50, steps=200 ) x = np.array(range(len(cumm_elast))) plt.plot(x/x.max(), cumm_elast, color="C0") plt.hlines( elast(debiased_test_pred, "sales-My(X)", "price-Mt(X)"), 0, 1, linestyles="--", color="black", label="Средн. эласт." ) plt.xlabel("% от топ эласт. покупателей") plt.ylabel("Эластичность топ %") plt.title("Накопленная эластичность") plt.legend();
Интуиция, лежащая в основе ортогонализации  511 Разумеется, то же самое мы можем проделать и с кривой накопленного выигрыша. plt.figure(figsize=(10,6)) cumm_gain = cumulative_gain_ci( debiased_test_pred, "m3_pred", "sales-My(X)", "price-Mt(X)", min_periods=50, steps=200 ) x = np.array(range(len(cumm_gain))) plt.plot(x/x.max(), cumm_gain, color="C1") plt.plot( [0, 1], [0, elast(debiased_test_pred, "sales-My(X)", "price-Mt(X)")], linestyle="--", label="Случайная модель", color="black" ) plt.xlabel("% от топ эласт. покупателей") plt.ylabel("Накопленный выигрыш") plt.title("Накопленный выигрыш на несмещенной выборке") plt.legend();
512  Устранение смещения с помощью ортогонализации Обратите внимание, насколько эти графики похожи на графики из предыдущей главы. Это свидетельствует о том, что процедура удаления смещения здесь поработала на славу. Для сравнения давайте посмотрим, как выглядел бы график накопленного выигрыша, если бы мы использовали исходные, смещенные данные. plt.figure(figsize=(10,6)) cumm_gain = cumulative_gain_ci( debiased_test_pred, "m3_pred", "sales", "price", min_periods=50, steps=200 ) x = np.array(range(len(cumm_gain))) plt.plot(x/x.max(), cumm_gain, color="C1") plt.plot( [0, 1], [0, elast(debiased_test_pred, "sales", "price")], linestyle="--", label="Случайная модель", color="black" ) plt.xlabel("% от топ эласт. покупателей") plt.title("Накопленные выигрыши на смещенной выборке") plt.ylabel("Накопленные выигрыши") plt.legend();
­ Ортогонализация с помощью машинного обучения  513 Первое, что вы должны заметить, – средняя эластичность увеличивается, а не уменьшается. Мы уже видели это раньше. В смещенных данных кажется, что продажи растут по мере роста цены. В результате конечная точка на графике накопленного выигрыша становится положительной. В этом нет никакого смысла, поскольку мы знаем, что люди не покупают больше мороженого при увеличении цены на него. Если средняя ценовая эластичность уже искажена, то любое ее упорядочивание также не имеет смысла. Итог таков: эти данные не следует использовать для оценки модели. Ортогонализация с помощью машинного обучения В статье 2016 года Виктор Черножуков совместно с коллегами показал, что ортогонализацию можно делать и с помощью моделей машинного обучения. Очевидно, что это очень молодое научное направление, и нам еще предстоит многое узнать о том, что можно и чего нельзя делать с помощью моделей машинного обучения. Тем не менее это очень интересная идея, о которой стоит знать. Основные моменты практически не отличаются от тех, что мы уже рассмот рели. Единственное отличие заключается в том, что теперь для устранения смещения мы используем модели машинного обучения:
­ 514  Устранение смещения с помощью ортогонализации ŷi = yi – My(Xi ), t̂ i = ti – Mt (Xi ). Однако здесь есть одна загвоздка. Как мы прекрасно знаем, модели машинного обучения настолько мощные, что могут выполнить идеальную подгонку обучающих данных, т. е. они могут быть переобученными. Просто взглянув на уравнения выше, мы можем понять, что произойдет в этом случае. Если My каким-то образом переобучается, то все остатки будут очень близки к нулю. Если это произойдет, будет трудно выяснить, как t влияет на это. Аналогично если Mt каким-то образом переобучается, то ее остатки также будут близки к нулю. Следовательно, в остатках от воздействия не будет дисперсии, чтобы понять, как воздействие может повлиять на результат. Чтобы учесть это, нам нужно выполнить разбиение выборки. То есть мы оцениваем модель на одной части набора данных и делаем прогнозы на другой части. Самый простой способ сделать это – разделить выборку пополам, построить две модели таким образом, чтобы каждая из них оценивалась на одной половине набора данных и делала прогнозы на другой. Несколько более элегантная реализация использует k-блочную перекрестную проверку. Преимущество заключается в том, что мы можем обучить все модели на выборке, которая больше тестового набора. К счастью, получение прогнозов с помощью перекрестной проверки очень легко реализовать, воспользовавшись функцией cross_val_predict() библио теки scikit-learn. from sklearn.model_selection import cross_val_predict from sklearn.ensemble import RandomForestRegressor X = ["cost", "weekday", "temp"] t = "price" y = "sales" folds = 5 np.random.seed(123) m_t = RandomForestRegressor(n_estimators=100) t_res = test[t] - cross_val_predict(m_t, test[X], test[t], cv=folds) m_y = RandomForestRegressor(n_estimators=100) y_res = test[y] - cross_val_predict(m_y, test[X], test[y], cv=folds)
Ортогонализация с помощью машинного обучения  515 Теперь, когда у нас есть остатки, давайте сохраним их в виде столбцов в новом наборе данных. ml_debiased_test = test.assign(**{ "sales-ML_y(X)": y_res, "price-ML_t(X)": t_res, }) ml_debiased_test.head() Наконец, мы можем визуализировать набор данных, очищенный от смещения. np.random.seed(123) sns.scatterplot(data=ml_debiased_test.sample(1000), x="price-ML_t(X)", y="sales-ML_y(X)", hue="weekday"); И снова мы обнаружили отрицательную эластичность цены по продажам (увеличение цены приводит к снижению объема продаж). На самом деле график невероятно похож на тот, который мы получили, используя простую линейную регрессию. Но это, вероятно, обусловлено тем, что мы взяли очень простой набор данных. Преимущества ортогонализации с помощью машин-
516  Устранение смещения с помощью ортогонализации ного обучения заключаются в том, что она может оценивать более сложные функции. Она может выучивать такие взаимодействия и нелинейности, которые трудно ввести в модель линейной регрессии. Кроме того, некоторые модели машинного обучения (основанные на деревьях решений) гораздо проще в обучении, чем линейная регрессия. Они могут обрабатывать категориальные данные, выбросы и даже пропущенные данные – то, что потребовало бы определенного внимания, если бы вы просто использовали линейную регрессию. Наконец, прежде чем мы закончим, я хотел бы рассказать об одной распространенной ошибке, которую часто допускают специалисты по data science, когда знакомятся с этой идеей (сам проходил через это). Если воздействие или результат является бинарным, можно подумать, что лучше заменить регрессионные модели машинного обучения на их классификационные версии. Однако это не работает. Теория ортогонализации работает только для регрессионных моделей, аналогично тому, что мы разбирали ранее, рассказывая об инструментальных переменных. Честно говоря, не то чтобы модель потерпит неудачу, если вы замените регрессию на классификацию, но я бы не советовал этого делать. Если теория не оправдывает этого шага, зачем рисковать? Ключевые идеи Мы начали это приложение с того, что подчеркнули необходимость случайного назначения воздействия для надежной работы методов, оценивающих качество причинно-следственных моделей. Проблема возникает в том случае, если случайные данные нам недоступны. Для ясности: самое безопасное решение в этом случае – пойти и провести несколько экспериментов, чтобы получить случайные данные. Если об этом не может быть и речи, то в этом случае мы можем прибегнуть к умной альтернативе: преобразовываем наши данные таким образом, чтобы они выглядели так, как будто воздействие было назначено случайно. В этом приложении мы рассмотрели такой способ, используя принципы ортогонализации. Во-первых, мы построили модель, которая использует наши признаки X для прогнозирования воздействия t и получения его остатков. Идея заключается в том, что остатки от воздействия, по определению, не зависят от признаков, использованных для их вычисления. Другими словами, остатки от воздействия ортогональны признакам. Мы можем рассматривать эти остатки как вариант воздействия, в котором устранено спутывающее смещение, возникающее из-за X. Одного этого достаточно, чтобы наши данные выглядели не хуже случайных. Но мы можем пойти еще на один шаг дальше. Мы можем построить модель, которая предсказывает результат y, используя признаки X, но не воздействие, и также получить ее остатки. Опять же, интуиция очень похожа. Эти остатки – вариант результата, в котором объяснена вся дисперсия,
­ Дополнительное чтение  517 связанная с признаками. Данный факт, как мы надеемся, устранит большую часть дисперсии, что позволит легче оценить эффект воздействия. В нашем случае мы используем ортогонализацию с целью устранения смещения из данных для оценки качества модели. Однако этот метод используется и для других целей. А именно многие модели причинно-следственного вывода используют ортогонализацию в качестве первого шага предварительной обработки, чтобы упростить задачу модели причинно-следственного вывода. Можно сказать, что ортогонализация лежит в основе многих современных алгоритмов анализа причинно-следственных связей. СОВРЕМЕННЫЙ ПРИЧИННО-СЛЕДСТВЕННЫЙ АНАЛИЗ ОРТОГОНАЛИЗАЦИЯ Дополнительное чтение Материал, который я написал в этом приложении, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю этот материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. Эта глава основана на работе Виктора Черножукова (Victor Chernozhukov) и коллег «Double/Debiased Machine Learning for Treatment and Causal Parame ters» 2016 года, https://arxiv.org/abs/1608.00060. Вы также можете ознакомиться с оригинальной статьей Рагнара Фриша (Ragnar Frisch) и Фредерика Во (Frederick Waugh) «Partial Time Regressions as Compared with Individual Trends», написанной в 1933 году, https://www.sv.uio.no/econ/om/nobelprisvinnere/ragnarfrisch/published-scientific-work/rf-published-scientific-works/rf1933f.pdf. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Приложение 2 Устранение смещения с помощью оценки склонности Ранее мы выяснили, как из смещенного набора данных получить такой набор, в котором воздействие будет выглядеть случайно назначенным. Для этого мы использовали ортогонализацию. Этот метод основан на прогнозировании воздействия и результата, а затем замене обоих на остатки от их прогнозов: t* = t – Mt (X), y* = y – My(X). Это сам по себе мощный метод. Он работает как для непрерывного, так и для бинарного воздействия. Теперь мы рассмотрим другой метод, основанный на оценке склонности. Поскольку оценка склонности лучше всего работает, когда воздействие является бинарным или категориальным, этот метод устранения смещения также будет работать только для категориальных или бинарных воздействий. Однако в некоторых ситуациях она может быть более надежной, чем ортогонализация, и ее стоит изучить. Но сначала нам нужно немного изменить контекст. Мы говорили о ценах на мороженое, примере непрерывного воздействия. Теперь мы рассмотрим маркетинговые письма, бинарное воздействие. Здесь отметим, что вполне допустимо дискретизировать непрерывное воздействие на интервалы, чтобы оно выглядело категориальным (например, можно взять цену, которая является непрерывной, и дискретизировать ее на интервалы по 2.00 реала как [2.00, 4.00, 6.00, 8.00]). Вернемся к письмам. Ситуация следующая. Вы работаете в финансовой компании (у меня не очень креативные примеры, извините), которая предоставляет финансовые продукты, такие как страхование жизни, сберегательный счет, инвестиционный счет и т. д. Компания запускает новую услугу финансового консультиро-
Дополнительное чтение  519 вания и хочет прорекламировать ее своим клиентам. Для этого маркетинговая команда опробовала три разных письма: em1, em2 и em3. Поскольку маркетологи хорошо образованны в области статистики и планирования экспериментов, они не просто отправляют письма всем подряд и наблюдают, что произойдет. Вместо этого они разработали эксперимент, в котором каждый клиент имеет вероятность получения письма. Эта вероятность основана на их бизнес-интуиции относительно того, кто будет более восприимчив к письму. Например, em1 нацелено на массовую аудиторию, которая мало инвестирует и не имеет высокого дохода. Эти люди будут иметь более высокую вероятность получения em1. Наконец, после определения этой вероятности мы случайным образом назначаем рассылку, так чтобы клиенты получали письмо в соответствии с назначенными вероятностями. Customer Features (Ex: Age, Income …) Customer Features (Ex: Age, Income …) Email Probability Business Intuition Отмечу, что это лучший известный мне способ использовать бизнес-экспертизу для целевой аудитории, при этом позволяя сделать валидные выводы о том, насколько эффективна маркетинговая стратегия. Маркетологи не только назначали отправку письма в соответствии с этой функцией вероятности, но и фактически фиксировали вероятность каждого клиента, что будет очень полезно позже. Вот как выглядят данные этого эксперимента. У нас есть данные о четырех характеристиках клиентов: age, income, insurance (сумма, потраченная на страхование жизни) и invested (сумма инвестиций). Эти признаки маркетологи использовали для назначения вероятности получения каждого письма. Эти вероятности хранятся в столбцах em1_ps, em2_ps и em3_ps. Кроме того, у нас есть информация о том, получил ли клиент письмо в столбцах em1, em2 и em3. Это наши переменные воздействия. Наконец, у нас есть результат, флаг converted, который указывает, заключил ли клиент договор финансового консультирования. import pandas as pd import numpy as np from matplotlib import pyplot as plt import seaborn as sns email = pd.read_csv("data/invest_email.csv") email.head()
520  Устранение смещения с помощью оценки склонности Как видим, есть клиенты с очень низкой вероятностью получения em1. Это случай клиента 2, вероятность получения письма у которого составляет всего 0.062. Также обратите внимание, что этот клиент не получил em1. Это неудивительно, учитывая, что у него была очень низкая вероятность получения этого письма. Теперь сравните это со следующими клиентами. Все они имели очень высокую вероятность получения em1, но никто из них не получил его. email.query("em1 == 0").query("em1_ps>0.9") Взвешивание по обратной вероятности воздействия Черт, снова начинаем Просто чтобы напомнить идею устранения смещения с помощью оценки склонности (мы уже обсуждали ее, рассказывая об оценке склонности), нас
Взвешивание по обратной вероятности воздействия  521 очень интересуют клиенты, упомянутые выше. Они довольно похожи на клиентов, которые получили em1, ведь у них была высокая вероятность получения этого письма. Однако они его не получили. Это делает их отличными кандидатами для оценки контрфактического результата Y0 | em1 = 1. Смещение с использованием оценки склонности работает, учитывая эту интуитивно понятную важность, которую мы хотим придать клиентам, похожим на тестовые объекты, но не подвергнутым воздействию. Оно также назначит высокую важность тем, кто похож на контрольные объекты, но при этом подвергся воздействию. Идея довольно проста: просто взвешиваем каждый объект по обратной вероятности полученного воздействия. Если объект подвергся воздействию, взвешиваем его по 1/P(treatment = 1). Если объект не подвергся воздействию, взвешиваем его по 1/P(treatment = 0) или 1/P(control ). В случае бинарного воздействия это просто: Для воздействия с несколькими категориями мы можем обобщить это до вида: Теперь, когда у нас появилась идея о том, как использовать оценку склонности для устранения смещения, давайте проверим ее работу на практике. Но сначала посмотрим, что произойдет, если мы не устраним смещение из нашего набора данных. Как я уже говорил, воздействие связано с характеристиками клиента: age, income, insurance и invested. confounders = ["age", "income", "insurance", "invested"] Если мы посмотрим на их корреляции с переменной воздействия и переменной результата, то увидим, что эти переменные действительно являются спутывающими. Они коррелируют как с переменной воздействия em1, так и с переменной результата converted. email[confounders + ["em1", "converted"]].corr()[["em1", "converted"]]
522  Устранение смещения с помощью оценки склонности Если мы не учтем смещение, возникающее из-за спутывающих факторов, наши причинно-следственные оценки будут ошибочными. Например, рассмотрим переменную invested. Те, кто инвестирует меньше, с большей вероятностью заключат договор, а также с большей вероятностью получат электронное письмо. Следовательно, если мы не будем контролировать объем инвестиций, то будет казаться, что письмо em1 увеличивает вероятность конверсии. Но это может быть просто следствием корреляции, а не причинно-следственной связью, так как более низкий объем инвестиций ведет как к более высокой вероятности конверсии, так и к большей вероятности получения письма em1. Помимо корреляционного анализа для выявления спутывающих факторов, мы также можем проверить, как выглядит распределение для тех, кто получил и не получил письмо em1. plt_df = pd.melt(email[confounders + ["em1"]], ["em1"], confounders) g = sns.FacetGrid(plt_df, col="variable", hue="em1", col_wrap=4, sharey=False, sharex=False) for i, ax in enumerate(g.axes): iter_df = plt_df.loc[lambda df: df["variable"] == confounders[i]] sns.kdeplot(x="value", hue="em1", data=iter_df, ax=ax, fill=True) ax.set_xlabel(confounders[i]) plt.show() Сохраненные оценки склонности Теперь, когда мы подтвердили, что назначение письма em1 действительно имеет смещение, мы можем приступить к устранению этого смещения с помощью оценки склонности. Сейчас самое подходящее время ответить на вопрос, который может возникнуть у вас в голове: почему я не могу просто использовать ортогонализацию? Или сформулируем более элегантно: когда мне следует использовать оценку склонности вместо ортогонализации? Это
Сохраненные оценки склонности  523 хороший вопрос, и я признаю, что у меня нет ответа. Однако есть один очевидный случай, когда появляется веский аргумент в пользу использования оценки склонности. Когда вы сохраняете вероятности получения воздействия в ходе проведения эксперимента, устранение смещения с помощью оценки склонности можно выполнить, не прибегая к оцениванию модели. Если назначение писем было реализовано вероятностным образом и мы сохранили эти вероятности, то нам не нужны модели. Это огромное преимущество, так как модели никогда не бывают идеальными, это означает, что устранение смещения с их помощью также никогда не бывает идеальным. В данной ситуации у нас сохранены вероятности воздействий в столбцах em1_ps, em2_ps и em3_ps. Поскольку нас интересует только письмо em1, для устранения смещения нам нужна вероятность в столбце em1_ps. Вот что мы будем делать. Сначала мы сгенерируем веса для устранения смещения, используя вышеприведенную формулу. Затем мы выполним случайный отбор наблюдений с возвращением (случайный повторный отбор) из этого набора данных, используя вновь созданные веса. Это означает, что наблюдение с весом 2 будет отбираться в два раза чаще, чем наблюдение с весом 1. np.random.seed(123) em1_rnd = email.assign( em1_w = email["em1"] / email["em1_ps"] + ( 1 - email["em1"]) / (1 - email["em1_ps"]) ).sample(10000, replace=True, weights="em1_w") np.random.seed(5) em1_rnd.sample(5) Этот повторный отбор должен создать новый набор данных без смещения. Он должен увеличить долю объектов, которые выглядели как тестовые объекты (высокие значения em1_ps), но не подверглись воздействию, и те, которые выглядели как контрольные объекты (низкие значения em1_ps), но подверглись воздействию. Если мы посмотрим на корреляции между воздействием и спутывающими факторами, то увидим, что они практически исчезли. em1_rnd[confounders + ["em1", "converted"]].corr()[["em1", "converted"]]
524  Устранение смещения с помощью оценки склонности Более того, если мы рассмотрим распределения спутывающих факторов по назначению воздействия, мы можем увидеть, как хорошо они выравниваются. Это не является стопроцентным доказательством того, что устранение смещения сработало, но это хороший знак. plt_df = pd.melt(em1_rnd[confounders + ["em1"]], ["em1"], confounders) g = sns.FacetGrid(plt_df, col="variable", hue="em1", col_wrap=4, sharey=False, sharex=False) for i, ax in enumerate(g.axes): iter_df = plt_df.loc[lambda df: df["variable"] == confounders[i]] sns.kdeplot(x="value", hue="em1", data=iter_df, ax=ax, fill=True) ax.set_xlabel(confounders[i]) plt.show() Новый набор данных, который мы сейчас создали, не содержит смещения. Мы можем использовать его для оценки качества модели или любого другого анализа, который требует случайного назначения воздействия. Однако есть один момент, на который нужно обратить внимание. Обратите внимание, что я семплировал 10 000 точек, хотя исходный набор данных содержал только 5000. С помощью случайного повторного отбора я могу создать набор данных без смещения любого размера. Это означает, что доверительные интервалы, рассчитанные на его основе, не будут валидными, так как они не учитывают того факта, что размер выборки мог быть искусственно увеличен. Хорошо, этот метод был очень эффективным, потому что у нас изначально были сохраненные вероятности, но что, если у нас их нет? Что, если у нас есть
Вычисленная оценка склонности  525 только информация о спутывающих факторах и назначенном воздействии, но нет информации о том, какой была вероятность назначения воздействия этим объектам? Вычисленная оценка склонности Если у нас нет сохраненных оценок склонности, нам придется их вычислить. В этой ситуации уже не так очевидно, когда следует использовать оценку склонности, а когда – ортогонализацию для устранения смещения. Поскольку у нас нет оценок склонности, мы воспользуемся моделью машинного обучения для их вычисления. Оценка склонности тесно связана с вероятностью воздействия, поэтому эта модель машинного обучения должна быть калибрована для вывода вероятностей. Кроме того, нам нужно получить прогнозы в результате перекрестной проверки, чтобы избежать любого рода смещения, которое может возникнуть из-за переобучения. from sklearn.model_selection import cross_val_predict from sklearn.ensemble import RandomForestClassifier from sklearn.calibration import CalibratedClassifierCV t = "em1" folds = 5 np.random.seed(123) # берем калиброванный случайный лес m_t = CalibratedClassifierCV( RandomForestClassifier(n_estimators=100, min_samples_leaf=40, max_depth=3), cv=3 ) # вычисляем оценки склонности на основе прогнозов, # полученные в результате перекрестной проверки ps_score_m1 = cross_val_predict(m_t, email[confounders], email[t], cv=folds, method="predict_proba")[:, 1] email.assign(ps_score_m1_est = ps_score_m1).head()
­ 526  Устранение смещения с помощью оценки склонности Просто из любопытства, обратите внимание, что вычисленные оценки склонности, ps_score_m1_est, близки к фактическим оценкам склонности em1_ ps, но не идентичны им. Эти ошибки, возникшие в процессе оценивания, будут влиять на итоговое устранение смещения, но мы надеемся, что это влияние будет незначительным. Мы также можем проверить качество калиб ровки наших оценок склонности. Для этого мы можем построить график, на котором сравним среднее значение оценки склонности и среднее значение em1. Если оценка хорошо откалибрована, то 20 % клиентов с оценкой склонности 0.2 должны были получить email-1, 30 % клиентов с оценкой склонности 0.3 должны были получить email-1 и т. д. from sklearn.calibration import calibration_curve prob_true, prob_pred = calibration_curve( email["em1"], ps_score_m1, n_bins=3 ) plt.plot(prob_pred, prob_true, label="Калиброванный RF") plt.plot([.1,.8], [.1, .8], color="grey", linestyle="dashed", label="Идеальная калибровка") plt.ylabel("Доля наблюдений положительного класса") plt.xlabel("Среднее значение прогноза") plt.legend(); Теперь мы можем действовать так, как если бы у нас была истинная оценка склонности. np.random.seed(123) em1_rnd_est = email.assign( em1_w = email["em1"] / ps_score_m1 + (1 - email["em1"]) / (1 - ps_score_m1) ).sample(10000, replace=True, weights="em1_w")
Недостаток оценки склонности  527 Если мы взглянем на корреляции, то увидим, что между воздействием и спутывающими переменными все еще существуют сильные корреляции, даже после процедуры устранения смещения. Например, income имеет корреляцию –0.18, что ниже, чем корреляция в несмещенном наборе данных (–0.3), но намного выше, чем в наборе данных, где смещение было устранено с помощью исходной оценки склонности (0.01). То же самое касается переменной invested, которая все еще показывает некоторую корреляцию. em1_rnd_est[confounders + ["em1"]].corr()["em1"] age income insurance invested em1 Name: em1, -0.025840 -0.151268 -0.036037 -0.108351 1.000000 dtype: float64 plt_df = pd.melt(em1_rnd_est[confounders + ["em1"]], ["em1"], confounders) g = sns.FacetGrid(plt_df, col="variable", hue="em1", col_wrap=4, sharey=False, sharex=False) for i, ax in enumerate(g.axes): iter_df = plt_df.loc[lambda df: df["variable"] == confounders[i]] sns.kdeplot(x="value", hue="em1", data=iter_df, ax=ax, fill=True) ax.set_xlabel(confounders[i]) plt.show() Что касается распределений, мы видим, что они не совпадают так хорошо, как раньше, особенно это касается переменных invested и income. Недостаток оценки склонности Мы уже много говорили о недостатках оценки склонности в главе, посвященной ей, поэтому не буду тратить на них много времени, но стоит вспомнить один из основных недостатков оценки склонности. Он связан с очень высокими или очень низкими оценками склонности.
528  Устранение смещения с помощью оценки склонности Я не боюсь никого Но есть вещь… Она пугает меня Давайте взглянем на следующий объект в нашем исходном наборе данных. email.loc[[1014]] У этого объекта оценка склонности для em1 равна 0.027. Это значит, что его вес будет около 37 (1/0.027). Данный объект будет выбран почти в два раза чаще, чем тестовый объект с оценкой склонности 0.05 (вес 20), который уже является очень низким. Этот объект появляется 38 раз в наборе данных, который мы семплировали, используя сохраненные (невычисленные) оценки склонности. em1_rnd.loc[[1014]].shape (38, 12) Это проблема, потому что очищенный от смещения набор данных с избытком заполнен только одним объектом. Если бы этого объекта не было в исходном наборе данных, очищенный от смещения набор данных мог бы выглядеть совершенно иначе. Таким образом, удаление одного объекта может значительно повлиять на внешний вид очищенного набора данных. Это проблема высокой дисперсии. Если мы построим график количества повторов (реплик) каждого объекта в очищенном наборе данных, мы увидим, что некоторые из них встречают-
Недостаток оценки склонности  529 ся более 10 раз. Это тестовые объекты с низкими оценками склонности или контрольные объекты с высокими оценками склонности. plt.figure(figsize=(10,5)) sns.scatterplot( data=em1_rnd.assign(count=1).groupby(em1_rnd.index).agg( {"count":"count", "em1_ps": "mean", "em1": "mean"}), x="em1_ps", y="count", hue="em1", alpha=0.2 ) plt.title("Реплики объектов в наборе данных, " "очищенном от смещения"); Чтобы избежать наличия чрезмерно важных объектов, некоторые ученые предпочитают выполнять клиппинг весов (например, веса не должны быть больше 20). Это действительно поможет уменьшить дисперсию, но добавит обратно смещение. Поскольку вся суть этого подхода заключалась в удалении смещения, я считаю, что клиппинг весов – не лучшая идея. На мой взгляд, все это делать нужно на этапе эксперимента, а не на этапе анализа. Вы должны убедиться, что ни у одного из объектов нет слишком высокого веса. Другими словами, насколько это возможно, постарайтесь назначать воздействие всем с равными вероятностями. Честно говоря, метод взвешивания по оценке склонности будет испытывать проблемы, как и все методы причинно-следственного анализа, когда вероятность отнесения либо к тестовой группе, либо к контрольной группе слишком низка для всей выборки или для субпопуляции. Интуитивно это означает, что некоторые объекты почти никогда не станут тестовыми или
­ ­ 530  Устранение смещения с помощью оценки склонности контрольными, что затруднит оценку контрфактических результатов для этих объектов. Эту проблему можно рассматривать как нарушение предположения об «общей» области оценок склонности, т. е. предположения о пересечении распределений оценок склонности для тестовой и контрольной групп. Согласно этому предположению, должна существовать область оценок склонности, «общая» как для объектов тестовой группы, так и для объектов конт рольной группы. Предположение об «общей» области оценок склонности еще называют предположением о позитивности (у каждого объекта должна быть ненулевая вероятность попасть в тестовую или контрольную группу). Количество объектов Объекты, которые никогда не будут подвергаться действию фактора 0 Зона пересечения (common support) Объекты, которые всегда будут подвергаться действию фактора 0.5 Оценки склонности Тестовая группа 1 Контрольная группа Позитивность, или «общая» область оценок склонности Помимо проблем с высокой дисперсией, у нас также могут быть проблемы с позитивностью, или «общей» областью оценок склонности. Позитивность, или «общая» область оценок склонности, – это предположение в анализе причинно-следственных связей, согласно которому должно быть достаточное пересечение между характеристиками тестовых и контрольных объектов. Иными словами, у каждого объекта должна быть ненулевая вероятность попасть в тестовую или контрольную группу. Если этого не происходит, мы не сможем оценить каузальный эффект, который будет валиден для всей популяции, он будет валиден только для объектов, попавших в «общую» область. Здесь нужно пояснить, что проблемы с позитивностью касаются самих данных, а не метода вычисления оценки склонности. Метод вычисления оценки склонности только ясно показывает, когда возникают проблемы с позитивностью. В этом смысле это аргумент в пользу метода вычисления оценки склонности. Если большинство методов не предупреждают вас о проб
­ ­ Позитивность, или «общая» область оценок склонности  531 лемах с позитивностью, оценка склонности откроет вам глаза на них. Все, что вам нужно сделать, – это построить распределения оценок склонности для тестовых и контрольных объектов. sns.displot(data=email, x="em1_ps", hue="em1") plt.title("Проверка позитивности"); Если у этих распределений хорошее пересечение, как на графике выше, то у вас есть «общая» область оценок склонности. До сих пор мы рассматривали только email-1 (em1). Давайте теперь посмот рим на email-3. email.head() Первое, что бросается в глаза, – это наличие объектов с нулевой вероят ностью, что уже указывает на нарушение предположения о положительности. Теперь давайте посмотрим на распределение признаков по email-3 (em3).
532  Устранение смещения с помощью оценки склонности sns.pairplot(email.sample(1000)[confounders + ["em3"]], hue="em3", plot_kws=dict(alpha=0.3)); Похоже, что em3 было отправлено только клиентам старше 40 лет. Это большая проблема. Если контрольная группа включает молодых людей, а тестовая – нет, то нет способа оценить контрфактический результат Y0 | T = 1, age < 40. Просто потому, что мы не знаем, как молодые клиенты отреагируют на это электронное письмо. Мы можем попробовать устранить смещение с помощью оценки склонности. em3_weight = (email # используем другую реализацию, чтобы избежать деления на ноль .assign(em3_w = np.where(email["em3"].astype(bool), 1 / email["em3_ps"],
Позитивность, или «общая» область оценок склонности  533 1 / (1 - email["em3_ps"]))) .sample(10000, replace=True, weights="em3_w")) em3_weight[confounders + ["em3"]].corr()["em3"] age 0.251035 income 0.047607 insurance -0.000961 invested 0.053524 em3 1.000000 Name: em3, dtype: float64 Как видно, все еще существует значительная корреляция между возрастом и воздействием. Отсутствие тестовых объектов в возрасте моложе 40 лет привело к тому, что мы не смогли устранить смещение, связанное с возрастом. Кроме того, мы можем провести диагностику позитивности, построив распределение оценок склонности по воздействию. sns.displot(data=email, x="em3_ps", hue="em3") plt.title("Проверка позитивности"); Обратите внимание, насколько слабое здесь пересечение. Объекты с оценками склонности ниже 0.4 почти никогда не подвергаются воздействию. Не говоря уже о большом пике в районе нуля.
534  Устранение смещения с помощью оценки склонности Позитивность на практике Проблемы с позитивностью очень серьезны в академической среде, так как они касаются обобщения выводов или теорий. Если у вас есть только тестовые объекты старшего возраста, вы не сможете обобщить выявленный эффект воздействия на более молодой сегмент. Однако в индустрии нарушения предположения о позитивности могут быть не такими проблематичными. Например, если вы кредитор и хотите оценить эластичность суммы кредита по отношению к вероятности дефолта, вы, вероятно, не будете выдавать большие кредиты людям с очень низкими кредитными рейтингами. Конечно, это нарушит предположение о позитивности, но вам не очень интересно оценивать эластичность суммы кредита для рискованных клиентов, потому что вы все равно не собираетесь выдавать им кредиты. Или возьмем более преувеличенный пример, если вы хотите оценить ценовую эластичность какогото продукта, вы, вероятно, не будете тестировать цены от 0 до 1 000 000, поскольку у вас есть интуиция относительно того, где обычно находятся цены. Вы будете часто тестировать в каком-то определенном диапазоне. Суть в том, что вы можете (и, вероятно, должны) использовать свою интуицию, чтобы исключить часть объектов из тестовой (или контрольной) группы. Проведение случайных экспериментов дорого, поэтому вам следует сосредоточить свои усилия на наиболее перспективных областях. В нашем примере с email, вероятно, маркетологи решили, что email-3 не следует отправлять молодым людям. Возможно, он содержит текст, который явно касается чего-то, что могут понять только люди старшего возраста. Независимо от причины мы можем оценить эффект email-3 только на группу более зрелых объектов, группу объектов старше 40 лет. И это вполне нормально. Мы просто не должны обобщать наши выводы на более молодых. Учитывая это, давайте устраним смещение для email-3. Конечно, мы исключим молодых из выборки. em3_weight.query("age>40")[confounders + ["em3"]].corr()["em3"] age -0.034455 income 0.024633 insurance -0.005650 invested 0.020711 em3 1.000000 Name: em3, dtype: float64 Как только мы это сделаем, обратите внимание, как исчезает корреляция между воздействием и спутывающими факторами. Помните, что во всей выборке корреляция с возрастом была более 0.2. Теперь она, вероятно, неотличима от нуля. Кроме того, мы можем исследовать распределение тестовых и контрольных объектов в этой отфильтрованной выборке.
­ Ключевые идеи  535 sns.pairplot(em3_weight.query("age>40").sample(1000)[confounders + ["em3"]], hue="em3", plot_kws=dict(alpha=0.3)); Все это я пишу к тому, что, да, из-за нарушений предположения о позитивности вы не можете устранить смещение во всей выборке относительно em3. Но вы можете сделать это для той группы, для которой email-3 изначально был предназначен. И этого достаточно. Ключевые идеи В этом приложении представлена еще одна идея устранения смещения, когда воздействие является бинарным или дискретным. Идея заключается в том, чтобы отбирать объекты с возвращением, используя обратную оценку склонности в качестве веса. Это позволит увеличить выборку объектов, которые выглядят как тестовые (с высокими оценками склонности), но не подверг
­ 536  Устранение смещения с помощью оценки склонности лись воздействию, и объектов, которые выглядят как контрольные (с низкими оценками склонности), но подверглись воздействию. Основное преимущество устранения смещения с помощью оценки склонности заключается в том, что оно не требует оценивания модели, если вы сохранили вероятности воздействия во время эксперимента. Это не означает, что вы не можете применить взвешивание по оценке склонности, если у вас нет этих вероятностей, но необходимость их оценивания добавит дополнительную ошибку в ваш процесс устранения смещения. Что касается недостатков, взвешивание по оценке склонности может иметь высокую дисперсию, если оценка склонности является либо слишком высокой, либо слишком низкой, создавая огромные веса. Интуитивно это происходит, когда существует очень маленькое количество контрольных объектов, которые выглядят как тестовые, и очень маленькое количество тес товых объектов, которые выглядят как контрольные. Справедливости ради следует отметить, что вышеописанная ситуация представляет проблему для всех методов анализа причинно-следственных связей, но взвешивание по оценке склонности более четко подчеркивает это нарушение предположения о позитивности. Наконец, мы кратко обсудили, насколько серьезной проблемой является нарушение предположения о позитивности. В конечном итоге мы пришли к выводу, что это не проблема, если вы не хотите обобщать свои выводы на ту часть популяции, где позитивность не соблюдается. Дополнительное чтение Материал, который я написал в этом приложении, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю данный материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. Большая часть этой главы взята из книги Гвидо В. Имбенса (Guido W. Imbens) «Causal Inference for Statistics, Social, and Biomedical Sciences» (https://www. amazon.com/Causal-Inference-Statistics-Biomedical-Sciences/dp/0521885884), где вы найдете подробное обсуждение оценки склонности. Обсуждение позитивности принадлежит мне, однако взято оно из бесчисленных дебатов, которые я вел на работе со своими коллегами. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Приложение 3 Когда прогнозирование не работает Когда у вас есть только молоток... В период между 2015 и 2020 годом машинное обучение пережило огромный подъем. Его доказанная полезность в сферах компьютерного зрения и обработки естественного языка наряду с изначальной нехваткой профессионалов в этой области создали идеальные условия для индустрии обучения машинному обучению. Такие личности, как Эндрю Ын и Себастьян Трун, сумели научить мир машинному обучению по минимальным ценам. В то же время, с точки зрения программного обеспечения, стало значительно проще настроить сложную модель машинного обучения (как вы уже видели по весьма немногочисленным строкам программного кода, которые нам потребовались для реализации ML в предыдущей главе). Руководства по созданию интеллектуальных систем заполонили интернет. Стоимость входа в ML резко упала.
­ 538  Когда прогнозирование не работает Создание моделей машинного обучения стало настолько простым, что даже не нужно было уметь хорошо программировать (и я – живое подтверждение этого), знать математику, лежащую в основе алгоритмов. Фактически вы могли создавать чудеса с помощью следующих 5 строк на Python. X_train, y_train, X_test, y_test = train_test_split(X, y) ## инстатируем модель машинного обучения model = MachineLearningModel() ## обучаем модель машинного обучения model.fit(X_train, y_train) # делаем прогнозы для данных, не участвоваших в обучении y_pred = model.predict(X_test) # оцениваем качество прогнозов print("Качество", metric(y_test, y_pred)) Это все замечательно! Я полностью за то, чтобы сделать полезный контент доступным. Однако у всего этого есть и темная сторона. Новая волна специа листов по data science была в основном обучена прогнозному моделированию, поскольку именно эту задачу в первую очередь решает ML. В результате всякий раз, когда эти специалисты сталкивались с бизнес-проблемой, они пытались решить ее с помощью прогнозных моделей, что неудивительно. Когда это действительно были задачи прогнозирования, как в предыдущей главе, специалисты по data science обычно добивались успеха, и все были довольны. Однако существует целый класс задач, которые просто не могут быть решены с помощью методов прогнозирования. И когда такие задачи появлялись, специалисты по data science обычно терпели неудачу. Эти задачи формулируются как «насколько я могу увеличить Y, изменив X». По моему опыту, этот иной тип задач чаще всего интересует руководство. Оно часто хочет знать, как увеличить продажи, снизить затраты или привлечь побольше клиентов. Не нужно и говорить, что руководство не очень довольно, когда специалист по data science рассказывает ему о том, как предсказать продажи, вместо того чтобы рассказать, как их увеличить. К сожалению, если все, что знает специалист по data science, – это прогнозные модели, такое случается довольно часто. Как однажды сказал мне мой начальник: «когда у тебя есть только молоток, все начинает казаться гвоздем». Как я уже сказал, я полностью за снижение стоимости знаний, но в текущей учебной программе специалистов по data science есть огромный пробел. Я считаю, что моя задача здесь – заполнить этот пробел. Обеспечить вас инструментами для решения этого иного класса задач, которые имеют причинно-следственный характер. Вы пытаетесь оценить, как нечто, что вы можете контролировать (реклама, цена, обслуживание клиентов), влияет на что-то, что вы хотите изменить, но не можете контролировать напрямую (продажи, количество клиентов, прибыль и убытки). Но прежде чем рассказать вам, как решать эти задачи,
­ Кто хочет купон  539 я хочу показать вам, что происходит, когда вы относитесь к ним как к задачам прогнозирования и пытаетесь решить их с помощью традиционного набора инструментов машинного обучения. Причина в том, что специалисты по data science часто приходят ко мне и говорят: «ОК, хотя решение задач причинно-следственного анализа с помощью инструментов прогнозирования – не лучшая идея, эти инструменты наверняка чем-нибудь помогут, не правда ли? Я имею в виду, они не могут навредить...». Как оказалось, могут. И вам лучше понять это, прежде чем вы начнете «бить молотком по собственному большому пальцу». Хорошо продуманная политика, ориентированная на бизнес model.fit(X_train, y) model.predict(X_test) Кто хочет купон? Чтобы сделать объяснение более понятным, давайте возьмем пример, который мы использовали в предыдущей главе, но с небольшим изменением. Ранее мы пытались отличить прибыльных клиентов от неприбыльных. Мы сформулировали это как задачу прогнозирования: прогнозирование прибыльности клиента. Затем мы могли бы обучить модель машинного обучения под эту задачу и использовать ее для отбора клиентов, с которыми будем сотрудничать: только с теми, кого мы прогнозируем как прибыльных. Другими словами, наша цель заключалась в том, чтобы отделить прибыльных от неприбыльных, и это мы могли сделать с помощью прогнозной модели. Теперь у вас новая задача. Вы подозреваете, что раздача купонов новым клиентам увеличивает их вовлеченность в ваш бизнес и делает их более прибыльными в долгосрочной перспективе. То есть они тратят больше и в течение более длительного периода. Ваша новая задача – выяснить, каким должен быть номинал купона (включая ноль). Обратите внимание, что с купонами
540  Когда прогнозирование не работает вы фактически раздаете деньги, чтобы люди тратили их на ваш бизнес. По этой причине они входят в ваши бухгалтерские книги как расходы. Обратите внимание, что если номинал купона слишком высок, вы, вероятно, потеряете деньги, так как клиенты будут покупать все, что им нужно, используя только купоны. Иначе говоря, они будут получать ваш продукт бесплатно. С другой стороны, если номинал купона слишком низок (или равен нулю), вы вообще не даете купоны. Это может быть допустимым ответом, но вполне возможно, что некоторые предварительные скидки в виде купонов будут более прибыльными в долгосрочной перспективе. По причинам, которые вы увидите позже, мы воспользуемся функцией генерации данных вместо загрузки статичного набора данных. Функция ltv_with_coupons() генерирует данные о транзакциях для нас. Как вы видите, они имеют тот же формат, что и в предыдущем приложении, одна строка соответствует клиенту, есть столбец с информацией о стоимости привлечения и столбцы для транзакций с 0-го по 29-й день. import pandas as pd import numpy as np from sklearn import ensemble from sklearn.model_selection import (train_test_split, cross_val_predict) from sklearn.metrics import r2_score import seaborn as sns from matplotlib import pyplot as plt from matplotlib import style style.use("ggplot") # вспомогательные функции from nb18 import ltv_with_coupons transactions, customer_features = ltv_with_coupons() print(transactions.shape) transactions.head() Что касается других данных, вновь у нас есть идентификатор клиента, регион проживания клиента, доход клиента и возраст клиента. В дополнение к этому у нас теперь есть переменная, называющаяся coupons, которая показывает, какой номинал купона мы выдали этому клиенту. print(customer_features.shape) customer_features.head()
Кто хочет купон  541 Чтобы объединить эти данные в одном датафрейме, мы сложим все столбцы в первой таблице (то есть сложим CACQ с транзакциями). Это даст нам net_ value, рассчитанное так же, как в предыдущей главе. После этого мы присоединим признаки и обновим net_value, чтобы включить стоимость купонов. def process_data(transactions, customer_data): profitable = (transactions[["customer_id"]] .assign(net_value = transactions .drop(columns="customer_id") .sum(axis=1))) return (customer_data # присоединяем net_value и признаки .merge(profitable, on="customer_id") # включаем стоимость купонов .assign(net_value = lambda d: d["net_value"] - d["coupons"])) customer_features = process_data(transactions, customer_features) customer_features.head() Этот итоговый датафрейм содержит все, что нам нужно. В нем есть наша целевая переменная net_value, есть признаки клиента – region, income и age и есть рычаг или воздействие, которое мы хотим оптимизировать: номинал купонов (переменная сoupons). Чтобы выяснить, как купоны могут увеличить net_value, давайте посмотрим, как они раздавались. customer_features.groupby("coupons")["customer_id"].count() coupons 0 458 5 4749
542  Когда прогнозирование не работает 10 4154 15 639 Name: customer_id, dtype: int64 Мы видим, что большинство розданных купонов имели номинал 5 BRL, за ними следуют купоны номиналом 10 BRL. Мы выдали очень мало купонов номиналом 15 BRL или вообще не выдали купоны (значение 0). Это свидетельствует о том, что они НЕ были розданы случайным образом. Чтобы проверить это, давайте посмотрим корреляцию между другими переменными и купонами. customer_features.corr()[["coupons"]] Это интересно. Похоже, что чем старше человек, тем выше вероятность того, что он или она получит купон. Это указывает на наличие смещения в наших данных. Мы также видим отрицательную корреляцию между coupons и net_value: чем больше купонов мы раздаем, тем меньше net_value. Это вряд ли является причинно-следственной связью, так как мы уже знаем, что купоны не распределялись случайным образом. Может быть так, что, скажем, пожилые люди тратят меньше на наши продукты и также получают купоны с более высокими номиналами, что искажает связь между coupons и net_value до такой степени, что она становится отрицательной. Суть в том, что мы знаем о наличии смещения. Однако поскольку в этой главе уже слишком много информации, я пока проигнорирую это (на самом деле я обойду проблему с помощью артефакта, который вы увидите через мгновение). Просто имейте в виду, это то, с чем нам придется разобраться в будущем. На данном этапе анализа, если бы это была задача прогнозирования, мы, вероятно, разделили бы набор данных на обучающий и тестовый, чтобы соответственно строить и оценивать качество работы некоторых стратегий. Но это НЕ задача прогнозирования. Здесь конечная цель не заключается в том, чтобы получить хороший прогноз прибыльности клиентов. Вместо этого нужно выяснить оптимальную стратегию выдачи купонов. Чтобы оце-
Простая политика  543 нить эту оптимизацию, нам нужно выяснить, как бы развивались события, если бы мы выдали купоны иного номинала, нежели те, которые были выданы. Это своего рода контрфактический вопрос «что, если», который мы изучаем в рамках причинности. Кросс-валидация здесь не поможет, потому что мы просто не можем наблюдать контрфактические результаты. Мы можем увидеть только то, что произошло с фактически выданными купонами, но не можем узнать, что произошло бы, если бы клиенты получили купоны другого номинала. Если только у нас нет симулированных данных! Если наши данные являются симулированными, мы можем сгенерировать точно такие же данные, изменяя лишь параметры номинала купонов. Это позволит нам увидеть, как net_value меняется при разных стратегиях выдачи купонов. Мы сможем тогда вычислить эффект воздействия для разных стратегий NetValuet=a – NetValuet=b. Благодаря силе симулированных данных понять эту главу будет намного проще. О да, и это также сделает проблему смещения неактуальной, потому что мы будем наблюдать причинно-следственный эффект напрямую. Тем не менее всегда помните, что это педагогический артефакт. В реальном мире у вас нет симулированных данных, и вы, конечно, не можете увидеть, что бы произошло при разных стратегиях воздействия. Как всегда, индивидуальные эффекты воздействия остаются скрытыми. Это ставит интересную задачу. Как мы можем оценить наши стратегии для выявления причинно-следственного эффекта, если мы никогда не сможем увидеть реальный причинно-следственный эффект? Реальный ответ очень сложен и настолько важен, что заслуживает отдельной главы. Будьте уверены, что мы с этим разберемся. А пока просто наслаждайтесь простотой симулированных данных. И, говоря о простоте... Простая политика Как всегда, первое, что мы должны сделать, когда сталкиваемся с новой задачей анализа данных, – это задать себе вопрос: «Какая самая простая процедура уже может принести пользу?» В данном случае самое простое – это проанализировать имеющиеся данные и оценить net_value для каждого значения (номинала) купона. Затем выяснить, какой номинал купона генерирует наибольшее значение net_value, и выдавать только этот номинал купона всем клиентам. sns.barplot(data=customer_features, x="coupons", y="net_value") plt.title("Net Value по номиналу купона");
544  Когда прогнозирование не работает Проведя этот анализ, мы можем увидеть, что в среднем теряем деньги, когда номинал купона равен 0 или 15, и зарабатываем деньги при номиналах купонов 5 и 10 BRL. Наибольшее среднее значение net_value наблюдается при купонах в 5 BRL, приносящих около 250 BRL чистой стоимости на одного клиента. Естественно, самое простое, что мы можем попробовать, – это выдавать всем купоны по 5 BRL и посмотреть, что из этого выйдет. Это полностью игнорирует возможное наличие смещения, но мы говорим о простом решении! Чтобы оценить эту политику, функция ltv_with_coupons() принимает в качестве аргумента массив из 10 000 значений, содержащий желаемый купон для каждого из 10 000 клиентов в нашей базе данных. Чтобы создать этот массив, мы сгенерируем массив из единиц с помощью np.ones() размером с наш массив customer_features["coupons"] (10 000) и умножим его на 5. Затем мы передадим этот массив в функцию ltv_with_coupons(). Это создаст новый набор данных, аналогичный предыдущему, но в котором каждое значение купона будет равно 5. Затем мы обработаем эти данные, чтобы получить чистую стоимость согласно предложенной политике. simple_policy = 5 * np.ones(customer_features["coupons"].shape) transactions_simple_policy, customer_features_simple_policy = ( ltv_with_coupons(simple_policy) ) customer_features_simple_policy = process_data( transactions_simple_policy, customer_features_simple_policy ) customer_features_simple_policy.head()
Политика на основе модели машинного обучения  545 Для проверки здравого смысла давайте посмотрим, действительно ли характеристики клиентов остались неизменными, рассмотрев первых нескольких клиентов. Возьмем, например, третьего (customer_id 2). Для этого клиента region равен 35, income – 2034 и age – 33 года. Если мы прокрутим вверх, то увидим, что это соответствует тому, что у нас было раньше, так что здесь все в порядке. Кроме того, мы можем убедиться, что все купоны действительно равны 5 BRL. Наконец, чистая стоимость изменяется, как и ожидалось. Одна из причин этого заключается в том, что стоимость, связанная с купонами, изменится. Например, у этого клиента был купон на 15 BRL, а теперь у него купон на 5 BRL. Это уменьшит стоимость с 15 до 5 единиц. Но заметьте, что значение net_value изменилось с –23 до 63, что составляет увеличение на 86 BRL. Это намного больше, чем разница по затратам в 10 единиц. Здесь выдача купонов меньшего номинала сделала этого конкретного клиента намного более прибыльным, чем раньше. Наконец, чтобы оценить политику, мы можем просто взять среднее значение net_value. simple_policy_gain = customer_features_simple_policy["net_value"].mean() simple_policy_gain 252.9268 Как видим, эта простая политика показывает, что мы можем получать в среднем 253 BRL с каждого клиента, если будем выдавать всем купоны по 5 BRL. Это впечатляет! Но можем ли мы сделать лучше? Что, если использовать наш блестящий инструмент машинного обучения для решения этой задачи? Давайте попробуем его. Политика на основе модели машинного обучения Чтобы использовать машинное обучение (ML), мы адаптируем все то, что сделали в предыдущей главе. Идея состоит в том, чтобы построить модель ML, которая предсказывает net_value, как раньше, взять эти прогнозы и разбить их на определенное количество диапазонов. Затем мы разделим данные на эти диапазоны. По сути, мы разделим клиентов по спрогнозированным
546  Когда прогнозирование не работает значениям net_value. Клиенты, которые, по нашему мнению, будут генерировать примерно одинаковые значения net_value, окажутся в одном и том же бине, или группе. Наконец, для каждой группы мы посмотрим, какой номинал купона приносит максимальное значение net_value. Мы делаем то же самое, что и в простой политике, но теперь внутри групп, которые заданы диапазонами спрогнозированных значений. Простая политика All Customers Политика на основе модели ML Band-1 Band-2 Band-3 Интуиция здесь следующая: мы знаем, что в среднем купоны на 5 BRL работают лучше. Однако возможно, что для какой-то группы клиентов другое значение будет еще лучше, чем 5 BRL. Возможно, 5 BRL – это оптимальная стратегия для большинства клиентов, но не для всех. Если мы сможем идентифицировать тех, для кого оптимальное значение отличается, мы сможем выстроить более оптимальную стратегию раздачи купонов по сравнению с простой политикой, которую реализовали выше. Это то, что мы называем задачей персонализации. Мы можем использовать персонализацию, когда у нас есть более одной стратегии на выбор и хотя бы одна из них не является наилучшей стратегией в целом, но является наилучшей для какого-то подмножества целевой популяции. Это определение немного запутано, но интуиция проста. Если у вас есть только одна стратегия, вам не надо заниматься персонализацией. Вы делаете одно и то же действие для каждого клиента. Если у вас есть несколько стратегий, но одна из стра-
Политика на основе модели машинного обучения  547 тегий всегда лучше для каждого клиента, зачем вам персонализировать? Вы можете просто выбрать эту лучшую стратегию. Вы будете заниматься персонализацией только в том случае, если у вас есть одна стратегия, которая лучше работает для одного подмножества популяции, и другая стратегия, которая лучше работает для другого подмножества популяции. Вернемся к примеру. Первое, что нам нужно, – это функция, которая обучает нашу прогнозную модель и затем разбивает прогнозы на прогнозные диапазоны. Эта функция возвращает другую функцию – прогнозную функцию, которая будет принимать датафрейм данных и добавлять к нему столбец с прогнозами и столбец с диапазонами. def model_bands(train_set, features, target, model_params, n_bands, seed=1): np.random.seed(seed) # обучаем модель ML reg = ensemble.GradientBoostingRegressor(**model_params) reg.fit(train_set[features], train_set[target]) # получаем диапазоны прогнозов bands = pd.qcut(reg.predict(train_set[features]), q=n_bands, retbins=True)[1] def predict(test_set): # получаем прогнозы с помощью обученной модели predictions = reg.predict(test_set[features]) # относим прогнозы к тому или иному диапазону pred_bands = np.digitize(predictions, bands, right=False) return test_set.assign( predictions=predictions, # клиппинг позволяет избежать # создания новых верхних границ pred_bands=np.clip(pred_bands, 1, n_bands) ) return predict Чтобы оценить качество наших прогнозов, мы разделим набор данных на обучающий и тестовый. Здесь важно заметить, что мы оцениваем качество прогнозов, А НЕ политики. Это делается просто для того, чтобы понять, насколько хороша наша модель с точки зрения выполнения своей задачи. train, test = train_test_split(customer_features, test_size=0.3, random_state=1) Теперь обучим нашу модель и создадим 10 диапазонов прогнозов. model_params = {'n_estimators': 150, 'max_depth': 4,
548  Когда прогнозирование не работает 'min_samples_split': 10, 'learning_rate': 0.01, 'loss': 'squared_error'} features = ["region", "income", "age"] target = "net_value" np.random.seed(1) model = model_bands(train, features, target, model_params, n_bands=10) После обучения модели мы можем использовать ее для получения прогнозов, передав ей датафрейм. Результатом будет датафрейм с двумя новыми столбцами: predictions и pred_bands. model(train).head() Чтобы оценить прогнозную силу нашей модели, мы можем взглянуть на R2 для обучающего и тестового наборов. print("R2 - обучение:, ", r2_score( train["net_value"], model(train)["predictions"])) print("R2 - тест:, ", r2_score( test["net_value"], model(test)["predictions"])) R2 - обучение: 0.5382953634651921 R2 - тест: 0.504563847410434 Помните, что эта оценка качества касается только прогнозной силы. На самом деле интересует, может ли эта модель принести нам деньги. Давайте создадим политику! Идея здесь очень похожа на ту, что мы видели в предыдущей главе. Мы будем группировать клиентов по диапазонам прогнозов модели. Затем для каждого типа клиента (где тип определяется диапазоном) мы увидим, какое решение – номинал купона в нашем случае – является наилучшим. Для этого мы можем сгруппировать наши данные по диапазонам прогнозов и номиналам купонов и построить график значений net_value. customer_features_tmp = model(customer_features) customer_features_tmp['coupons'] = ( customer_features_tmp['coupons'].astype(str) )
Политика на основе модели машинного обучения  549 plt.figure(figsize=(12,6)) sns.barplot(data=customer_features_tmp, x="pred_bands", y="net_value", hue="coupons") plt.title("Net Value по номиналу купона"); Этот график очень интересен. Обратите внимание, как оптимальное решение меняется в зависимости от диапазонов прогнозов. Например, в диапазонах 1, 7 и 8 лучше всего выдавать купоны на 10 BRL. В диапазонах 3, 5 и 10 лучше всего выдавать купоны на 5 BRL. Это означает, что данная политика очень похожа на простую, за исключением последнего диапазона. Это свидетельство того, что персонализация может быть возможна, так как оптимальное решение меняется в зависимости от группы популяции. Мы можем закодировать эту политику с помощью нескольких операторов if... then..., но я покажу более общий подход, использующий операции с датафреймами. Когда ты немного владеешь магией pandas ПАРКУР!!!
­ 550  Когда прогнозирование не работает Сначала мы сгруппируем наших клиентов по диапазону и номиналу купона и возьмем среднее значение net_value для каждой группы, как на графике выше. pred_bands = (model(customer_features) .groupby(["pred_bands", "coupons"]) [["net_value"]].mean() .reset_index()) pred_bands.head(7) Затем мы сгруппируем по диапазону и возьмем ранг net_value для каждой строки. Это упорядочит строки в соответствии со средним значением net_ value, где значение 1 в столбце max_net – это наилучшее значение net_value в этом диапазоне. pred_bands["max_net"] = (pred_bands .groupby(['pred_bands']) [["net_value"]] .rank(ascending=False)) pred_bands.head(7) Например, для первого диапазона наилучшая стратегия по купонам – 10 BRL (поскольку именно этот номинал дает наибольшее значение net_va lue). Далее мы оставим только наибольшее значение net_value для каждого диапазона.
Политика на основе модели машинного обучения  551 best_coupons_per_band = pred_bands.query( "max_net==1")[["pred_bands", "coupons"]] best_coupons_per_band Чтобы сформировать политику на основе нашей модели, мы возьмем эту небольшую таблицу и присоединим ее обратно к исходной таблице, используя cтолбец pred_bands в качестве ключа. Это сопоставит каждую строку в исходном наборе данных с тем, что мы считаем оптимальным номиналом купона согласно этой политике. Затем мы отсортируем строки по customer_id, чтобы сохранить тот же порядок, который у нас был ранее. Это важно для оценки, так как ltv_with_coupons() принимает в качестве аргумента номинал купона, используя порядок наблюдений исходного датафрейма. coupons_per_id = (model(customer_features) .drop(columns=["coupons"]) .merge(best_coupons_per_band, on="pred_bands") [["customer_id", "coupons"]] .sort_values('customer_id')) coupons_per_id.head() Наконец, чтобы оценить эффективность политики, мы передадим столбец coupons в качестве массива купонов в функцию ltv_with_coupons(). Она заново генерирует данные, теперь предполагая, что купоны были выданы согласно политике на основе модели.
552  Когда прогнозирование не работает transactions_policy_w_model, customer_features_policy_w_model = ( ltv_with_coupons(coupons_per_id[["coupons"]].values.flatten()) ) customer_features_policy_w_model = process_data( transactions_policy_w_model, customer_features_policy_w_model ) customer_features_policy_w_model.head() Снова руководствуясь здравым смыслом, мы можем увидеть, что третий клиент – это все тот же клиент из региона 35 с доходом 2034 и возрастом 33 года. Он также имеет номинал купона, равный 5 BRL, как мы установили согласно нашей политике. Чтобы проверить, сколько денег приносит эта политика, мы можем вычислить среднее значение net_value для этого нового набора данных. policy_w_model_gain = customer_features_policy_w_model["net_value"].mean() policy_w_model_gain 229.9341 Неплохо! Мы можем ожидать около 230 BRL на одного клиента в соответствии с политикой на основе модели. Но подождите! Вы помните, сколько мы зарабатывали с помощью простой политики? Давайте сравним обе политики. plt.figure(figsize=(10,6)) sns.histplot(data=customer_features_policy_w_model, bins=40, x="net_value", label="Политика на основе модели", color="C0") sns.histplot(data=customer_features_simple_policy, bins=40, x="net_value", label="Простая политика", color="C1") plt.legend() plt.title(f"Выигрыш от простой политики: {simple_policy_gain};\n" f"Выигрыш от политики на основе модели: {policy_w_model_gain};");
Бьем молотком по собственному большому пальцу с помощью прогнозов  553 Вот где большинство специалистов по data science падают со стула. Политика на основе модели дает среднее значение net_value на 20 BRL меньше, чем очень простая политика. Как модель, которая хорошо предсказывает net_value, может быть неоптимальной для стратегии, направленной на максимизацию net_value? Должно быть, в коде есть ошибка. Вы явно ошиблись. Этого не может быть! Однако, оказывается, есть вполне разумное и простое объяснение этому. Но ответ на этот вопрос настолько важен, что стоит углубиться в него. Бьем молотком по собственному большому пальцу с помощью прогнозов Краткий ответ заключается в том, чтобы понять, чего мы хотим добиться с помощью этой политики, а именно – оптимизировать net_value путем манипуляций с номиналами купонов. Если представить это в виде графика, несложно представить, что зависимость net_value от номинала купонов будет квадратичной: по мере увеличения номинала купона net_value сначала увеличивается, достигает максимальной точки, а затем начинает снижаться, так как дальнейшее увеличение номинальной стоимости купона приносит больше затрат, чем прибыли.
­ 554  Когда прогнозирование не работает Поиск оптимального номинала купона эквивалентен поиску максимума функции net_value. Мы можем сделать это, продифференцировав функцию и приравняв производную к нулю (второй график). Экономисты могут увидеть в этом задачу ценообразования. Они (мы) решали бы эту задачу, предположив функциональную форму для net_value, дифференцируя ее и оптимизируя. У этого подхода есть свои достоинства, но он не является универсальным и требует множества предположений. К сожалению, реальные данные не сопровождаются явной функцией, которую можно дифференцировать, поэтому часто приходится полагаться на догадки. Более практичный подход (хотя и менее элегантный) заключается в том, чтобы протестировать различные номиналы купонов и зафиксировать, какой из них дает наибольшее значение net_value. Именно это и делает наша простая политика: она анализирует прошлые данные и повторяет воздействие, которое показало себя наиболее перспективным. Теперь сравним это с политикой, основанной на модели машинного обуче ния. Сначала такая политика создает модель машинного обучения для предсказания net_value. Затем она разбивает пространство клиентов в соответствии с прогнозами. Если модель является хорошей, ее прогнозы примерно эквивалентны разбиению пространства по самим значениям net_value, как показано на следующем графике.
Бьем молотком по собственному большому пальцу с помощью прогнозов  555 Чем лучше прогноз, тем ближе это разбиение пространства к разбиению по зависимой переменной net_value (деление клиентов будет таким же, как если бы мы делили их непосредственно по фактическому значению net_value). Обратите внимание на то, что происходит, когда вы это делаете. Фактически вы разбиваете клиентов на группы, в которых net_value – переменная, которую вы предсказали, не меняется! И это имеет смысл с точки зрения предсказаний: если ваша модель хороша в плане прогнозов, группы точек с одинаковыми прогнозами будут иметь одинаковое значение net_value. Все идет хорошо, но посмотрите, что такой подход делает с функциональной зависимостью net_value от номинала купонов (зеленые линии). Она выравнивается и не имеет наклона. С точки зрения прогнозов это замечательно, так как означает, что ваша модель учла всю вариацию net_value. Однако с точки зрения политики это ужасно, потому что в net_value не остается вариации, чтобы увидеть, как эта переменная меняется при изменении номинала купонов. Без этой вариации кажется, что изменение номинала купонов никак не влияет на net_value, лишая нас возможности оптимизации. Кстати, это общее явление, не связанное с конкретной квадратичной зависимостью, используемой здесь. Я лишь привел этот пример для того, чтобы сделать объяснение более конкретным. Я, смотрящая на свои плоские кривые отклика на лечение ИНТЕРЕСНО
556  Когда прогнозирование не работает Подведем итог: всякий раз, когда мы хотим оптимизировать какую-то переменную Y, используя переменную T, предсказание Y не только не поможет, но и навредит нашей политике, так как разбиения данных, определенные прогнозами, будут иметь ограниченную дисперсию Y, что затрудняет нашу возможность оценить, как T изменяет Y, т. е. эластичность . Это самый важный абзац данного приложения. Все в нем предельно важно, так что перечитайте его, если не поняли с первого раза. Ключ к исправлению этой ошибки заключается в корректировке нашей задачи, нам нужно оценить именно то, что мы действительно хотим оценить. Вместо оценивания Y, исходя из X, а именно это мы делаем с помощью прогнозов модели, нам нужно оценить , исходя из X. Легче сказать, чем сделать. Как вы уже догадались, именно в этом и заключается суть анализа причинно-следственных связей. И как это обычно бывает в задачах анализа причинно-следственных связей, мы не можем наблюдать нашу интересующую величину . Мы просто не можем наблюдать, как значение net_value изменилось бы, если бы мы изменили номинал купона, потому что мы наблюдаем только один случай выдачи купона на одного клиента. Мы никогда не узнаем, что бы произошло, если бы использовался другой номинал купона (если, конечно, не использовать симулированные данные, но это полезно только для учебных целей). Эта особенность причинно-следственных задач приводит к дальнейшим вопросам: как я могу узнать, хороша ли моя модель, если я не могу увидеть, что она предположительно оценивает? Как я могу валидировать такую модель? Кроме того, что делать, когда данные не являются случайными? Как оценить лучшую политику при смещенных данных? Это справедливые вопросы, и мы ответим на них со временем. Между тем помните, что при переходе от оценки Y к оценке многое придется изменить. Традиционный набор инструментов машинного обучения потребует некоторой адаптации. Когда прогноз может помочь Вся эта путаница с машинным обучением, мешающая нашей способности делать причинно-следственные выводы, происходит из-за неверной цели. Это происходит из-за оценки Y вместо . Но иногда можно использовать прогнозную модель для достижения цели анализа причинно-следственных связей. Для этого Y и должны быть как-то скоррелированы. Например, рассмотрим примеры на рисунке ниже.
­ Satisfaction'(WT) Net Value'(Coupon) Net Value Satisfaction Когда прогноз может помочь  557 Coupon Waiting Time (WT) Net Value Первый случай – это задача, которую мы видели раньше; мы смотрели, как номинал купона влияет на прибыль. Сначала по мере увеличения номинала купона прибыль увеличивается. Если они растут вместе, можно сказать, что они положительно скоррелированы. Более того, на графике производной по мере увеличения номинала купона эластичность уменьшается. Если объединить оба этих графика, то в первой области графика по мере увеличения net_value эластичность будет уменьшаться. Это означает, что и Y отрицательно скоррелированы. Но это только в начале. Если мы продолжим увеличивать номинал купона, прибыль будет снижаться, а эластичность прибыли также будет уменьшаться. Это означает, что и Y теперь положительно коррелированы. Так что прог нозная модель не поможет нам здесь, потому что нет прямой связи между результатом и эластичностью результата. Можно еще представить, что прогнозные модели делают срезы данных по оси Y. Если мы сделаем такие срезы, мы смешаем объекты, находящиеся как в области положительной эластичности, так и в области отрицательной эластичности. Теперь рассмотрим второй случай, когда мы хотим узнать, как ожидание в очереди для получения ответа от службы поддержки влияет на удовлетворенность клиентов. В этом случае мы видим, что удовлетворенность клиентов быстро падает в первые несколько минут ожидания. Клиенты очень раздражаются, когда им приходится ждать недолго, а то и совсем чуть-чуть.
558  Когда прогнозирование не работает Satisfaction Однако по мере увеличения времени ожидания удовлетворенность уже настолько низкая, что она не падает сильно дальше. Она как бы насыщается на низком уровне. Этот случай интересен тем, что связь между и Y сильно не меняется. Она всегда отрицательно скоррелирована. По мере увеличения времени ожидания удовлетворенность уменьшается, а эластичность удовлетворенности увеличивается. Или по мере увеличения T тоже увеличивается, а Y уменьшается. В этом случае прогнозная модель может быть полезной. Причина в том, что теперь, если мы делаем срезы по оси Y, мы будем группировать объекты с похожими эластичностями. В общем, всякий раз, когда у нас есть такие функциональные формы, где эластичность изменяется примерно в том же направлении, что и результат, можно использовать прогнозные модели. Однако будьте осторожны. Это не значит, что они являются вашим лучшим выбором или что вы должны использовать их для задач анализа причинно-следственных связей. Я просто упоминаю об этом здесь, чтобы вы могли понять, что происходит, когда вы сталкиваетесь с прогнозной моделью, которая пытается решить задачу анализа причинно-следственных связей. Объяснение с помощью графов Тот факт, что прогнозные модели могут быть вредны для персонализации, настолько контринтуитивен и странен, что я чувствую необходимость рассмотреть проблему с нескольких сторон. Следующее объяснение, полученное с помощью графов, – это способ понять, что у нас происходит, с другой точки зрения. Рассмотрим крайний левый граф причинно-следственных связей на рисунке ниже. Это типичный граф со спутывающей переменной, где у нас есть характеристики X, влияющие как на результат Y, так и на воздействие T. Что происходит, если вы строите прогнозную модель и используете ее прогнозы для сегментирования объектов?
Объяснение с помощью графов  559 X X T X T T Y M(T, X) M(T, X) Y Y Вы получаете нечто, подобное графам слева (технически они уже не являются причинно-следственными, потому что модель, очевидно, не является причиной результата. Она просто выучивает функцию, отображающую характеристики в результаты, но это не отменяет приведенных мною доводов). Сегментирование на основе прогнозов этой модели эквивалентно обусловленности узлом модели. Вы фиксируете M(T, X) в каждом сегменте. Если вы делаете это и пытаетесь исследовать связь между T и Y, фиксируя M(T, X), вы получите ситуацию, которую мы обсуждали в первой части, рассказывая о плохих контрольных переменных (фиксация M(T, X) обозначает контроль M(T, X)). И если вы не помните, напомню: когда вы контролируете что-либо на пути между воздействием и результатом, вы блокируете путь, по которому воздействие влияет на результат. В результате воспринимаемый эффект выглядит меньше реального эффекта. Так что вот вам еще одно объяснение, приводящее к тому же самому заключению: сегментирование объектов на основе прогнозов модели машинного обучения мешает нашей способности определить причинно-следственный эффект. Теперь, если вы умны, вы, вероятно, возразите следующим образом. Что, если мы просто уберем T из модели? Это действительно хороший аргумент. Настолько хороший, что мне потребовались годы, чтобы правильно объяснить, почему попытки обмануть вашу модель подобным образом, вероятно, являются не самым разумным решением. На самом деле если вы вернетесь к примеру, который мы рассмотрели в этом приложении, то заметите, что я даже не передавал в прогнозную модель переменную воздействия coupons. Это не решило проблему, заключающуюся в том, что прогнозы модели выравнивают нашу измеренную эластичность до горизонтальной линии. Ответ на данный вопрос показан на следующем рисунке.
­ 560  Когда прогнозирование не работает X X T T M(X) X M(X) T Y Y M(X) Y Исключение воздействия T из вашей модели M(X) недостаточно, чтобы модель игнорировала связь между T и Y. Поскольку X вызывает T, модель может косвенно выучить T через X. Почему это происходит? Потому что если T вызывает Y и M хочет максимально точно предсказать Y, она будет вынуждена исследовать закономерности в X, которые связывают T с Y. Другими словами, информация о том, как воздействие влияет на результат, передается обратно в нашу модель через признаки. Например, предположим, что вы даете скидки (T) только клиентам, проживающим в определенном районе города (X). Допустим, что район не влияет на продажи (Y ) напрямую. Даже в этом случае район будет иметь предсказательную силу, потому что он вызывает воздействие T, которое, в свою очередь, вызывает Y. Таким образом, модель будет использовать этот факт для предсказания Y. Ради справедливости скажу, что вы действительно можете немного обмануть свою модель, не передав ей T. В этих целях я рекомендую вам перезапус тить код на наших примерах, но на этот раз включите coupons в список признаков для прогнозной модели. Вы увидите, что политика на основе модели будет работать еще хуже. Это означает, что, НЕ передавая T в нашу модель, мы делаем что-то полезное, хотя и недостаточно полезное. Наконец, существует один способ избежать вышеописанной ситуации – это когда воздействие T является случайным. В этом случае прогнозная модель может действительно помочь вам с анализом причинно-следственных связей. Но сначала давайте поймем, почему это не вредит. Когда T случайно, это исключает любое спутывание, исходящее от X. В этом случае X не обладает информацией о том, как возникает T. X T M(X) Y X T M(X) Y
Ключевые идеи  561 Когда это происходит, даже если вы контролируете M(X), вы все равно позволяете всему эффекту от T переходить на Y. Другими словами, даже если вы рассматриваете сегменты, где M(X) фиксирован, Y все равно будет изменяться из-за T. M(X) больше не может контролировать эту изменчивость, потому что у него нет способа выучить T. Более того, это условие действительно поможет вам определить средний эффект воздействия. Мы уже видели это в части I. Если мы контролируем переменные, которые являются хорошими предикторами Y, но не вызывают T, мы снизим дисперсию наших каузальных оценок. Интуитивная причина заключается в том, что большая часть вариации в Y, вызванная X, устраняется в силу обусловленности моделью. Поскольку X таким образом объясняется, причина, по которой Y все еще изменяется, должна быть в основном обусловлена воздействием T (или другими неучтенными факторами, но вы понимаете, что это помогает). Опять же, это не аргумент в пользу использования прогнозных моделей, когда цель заключается в анализе причинно-следственных связей. На самом деле сегменты, определенные прогнозной моделью, помогут вам идентифицировать ATE, но это не означает, что у каждого сегмента будет разное значение ATE (или эластичность). Помните, что вам нужно не только оценить эластичность, но и найти сегменты, где она выше или ниже среднего, чтобы вы могли персонализировать воздействие. Ключевые идеи Мы увидели, что существует целый ряд задач, которые не решаются с помощью традиционного прогнозного машинного обучения. Эти задачи являются оптимизационными, с причинно-следственным компонентом, которые обычно формулируются примерно так: «как я должен задать контролируемый параметр T (цену, количество звонков, скидку и т. д.), чтобы оптимизировать показатель Y (прибыль, удовлетворенность клиентов, конверсию, затраты и т. д.)». В этом случае прогнозные модели могут причинить больше вреда, чем пользы. Чтобы доказать это, мы рассмотрели пример, где нам нужно было найти оптимальный номинал купона. Первое, что мы попробовали, – мы посмотрели на прошлые данные и выяснили, какой номинал купона дает наибольший доход. Затем мы взяли купоны с этим предположительно оптимальным номиналом и раздали каждому клиенту. Этот подход отвечает на вопрос, каким будет наилучший номинал купона (наилучшее значение воздействия, если говорить в терминах анализа причинно-следственных связей) в среднем. То есть если бы нам нужно было выбрать только один фиксированный номинал купона, то каким бы он был? Но у нас нет такого ограничения. Если некоторые клиенты лучше реагируют на купоны с низким номиналом, а некоторые – на купоны с более высоким номиналом, мы могли бы персонализировать купон для каждого клиента. Для этого мы попробовали построить прогнозную модель машинного обучения – модель, которая прогнозирует
­ 562  Когда прогнозирование не работает значение net_value, которое каждый клиент принесет. Затем мы попытались персонализировать номинал купона на основе прогнозов модели. Мы сгруппировали клиентов согласно прогнозам и для каждой группы нашли оптимальный номинал купона. Потом мы попробовали использовать эти значения в качестве стратегии раздачи купонов. Однако мы обнаружили, что это показало худший результат, чем простая стратегия, в ходе которой мы раздавали всем купоны с одним и тем же номиналом. Другими словами, наша модель машинного обучения не смогла разделить клиентов, которые лучше или хуже реагировали на номинал купона. Обратите внимание, что проблема, которую мы пытаемся решить здесь, является проблемой персонализации. В нашем случае это попытка выяснить, какой номинал купона является лучшим для каждого клиента. В целом это формулируется как «какое лучшее решение T я могу принять для каждого клиента i, чтобы максимизировать Yi по всем клиентам». Это нелегкая задача. Но мы можем решить ее с помощью анализа причинно-следственных связей. Есть еще одна важная вещь, которую вы должны усвоить из этого приложения. Во-первых, обратите внимание, что анализ причинно-следственных связей, рассмотренный нами в первой части, помогает вам разрабатывать политики, аналогичные ранее созданной простой политике. С их помощью мы пытаемся оценить средний причинно-следственный эффект воздействия и найти воздействие, которое работает лучше всего в среднем. Теперь наши амбиции возросли. Нас не интересует лучшее воздействие в среднем, нам нужно найти лучшее воздействие для каждого отдельного клиента. Это и есть суть персонализации, и она сильно зависит от методов причинно-следственного анализа. Дополнительное чтение Материал, который я написал в этом приложении, в основном является результатом моего опыта. Я узнал обо всем этом на практике. Это означает, что приведенная здесь информация не прошла проверку, которой подвергается хорошая наука. Тем не менее обратите внимание, что я говорю о вещах, которые работают на практике, но я не трачу слишком много времени на объяснение того, почему это так. Это своего рода «уличная наука», если хотите. Однако я выставляю данный материал на суд публике, так что если вы найдете что-то нелепое, задавайте вопрос (заводите issue на Github), и я постараюсь разобраться в нем наилучшим образом. Для этой главы у меня немного ссылок, которые я мог бы указать. Есть только одна отличная статья 2016 года, написанная Сьюзан Эти (Susan Athey) и Гвидо У. Имбенсом (Guido W. Imbens) «Recursive Partitioning for Heterogeneous Causal Effects» (https://arxiv.org/abs/1504.01132), в ней сформулирована задача персонализации, которую мы пытаемся решить с помощью анализа причинно-следственных связей. Кроме того, первая картинка взята из видео ролика на YouTube от Сираджа Равала (Siraj Raval), https://www.youtube.com/ watch?v=QfNvhPx5Px8. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Приложение 4 Когда прогнозные метрики опасны для причинноследственных моделей Распространенное заблуждение, которое я часто слышу, заключается в том, что если у нас есть случайные данные, то для оценки качества причинноследственной модели мы можем просто оценить предсказательную способность такой модели на случайном наборе данных, используя метрику типа R2. К сожалению, все не так просто, и я постараюсь объяснить причины. В общем случае мы можем сказать, что любой результат является функцией от воздействия и ковариат: Y = F(x, t). Предположим, мы можем разложить эту функцию на две аддитивные части. Одна часть не зависит от воздействия, а другая зависит только от воздействия и возможных взаимодействий: Y = g(x) + f(t, x). Эта аддитивная структура накладывает некоторые ограничения на функциональную форму, но незначительные, поэтому мы можем утверждать, что это достаточно общий способ описания процесса, генерирующего данные (Data Generating Process – DGP). Суть заключается в том, что если эффект воздействия слабее, чем эффект ковариат, то, даже если у нас есть случайные данные, мы можем получить модель, которая имеет высокую прогнозную силу, но плоха для анализа причинно-следственных связей. Все, что должна делать такая модель, – это аппроксимировать g(x), игнорируя f(t, x).
564  Когда прогнозные метрики опасны для причинно-следственных моделей Другими словами, предсказательная способность на случайном наборе данных не отражает степень пригодности модели для анализа причинно-следственных связей. Чтобы показать это, давайте используем симулированные данные. Симулируем данные Следующий процесс, генерирующий данные, предполагает наличие ковариат X, которые имеют высокую прогнозную силу, но не взаимодействуют с воздействием. Другими словами, они не диктуют гетерогенность эффекта воздействия. Кроме того, у нас есть признаки W, которые влияют на результат только через воздействие (т. е. не являются спутывающими переменными). Поскольку воздействие имеет низкую прогнозную силу, W также не обладают большой прогнозной силой: Yi = g(Xi) + f(T i, Wi) + ei. import pandas as pd import numpy as np from matplotlib import pyplot as plt import statsmodels.formula.api as smf from toolz import * n = 100000 n_features = 20 n_heter = 10 np.random.seed(12321) X = np.random.normal(1, 10, (n, n_features)) nuisance = np.random.uniform(-1,1, (n_features, 1)) W = np.random.normal(1, 10, (n, n_heter)) heter_y = np.random.uniform(-1,1, (n_heter, 1)) T = np.random.normal(10, 2, (n, 1)) # T случайно! Y = np.random.normal(T + T*W.dot(heter_y) + 20*X.dot(nuisance), 0.1) df = pd.concat([ pd.DataFrame(X, columns=[f"f{f}" for f in range(n_features)]), pd.DataFrame(W, columns=[f"w{f}" for f in range(n_heter)]) ], axis=1).assign(T=T, Y=Y) Давайте разобьем набор данных на обучающую и тестовую выборки. from sklearn.model_selection import train_test_split train, test = train_test_split(df, test_size=0.5) train.shape, test.shape ((50000, 32), (50000, 32))
Симулируем данные  565 Далее обучим две модели для оценки гетерогенности эффекта воздействия, M1 и M2. M1 будет включать признаки с высокой прогнозной силой, которые не влияют на гетерогенность воздействия, а M2 будет включать признаки с низкой прогнозной силой, которые влияют на гетерогенность воздействия. m1 = smf.ols( "Y~T*(" + "+".join([f"f{f}" for f in range(n_features)])+")", data=df).fit() m2 = smf.ols( "Y~T*(" + "+".join([f"w{f}" for f in range(n_heter)])+")", data=df).fit() Если мы посмотрим прогнозную силу обеих моделей, используя R2, то действительно M1 намного лучше, чем M2. from sklearn.metrics import r2_score print("M1:", r2_score(test["Y"], m1.predict(test))) print("M2:", r2_score(test["Y"], m2.predict(test))) M1: 0.9160516511287359 M2: 0.08378351037639298 Теперь рассчитаем кривую накопленной эластичности для обеих моделей. Для этого нам понадобятся прогнозы условного среднего эффекта лечения. Поскольку все модели здесь являются линейными, мы можем воспользоваться следующей формулой для получения прогноза CATE: CÂTEi = M(X, W, t) – M(X, W, t – 1). @curry def elast(data, y, t): return (np.sum((data[t] - data[t].mean()) * (data[y] - data[y].mean())) / np.sum((data[t] - data[t].mean())**2)) def cumulative_gain(dataset, prediction, y, t, min_periods=30, steps=100): size = dataset.shape[0] ordered_df = dataset.sort_values( prediction, ascending=False).reset_index(drop=True) n_rows = list( range(min_periods, size, size // steps) ) + [size] return np.array( [elast(ordered_df.head(rows), y, t) * (rows/size) for rows in n_rows] ) test_pred = test.assign(
­ 566  Когда прогнозные метрики опасны для причинно-следственных моделей cate_1 = m1.predict(test) - m1.predict(test.assign(T=test["T"]-1)), cate_2 = m2.predict(test) - m2.predict(test.assign(T=test["T"]-1)) ) Получив прогнозы CATE, мы можем оценить их с помощью кривой накоп ленной эластичности. cumelast_1 = cumulative_gain(test_pred, "cate_1", "Y", "T", steps=100) cumelast_2 = cumulative_gain(test_pred, "cate_2", "Y", "T", steps=100) plt.plot(range(len(cumelast_1)), cumelast_1, label="M1") plt.plot(range(len(cumelast_2)), cumelast_2, label="M2") plt.plot([0, 100], [0, elast(test_pred, "Y", "T")], linestyle="--", label="Случайная модель", color="black") plt.legend() plt.ylabel("Накопленная эластичность"); Как мы видим, сейчас M1 работает гораздо хуже, чем M2, несмотря на то что у нее более высокое значение R2 на тестовом наборе. Это говорит о том, что прогнозная сила не переходит напрямую в хорошую причинно-следственную модель, даже если мы используем случайные данные. Прогнозные метрики для анализа причинно-следственных связей Это не значит, что мы не можем придумать способ корректной оценки причинно-следственной модели с помощью прогнозных метрик. Для этого да-
­ Прогнозные метрики для анализа причинно-следственных связей  567 вайте вернемся к нашему аддитивному предположению о процессе, генерирующем данные: Y = g(x) + f(t, x). Чтобы использовать прогнозную метрику, нам нужно каким-то образом преобразовать результат, дабы удалить из него компоненту g(x). Таким образом, вся оставшаяся прогнозная сила обязательно будет использована для изучения причинно-следственной связи. Ỹ = Y – g(x) = f(t, x). Один из способов сделать это – ортогонализация Y. Мы можем воспользоваться любой моделью ML для оценки g и получить остатки Ỹ = Y – ĝ(x). denoise_m = smf.ols( "Y~"+ "+".join([f"w{f}" for f in range(n_heter)])+ "+"+ "+".join([f"f{f}" for f in range(n_features)]), data=train).fit() test_res = test.assign( Y_res = test["Y"] - denoise_m.predict(test) + test["Y"].mean() ) После этого мы можем оценить силу каждой модели с точки зрения прог нозирования остатков результата Ỹ. Хорошее качество прогнозирования этой модифицированной переменной результата будет напрямую указывать на лучшую причинно-следственную модель. print("M1:", r2_score(test_res["Y_res"], m1.predict(test_res))) print("M2:", r2_score(test_res["Y_res"], m2.predict(test_res))) M1: -281.79501850924845 M2: -24.049124816545746 Недостаток этого подхода заключается в том, что он зависит от качества прогнозирования g(x). Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Приложение 5 Конформный вывод для синтетического контроля Освежаем знания о синтетическом контроле Синтетический контроль (Synthetic Control – SC) – это особенно полезный метод анализа причинно-следственных связей, когда у вас есть один тестовый объект (объект, подвергнутый воздействию) и очень мало контрольных объектов, но для каждого объекта имеются повторяющиеся во времени наблюдения (хотя существует множество расширений SC для больших данных). Классический пример использования синтетического контроля – когда вы хотите узнать влияние воздействия в одной географической области (например, в штате) и используете другие штаты, не подвергнутые воздействию, в качестве контрольных объектов. В нашей главе о синтетическом контроле мы проиллюстрировали этот метод, пытаясь оценить эффект от принятия Положения 99 (закона, принятого в 1988 году, который увеличил налог на сигареты в Калифорнии) на продажи сигарет. Чтобы сделать это, нам нужно оценить, что бы произошло в Калифорнии, если бы не было принято Положение 99. Это сводится к оценке контрфактического результата Yt(0), чтобы мы могли сравнить его с наблюдаемым результатом в периоды после вмешательства:
Освежаем знания о синтетическом контроле  569 ATT = Yt(1) – Yt(0) = Yt – Yt(0) для t ≥ 1988. Существует множество методов выполнить эту процедуру, среди них – синтетический контроль. Синтетический контроль пытается смоделировать Y(0) для тестового объекта, комбинируя несколько контрольных объектов таким образом, чтобы они имитировали поведение тестового объекта до воздействия. В нашем случае речь идет о поиске комбинации штатов, которые все вместе аппроксимируют тренд продаж сигарет в Калифорнии до принятия Положения 99. Данная процедура обусловлена тем, что у нас редко бывает контрольный объект, который подчиняется тому же самому паттерну, что и тестовый объект. Мы можем увидеть это, построив график тренда продаж сигарет для нескольких штатов. Обратите внимание, что ни у одного из них нет тренда, который точно соответствует Калифорнии. import numpy as np import pandas as pd from toolz import curry import seaborn as sns from matplotlib import pyplot as plt import statsmodels.formula.api as smf import cvxpy as cp import toolz as f from sklearn.linear_model import Lasso import warnings warnings.filterwarnings('ignore') from matplotlib import style style.use("ggplot") data = pd.read_csv("data/smoking.csv") data = data.pivot("year", "state", "cigsale") data = data.rename( columns={c: f"state_{c}" for c in data.columns}).rename( columns={"state_3": "california"} ) data.shape (31, 39) plt.figure(figsize=(10,5)) plt.plot(data.drop(columns=["california"]), color="C1", alpha=0.5) plt.plot(data["california"], color="C0", label="Калифорния") plt.vlines(x=1988, ymin=40, ymax=300, linestyle=":", lw=2, label="Положение 99", color="black") plt.legend() plt.ylabel("Продажи сигарет");
570  Конформный вывод для синтетического контроля Вот почему мы комбинируем несколько контрольных объектов. Цель состоит в том, что если у нас нет достаточно хорошего контрольного объекта, мы могли создать синтетический, который будет напоминать нужный нам тестовый объект. Чтобы найти комбинацию штатов, которая лучше всего аппроксимирует тренд до воздействия в Калифорнии, метод синтетического контроля выполняет горизонтальную регрессию, в которой строки представляют собой временные периоды, а столбцы – штаты. Он пытается найти веса, которые при умножении на контрольные штаты лучше всего аппроксимируют тестовый штат. Объекты W Y = Объект 1 t1 Время t2 t3 t4 t5 t6 Поскольку у нас больше штатов (39, некоторые были исключены из анализа), чем временных периодов, регрессия без ограничений просто привела бы к переобучению, поэтому метод синтетического контроля накладывает два ограничения: 1) веса должны в сумме давать 1; 2) веса должны быть неотрицательными.
Освежаем знания о синтетическом контроле  571 Или, выражаясь математическими терминами, пусть y – это вектор результатов для штата, подвергнутого воздействию, в периоды до воздействия, X – это матрица размером J на T0, в которой каждый столбец – это штат j, а каждая строка – период t до периода воздействия, T1 = T0 + 1: при условии что ∑wj = 1 и wj > 0 ∀ j. В совокупности эти ограничения означают, что мы определяем синтетический контроль как выпуклую комбинацию контрольных объектов. Это также означает, что мы не делаем опасных экстраполяций и что наш синтетический контроль будет использовать только небольшое подмножество контрольных объектов. Вот как это выглядит в программном коде, в виде модели sklearn: from sklearn.base import BaseEstimator, RegressorMixin from sklearn.utils.validation import (check_X_y, check_array, check_is_fitted) import cvxpy as cp class SyntheticControl(BaseEstimator, RegressorMixin): def __init__(self,): pass def fit(self, X, y): X, y = check_X_y(X, y) w = cp.Variable(X.shape[1]) objective = cp.Minimize(cp.sum_squares(X@w - y)) constraints = [cp.sum(w) == 1, w >= 0] problem = cp.Problem(objective, constraints) problem.solve(verbose=False)
572  Конформный вывод для синтетического контроля self.X_ = X self.y_ = y self.w_ = w.value self.is_fitted_ = True return self def predict(self, X): check_is_fitted(self) X = check_array(X) return X @ self.w_ Давайте применим эту модель к нашим данным, обучив ее на периоде до вмешательства (до 1988 года). model = SyntheticControl() train = data[data.index < 1988] model.fit(train.drop(columns=["california"]), train["california"]); Теперь мы можем построить графики трендов для Калифорнии и для синтетического контроля, который мы только что создали. Разница между этими двумя линиями – это оцененный эффект от принятия Положения 99 в Калифорнии. plt.plot(data["california"], label="Калифорния") plt.plot(data["california"].index, model.predict(data.drop(columns=["california"])), label="Синтетический контроль") plt.vlines(x=1988, ymin=40, ymax=120, linestyle=":", lw=2, label="Положение 99", color="black") plt.legend();
­ Выводы для взрослых  573 Судя по этому графику, похоже, что Положение 99 оказало значительное влияние на снижение продаж сигарет. pred_data = data.assign(**{"residuals": data["california"] model.predict(data.drop(columns=["california"]))}) plt.plot(pred_data["california"].index, pred_data["residuals"], label="Оцененный эффект") plt.hlines(y=0, xmin=1970, xmax=2000, lw=2, color="Black") plt.vlines(x=1988, ymin=5, ymax=-25, linestyle=":", lw=2, label="Положение 99", color="Black") plt.legend(); Выводы для взрослых В главе, посвященной синтетическому контролю, мы показали процедуру, в рамках которой мы выполняли пермутацию объектов, делая вид, что конт рольные объекты были подвергнуты воздействию. Эта процедура также известна как плацебо-тест, в рамках которого мы проверяем эффект объектов, которые не подвергались воздействию. Если оцененный эффект тестового объекта выше, чем самый большой из плацебо-эффектов, мы считаем, что эта оценка эффекта является значимой. plt.figure(figsize=(10,5)) for state in data.columns: model_ier = SyntheticControl()
574  Конформный вывод для синтетического контроля train_iter = data[data.index < 1988] model_ier.fit(train_iter.drop(columns=[state]), train_iter[state]) effect = data[state] - model_ier.predict(data.drop(columns=[state])) is_california = state == "california" plt.plot(effect, color="C0" if is_california else "C1", alpha=1 if is_california else 0.5, label="Калифорния" if is_california else None) plt.hlines(y=0, xmin=1970, xmax=2000, lw=2, color="Black") plt.vlines(x=1988, ymin=-50, ymax=100, linestyle=":", lw=2, label="Положение 99", color="Black") plt.ylabel("Оценка эффекта") plt.legend(); В нашем примере мы видим, что разница после воздействия для Калифорнии весьма значительна по сравнению с другими штатами. Однако есть несколько штатов с очень плохим соответствием в период до воздействия, что затем приводит к огромной ошибке в период после вмешательства. В этом случае нужно удалить объекты с высокой ошибкой до воздействия, но определить, какая величина ошибки является приемлемой, немного сложнее. Кроме того, эта процедура предполагает случайное назначение воздействия, что неправдоподобно для такого рода политических мер (см. Abadie, 2021). Один из альтернативных методов заключается в том, чтобы переосмыслить задачу оценивания эффекта как прогнозирование контрфактического результата. Если подумать, все, что мы пытаемся сделать, – это спрогнозировать контрфактический результат Yi,t(0), где i – это тестовый объект и t ≥ T1, то есть в период после вмешательства. Если мы это сделаем, мы можем воспользоваться литературой по конформному прогнозированию (con-
Тестирование гипотез и p-значения  575 formal prediction). Достаточно интересно, что этот метод является довольно универсальным и применим к другим моделям Yi,t(0), но давайте здесь сосредоточимся только на синтетическом контроле. Чтобы понять эту процедуру, сначала посмотрим, как нужно проводить тестирование гипотез и вычислять p-значения. Тестирование гипотез и p-значения Допустим, нас интересует проверка гипотезы о траектории эффектов в период после воздействия θ = (θT0+1, …, θT): H 0 : θ = θ 0. Например, если мы хотим протестировать отсутствие какого-либо эффекта, мы можем задать θ 0 = (0, …, 0) . Обратите внимание, что эта гипотеза полностью определяет контрфактический результат при отсутствии воздействия: Yt(0) = Yt(1) – θt = Yt – θt. Ключевая идея заключается в том, чтобы сгенерировать данные, соответствующие нулевой гипотезе, которую мы хотим протестировать, и проверить остатки модели для Y(0), построенной на этих сгенерированных данных. Если остатки слишком экстремальны, мы говорим, что данные вряд ли соответствуют выдвинутой нами нулевой гипотезе. Если эта процедура покажется вам на первый взгляд непонятной, не волнуйтесь, все станет ясно, по мере того как мы будем пошагово реализовывать ее. Первым шагом является генерация данных согласно нулевой гипотезе. Мы вычитаем данные, соответствующие нулевой гипотезе, из результата тестового объекта, как в вышеприведенном уравнении. Вот программный код, реализующий первый шаг: def with_effect(df, state, null_hypothesis, start_at, window): window_mask = (df.index >= start_at) & (df.index < (start_at + window)) y = np.where(window_mask, df[state] - null_hypothesis, df[state]) return df.assign(**{state: y}) plt.plot(with_effect(data, "california", 0, 1988, 2000-1988+1)["california"], label="H0: 0") plt.plot(with_effect(data, "california", -4, 1988, 2000-1988+1)["california"], label="H0: -4") plt.ylabel("Y0 согласно нулевой гипотезе") plt.legend();
576  Конформный вывод для синтетического контроля Если мы выдвигаем нулевую гипотезу об отсутствии эффекта, то данные согласно этой нулевой гипотезе означают, что Y(0) = Y(1) = Y, это просто траектория наблюдаемого результата, который мы видим для тестового штата Калифорния. Сейчас, если мы считаем, что нулевая гипотеза равна –4, то есть Положение 99 уменьшает продажи сигарет на 4 пачки, то Y(0) = Y(1) – (–4), что смещает траекторию результатов после воздействия на +4. Это интуитивно понятно. Если мы считаем, что закон уменьшает продажи сигарет, то в случае его отсутствия мы должны наблюдать более высокие продажи сигарет, чем те, которые у нас есть в наших наблюдаемых данных. Следующий шаг – это обучение модели для вычисления контрфактического результата Y(0) (получаем с помощью только что написанной функции) на всех данных, как до, так и после периода вмешательства. Это важное отличие от обычного способа обучения моделей синтетического контроля. Идея здесь заключается в том, что модель должна оцениваться на всех данных, в рамках сформулированной нулевой гипотезы, чтобы избежать огромных остатков после вмешательства. С помощью этой модели мы затем вычисляем остатки ût = Yt – Ŷt (0) для всех временных периодов t. Функция residuals(), вычисляющая остатки, сначала использует функцию with_effect(), которую мы создали ранее, чтобы сгенерировать данные согласно нулевой гипотезе. Затем она обучает модель на этих данных, полученных согласно нулевой гипотезе. Далее мы оцениваем Y(0), делая прогнозы с помощью только что обученной модели. Наконец, мы вычисляем остатки ût и сохраняем все в датафрейме. @curry def residuals(df, state, null, intervention_start, window, model): null_data = with_effect(df, state, null, intervention_start, window)
­ Тестирование гипотез и p-значения  577 model.fit(null_data.drop(columns=[state]), null_data[state]) y0_est = pd.Series(model.predict(null_data.drop(columns=[state])), index=null_data.index) residuals = null_data[state] - y0_est test_mask = (null_data.index >= intervention_start) & ( null_data.index < (intervention_start + window) ) return pd.DataFrame({ "y0": null_data[state], "y0_est": y0_est, "residuals": residuals, "post_intervention": test_mask # возвращаем наблюдения датафрейма # до начала вмешательства + окно })[lambda d: d.index < (intervention_start + window)] В случае с нашими данными, чтобы получить остатки для H0 : 0 (что озна чает, что Положение 99 не оказало эффекта), мы можем просто передать 0 в качестве нулевой гипотезы, используя параметр null нашей функции residuals(). model = SyntheticControl() residuals_df = residuals(data, "california", null=0.0, intervention_start=1988, window=2000-1988+1, model=model) residuals_df.head() Результатом становится датафрейм, содержащий вычисленные остатки для каждого временного периода, которые мы будем использовать в дальнейшем. Помните, что идея здесь состоит в том, чтобы выяснить, не слишком ли большая величина у этого остатка в период после вмешательства. Если это так, то маловероятно, что данные подчиняются нулевой гипотезе, согласно которой эффект равен нулю. Чтобы получить наглядное представление, мы
­ 578  Конформный вывод для синтетического контроля можем визуально проанализировать ошибку нашей модели в период после вмешательства. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 4)) residuals_df[["y0", "y0_est"]].plot(ax=ax1) ax1.set_title("Y0 согласно H0: 0"); residuals_df[["residuals"]].plot(ax=ax2); ax2.set_title("Остатки согласно H0: 0"); Мы видим, что модель, обученная в соответствии с H0 : 0, дает довольно большие отрицательные остатки, это является некоторым доказательством того, что мы, возможно, захотим отвергнуть нулевую гипотезу об отсутствии эффекта. Тестовая статистика Эти наглядные доказательства упрощают понимание происходящего, но нам нужно быть более точными. Это достигается путем определения тестовой статистики S, которая суммирует остатки и, следовательно, показывает, насколько маловероятны наблюдаемые данные согласно нулевой гипотезе: Здесь мы фокусируемся на q = 1, что дает нам S(û) = ∑ Tt=T0+1|ut|. Обратите внимание, что эта статистика рассчитывается только для перио да после вмешательства, с t ≥ T0 + 1. Таким образом, хотя мы используем все данные, чтобы обучить нашу модель для вычисления контрфактического результата Y(0), остатки проверяются только для результатов, которые касаются сформулированной нулевой гипотезы, т. е. нас интересует период после вмешательства. def test_statistic(u_hat, q=1, axis=0): return (np.abs(u_hat) ** q).mean(axis=axis) ** (1/q)
­ p-значение  579 print("H0:0 ", test_statistic( residuals_df.query("post_intervention")["residuals"])) H0:0 12.602929955114083 Высокое значение этой тестовой статистики указывает на плохое качество подгонки для периода после вмешательства и, следовательно, на отклонение нулевой гипотезы. Однако у нас могут быть довольно большие значения тес товой статистики в период после вмешательства при плохом качестве подгонки модели, даже если H0 верна. Это значит, что мы не можем определить в абсолютных терминах, что является «высоким значением». Вместо этого мы должны выяснить, насколько высоки остатки в период после вмешательства по сравнению с остатками в период до вмешательства. p-значение Для вычисления p-значения мы перемешиваем остатки блоками, вычисляя тестовую статистику в каждой перестановке. Этот процесс проще понять с помощью следующей иллюстрации. Когда мы сделаем это, мы получим T тестовых статистик, по одной для каждой блочной перестановки. Пусть Π – множество всех блочных перестановок, тогда p-значение вычисляется по формуле:
580  Конформный вывод для синтетического контроля где 1{S(ûπ0 ) ≤ S(ûπ)} – индикаторная функция, которая равна 1, если тестовая статистика для исходных (непермутированных) остатков S(ûπ0 ) меньше или равна тестовой статистике для пермутированных остатков S(ûπ), и 0 в противном случае. В итоге мы находим долю случаев, когда тестовая статистика для исходных остатков меньше или равна тестовой статистике для остатков, вычисленных для той или иной перестановки. Для реализации формулы мы воспользуемся функцией np.roll(), которая берет массив и обходит его «по кругу» (циклически сдвигает элементы массива на заданное количество позиций), как показано на рисунке выше. def p_value(resid_df, q=1): u = resid_df["residuals"].values post_intervention = resid_df["post_intervention"].values block_permutations = np.stack( [np.roll(u, permutation, axis=0)[post_intervention] for permutation in range(len(u))] ) statistics = test_statistic(block_permutations, q=1, axis=1) p_val = np.mean(statistics >= statistics[0]) return p_val Теперь мы можем вычислить p-значение для H0 : 0. Как мы видим, оно низкое, но не экстремально низкое. При уровне значимости α = 0.1 мы не можем отклонить нулевую гипотезу об отсутствии эффекта. p_value(residuals_df) 0.16129032258064516 Вспомним, что речь идет о p-значении для нулевой гипотезы, которая утверждает, что эффект во все периоды времени равен нулю: θ = (θT0+1 = 0, …, θT = 0). Глядя на график эффекта, оцененного по методу синтетического контроля, мы можем заключить, что эффект от Положения 99 не является фиксированным числом. Мы видим, что он начинается с небольшого значения, около –5, но постепенно увеличивается до –25. По этой причине интереснее построить доверительный интервал для эффекта каждого периода после воздействия, а не просто проверять нулевую гипотезу о траектории эффекта в целом. Доверительные интервалы Чтобы понять, как можно построить доверительный интервал эффекта для каждого периода после воздействия, давайте сначала попробуем понять, как
­ Доверительные интервалы  581 бы мы определили доверительный интервал для одного временного периода. Если у нас есть один период, то H0 задается как скалярное значение, а не как вектор траектории θ. Это значит, что мы можем сгенерировать ряд нулевых гипотез H0 и вычислить p-значение, связанное с каждой нулевой гипотезой. Предположим, мы думаем, что эффект от Положения 99 в 1988 году (в год его принятия) находится в диапазоне от –20 до 20. Тогда мы можем создать таблицу, содержащую множество нулевых гипотез H0 от –20 до 20 и каждое связанное с ними p-значение: P-value(H_0: P-value(H_0: P-value(H_0: ... P-value(H_0: P-value(H_0: P-value(H_0: -20) = 0.01 -19) = 0.01 -18) = 0.02 18) = 0.03 19) = 0.03 20) = 0.02 С помощью написанных нами функций это можно реализовать, сначала добавив интересующий период (в этом примере – 1988 год) в конец перио да до вмешательства, создавая так называемый аугментированный набор данных. Затем мы перебираем линейный ряд нулевых гипотез, вычисляя p-значение для окна после вмешательства размером 1, которое начинается с интересующего нас периода. def p_val_grid(df, state, nulls, intervention_start, period, model): df_aug = df[df.index < intervention_start].append(df.loc[period]) p_vals = {null: p_value(residuals(df_aug, state, null=null, intervention_start=period, window=1, model=model)) for null in nulls} return pd.DataFrame(p_vals, index=[period]).T model = SyntheticControl() nulls = np.linspace(-20, 20, 100) p_values_df = p_val_grid( data, "california", nulls=nulls, intervention_start=1988, period=1988, model=model ) p_values_df
582  Конформный вывод для синтетического контроля Как вы видите, результат представляет собой таблицу, в которой индекс строки – это нулевая гипотеза, а значения в строках – это p-значения. Чтобы построить доверительный интервал, нам нужно просто отфильтровать те нулевые гипотезы H0, которые дали нам низкое p-значение. Помните: низкое p-значение означает, что имеющиеся у нас данные с малой вероятностью могли быть получены согласно данной нулевой гипотезе. Например, если мы зададим уровень значимости равным 0.1, мы исключим те нулевые гипотезы H0, у которых p-значение меньше 0.1. def confidence_interval_from_p_values(p_values, alpha=0.1): big_p_values = p_values[p_values.values >= alpha] return pd.DataFrame({ f"{int(100-alpha*100)}_ci_lower": big_p_values.index.min(), f"{int(100-alpha*100)}_ci_upper": big_p_values.index.max(), }, index=[p_values.columns[0]]) confidence_interval_from_p_values(p_values_df) Это дает нам доверительный интервал для эффекта в 1988 году. Мы также можем визуализировать нулевые гипотезы H0 по p-значению, чтобы лучше понять, как был получен этот доверительный интервал. На графике ниже пунктирная линия соответствует значению 0.1, что является заданным нами уровнем значимости α. Синие линии обозначают доверительные интервалы. Нулевые гипотезы H0 за пределами этих линий имеют p-значения ниже 0.1. plt.plot(p_values_df[1988], p_values_df.index) plt.xlabel("p-значение")
Доверительные интервалы  583 plt.ylabel("H0") plt.vlines(0.1, nulls.min(), nulls.max(), color="black", ls="dotted", label="0.1") plt.hlines(confidence_interval_from_p_values(p_values_df)["90_ci_upper"], 0, 1, color="C1", ls="dashed") plt.hlines(confidence_interval_from_p_values(p_values_df)["90_ci_lower"], 0, 1, color="C1", ls="dashed", label="90%-ный ДИ") plt.legend() plt.title("Доверительный интервал для эффекта в 1988"); Остается только повторить вышеописанную процедуру для каждого временного периода. Это означает, что каждый год после вмешательства необходимо добавить в конец периода до вмешательства, чтобы создать аугментированный набор данных, а затем вычислить доверительный интервал так же, как мы делали выше.
584  Конформный вывод для синтетического контроля def compute_period_ci(df, state, nulls, intervention_start, period, model, alpha=0.1): p_vals = p_val_grid(df=df, state=state, nulls=nulls, intervention_start=intervention_start, period=period, model=model) return confidence_interval_from_p_values(p_vals, alpha=alpha) def confidence_interval(df, state, nulls, intervention_start, window, model, alpha=0.1, jobs=4): return pd.concat([compute_period_ci(df, state, nulls, intervention_start, period, model, alpha) for period in range(intervention_start, intervention_start+window)]) Теперь мы готовы вычислить доверительный интервал для всех периодов после вмешательства. model = SyntheticControl() nulls = np.linspace(-60, 20, 100) ci_df = confidence_interval( data, "california", nulls=nulls, intervention_start=1988, window=2000 - 1988 + 1, model=model ) ci_df
Дополнительное чтение  585 plt.figure(figsize=(10,5)) plt.fill_between(ci_df.index, ci_df["90_ci_lower"], ci_df["90_ci_upper"], alpha=0.2, color="C1") plt.plot(pred_data["california"].index, pred_data["residuals"], label="Калифорния", color="C1") plt.hlines(y=0, xmin=1970, xmax=2000, lw=2, color="Black") plt.vlines(x=1988, ymin=10, ymax=-50, linestyle=":", color="Black", lw=2, label="Положение 99") plt.legend() plt.ylabel("Разрыв в продажах сигарет\n" "на душу населения (в пачках)"); Дополнительное чтение Это приложение опирается на статью Виктора Черножукова (Victor Chernozhukov), Каспара Вютриха (Kaspar Wuethrich) и Инчу Чжу (Yinchu Zhu) «An Exact and Robust Conformal Inference Method for Counterfactual and Synthetic Controls», https://arxiv.org/abs/1712.09089. Я хотел бы выразить особую благодарность Каспару, который прояснил многие вопросы, которые у меня были. Для получения более подробной информации по синтетическим конт­ рольным объектам ознакомьтесь с работой Альберто Абадие (Alberto Abadie) «Using Synthetic Controls: Feasibility, Data Requirements, and Methodological Aspects» (2021), https://www.aeaweb.org/articles?id=10.1257/jel.20191450.
Словарь 1. Always-takers (всегда принимающие) – субъекты, которые всегда принимают воздействие независимо от назначения. 2. Angrist formula (формула Ангриста) – формула «короткая регрессия равна длинной регрессии плюс эффект опущенной переменной, умноженный на регрессию опущенной переменной по включенной переменной». 3. Association (ассоциация) – общее понятие, описывающее связь между переменными, которая может быть как линейной, так и нелинейной. Ассоциация включает в себя корреляцию, но может учитывать и более сложные зависимости. Ассоциация не обязательно должна быть причинно-следственной связью. 4. Average treatment effect или ATE (средний эффект воздействия) – математическое ожидание разницы между потенциальными результатами при воздействии и без воздействия ATE = E[Y1 – Y0]. 5. Average treatment effect on the treated, или ATT (средний эффект воздействия для тех, кто подвергся воздействию) – математическое ожидание разницы между потенциальными результатами при воздействии и без воздействия, при условии что объект подвергся воздействию ATT = E[Y1 – Y0 | T = 1]. 6. Backdoor path (обходной путь) – путь в графе причинно-следственных связей, который связывает переменные, нарушая интерпретацию причинноследственных связей. 7. Bias (смещение) – разница между тестовой и контрольной группами, не связанная с воздействием. 8. Block treatment assignment (блочное назначение воздействия) – процедура назначения воздействия внутри заранее определенных групп. 9. Bootstrap (бутстреп) – метод статистической оценки, использующий случайный отбор с возвращением. 10. Causal effect (причинно-следственный эффект) – это влияние, которое одна переменная (причина) оказывает на другую переменную (следствие) в результате причинно-следственной связи между ними. В отличие от корреляции, которая лишь указывает на связь между переменными, причинно-следственный эффект подразумевает, что изменение значения одной переменной приводит к изменению значения другой. 11. Causal graph models (графовые причинно-следственные модели) – модели, отображающие причинно-следственные связи в виде графов. 12. Causal loss function (причинно-следственная функция потерь) – функция потерь, учитывающая причинно-следственные зависимости.
Дополнительное чтение  587 13. Causal model (причинно-следственная модель) – модель, описывающая причинно-следственные связи между переменными. 14. Causality (причинность) – концепция, описывающая связь, при которой одно событие вызывает другое. 15. Causation (причинно-следственная связь) – связь между двумя переменными, где изменение одной переменной (X) приводит к изменению другой (Y). В отличие от ассоциации, которая отражает только статистическую зависимость, каузация устанавливает направленную связь: X является причиной Y. Количественной мерой причинно-следственной связи является средний эффект воздействия ATE = E[Y1 – Y0], математическое ожидание разницы между результатом при наличии воздействия (т. е. если «причина» происходит) и результатом при отсутствии воздействия. Если корреляция или ассоциация не подразумевает каузацию, то каузация включает в себя ассоциацию. 16. Central limit theorem (центральная предельная теорема) – теорема о том, что распределение выборочных средних при достаточном размере выборок подчиняется нормальному распределению. 17. Chain (цепочка) – последовательность причинно-следственных зависимостей между переменными: A является причиной B, B является причиной C (A → B → C). 18. Collider (коллайдер) – это переменная, на которую влияют две переменные. Стрелки от переменных «сталкиваются» в одной переменной (A → C ← B). Два узла имеют общего потомка, но при этом между ними не су­ ществует прямых отношений. Коллайдер еще называют перевернутой вилкой (inverted fork). 19. Common effect (общий эффект) – общий результат воздействия нескольких факторов. 20. Compliers (послушные исполнители) – субъекты, которые принимают воздействие только при его назначении. 21. Conditional Average Treatment Effect или CATE (условный средний эффект воздействия) – средний эффект воздействия (или вмешательства) для подгруппы индивидов, которые обладают определенными характерис­ тиками или находятся в определенных условиях. CATE рассчитывается как средняя разница между результатами для группы, получившей вмешательство, и результатами для группы, не получившей вмешательства, с учетом определенных характеристик (например, возраста, пола, уровня дохода и других переменных). 22. Confidence interval (доверительный интервал) – диапазон значений, который с заданной доверительной вероятностью накрывает истинное значение параметра. Для понимания доверительной вероятности полезен следующий мысленный эксперимент: представьте, у вас есть генеральная совокупность, вы извлекаете случайным образом выборки и строите доверительные интервалы, каждый раз доверительный интервал либо накрывает истинное значение параметра, либо не накрывает его. Доля всех возможных доверительных интервалов, которые накрывают неизвестный параметр генеральной совокупности, и будет доверительной вероятностью (или уров-
588  Словарь нем доверия). Для 95%-ной доверительной вероятности 95 % доверительных интервалов будут накрывать истинное значение параметра, а 5 % не будут накрывать его. 23. Confounding (спутывающий фактор) – переменная, влияющая одновременно на воздействие и результат. 24. Confounding bias (смещение из-за спутывающих факторов) – смещение, вызванное влиянием спутывающих факторов. 25. Control group (контрольная группа) – группа, не подвергающаяся воздействию в эксперименте. 26. Control variable (контрольная переменная) – переменная, используемая для учета влияния третьих факторов. 27. Conditional-on-Positives effect, COP-effect (эффект обусловленности положительными результатами) – эффект воздействия среди субъектов с положительным исходом. 28. Correlation (корреляция) – мера связи между двумя переменными. Корреляция описывает исключительно линейную зависимость в отличие от ассоциации, описывающей не только линейную, но и нелинейную связь между переменными. 29. Counterfactual result (контрфактический результат) – потенциальный результат, который произошел бы при другом воздействии. 30. Debiased/Orthogonal Machine Learning (несмещенное/ортогональное машинное обучение) – методы обучения, устраняющие смещения в оценках. 31. Defiers (бунтари) – субъекты, которые всегда отвергают воздействие, даже если оно назначено. 32. Difference-in-Difference (разность разностей) – метод оценки эффекта путем сравнения изменений в контрольной и тестовой группах. 33. Doubly Robust Estimation (двойное робастное оценивание) – метод оценки эффекта, устойчивый к ошибкам спецификации модели. 34. F-Learner (F-модель) – это модель, используемая в каузальном анализе для оценки условного среднего эффекта воздействия (CATE). 35. Fixed Effects (фиксированные эффекты) – метод учета постоянных характеристик объектов. 36. Fork (вилка) – одна и та же переменная является причиной двух других переменных в нижней части графа (A ← С → B). Можно еще сказать, что у интересующих нас переменных есть общая причина. 37. Frisch-Waugh-Lovell theorem (теорема Фриша–Во–Ловелла) – тео­ рема, согласно которой МНК-оценка коэффициентов любой части объясняющих переменных в модели линейной регрессии эквивалентна МНК-оценке линейной регрессии только на данную переменную, очищенную от влияния прочих объясняющих переменных. 38. Fuzzy RDD (нечеткий разрывной регрессионный дизайн) – метод, в котором назначение воздействия вероятностно связано с порогом. 39. Heterogeneous Treatment Effects (гетерогенные эффекты воздействия) – различия в эффекте воздействия между субъектами.
Дополнительное чтение  589 40. Ignorability assumption (предположение об отсутствии спутываю­ щих факторов) – предположение, что воздействие независимым образом распределено относительно результата. 41. Instrumental variable (инструментальная переменная) – переменная, влияющая на воздействие, но не напрямую на результат. 42. Inverse Probability of Treatment Weighting – IPTW (взвешивание по обратной вероятности воздействия) – метод коррекции смещений путем взвешивания. 43. Inverted fork (перевернутая вилка) – переменная, на которую влия­ ют две переменные. Стрелки от переменных «сталкиваются» в одной переменной (A → C ← B). Два узла имеют общего потомка, но при этом между ними не существует прямых отношений. 44. Local average treatment effect или LATE – это средний эффект воздействия (T на Y) для «послушных исполнителей», то есть для тех субъектов, которые соблюдают статус получения воздействия (T) в зависимости от значения инструментальной переменной (Z). 45. Matching (сопоставление) – метод, сравнивающий схожие субъекты для оценки эффекта воздействия. 46. Mediator (медиатор) – переменная, через которую воздействие влия­ ет на результат. 47. Never-takers (всегда отказывающиеся) – субъекты, которые никогда не принимают воздействие, независимо от назначения. 48. Non compliance (несоблюдение) – несоответствие между назначением воздействия и его фактическим применением. 49. Non-Parametric Double/Debiased ML – методы машинного обучения для оценки эффектов, не зависящие от параметрических предположений. 50. Nuisance Parameters (мешающие параметры) – параметры, не представляющие интерес, но влияющие на оценку модели. 51. Omitted variable bias (смещение из-за пропущенной переменной) – смещение, вызванное отсутствием значимой переменной в модели. 52. Orthogonalization (ортогонализация) – процесс устранения корреляции между переменными. 53. p-value (p-значение) – вероятность того, что случайная величина, имеющая распределение выбранной статистики при условии верности нулевой гипотезы, примет значение, равное вычисленному значению этой статистики, или более экстремальное. Можно сказать проще: p-значение – это вероятность наблюдать полученное экстремальное значение, когда нулевая гипотеза верна. Если совсем просто, p-значение измеряет, насколько маловероятно, что вы увидите вычисленное значение, когда нулевая гипотеза верна. Обратите внимание на разницу. p-значение – это НЕ P(H0 | данные), а P(данные | H0). p-значение – это не вероятность верности нулевой гипотезы при вычисленном значении, а вероятность наблюдать вычисленное значение при верности нулевой гипотезы. 54. Panel data (панельные данные) – данные, отслеживающие одни и те же объекты во времени.
590  Словарь 55. Parallel trends assumption (предположение о параллельных трендах) – условие для корректности Difference-in-Difference. 56. Placebo Variance Estimation (оценка плацебо-дисперсии) – метод проверки значимости эффекта воздействия. 57. Positivity assumption (предположение о позитивности) – предположение, что вероятность назначения воздействия положительна для всех субъектов. 58. Propensity score (оценка склонности) – вероятность назначения воздействия, учитывая наблюдаемые характеристики. 59. Propensity score matching (сопоставление по склонности) – метод сопоставления субъектов с близкими вероятностями воздействия. 60. R-Learner (R-модель) – метод для оценки причинно-следственных эффектов с использованием резидуализации. 61. Randomized Controlled Trials – RCT (рандомизированные контролируемые испытания) – эксперименты, в которых воздействие случайным образом распределяется между группами. 62. Randomized experiments (рандомизированные эксперименты) – эксперименты с рандомизацией воздействия. 63. Regression Discontinuity Design – метод оценки, использующий пороговое назначение воздействия. 64. S-Learner (S-модель) – это подход для оценки условного среднего эффекта воздействия (CATE), при котором одна модель обучается для прог­ нозирования результатов как с воздействием, так и без него. Этот метод использует одну объединенную модель, чтобы предсказать результат, включив информацию о воздействии в качестве входной переменной. 65. Selection bias (смещение из-за отбора) – смещение, возникающее из-за нерепрезентативного отбора. 66. Sheepskin Effect (эффект овчины) – резкое увеличение выгоды от завершения формального образования. 67. Staggered adoption treatment assignment (пошаговое назначение воздействия) – схема назначения воздействия в разное время. 68. Synthetic Control (синтетический контроль) – метод оценки воздействия путем построения контрольной группы из агрегированных данных. 69. T-Learner (T-модель) – это подход для оценки условного среднего эффекта воздействия (CATE), который использует две отдельные модели для предсказания результатов в разных состояниях: с воздействием (T = 1) и без воздействия (T = 0). 70. Test group (тестовая группа) – группа, получающая воздействие в эксперименте. 71. The McCrary Test (тест Маккрари) – статистический тест, используе­ мый для проверки наличия манипуляций (или дисконтинуальности) в распределении пороговой переменной в контексте регрессионного разрывного дизайна (Regression Discontinuity Design, RDD). Этот тест помогает убедиться, что ключевое предположение RDD выполнено: распределение переменной, определяющей попадание в одну из групп, должно быть гладким.
Дополнительное чтение  591 72. Time fixed parameter (фиксированный временной параметр) – параметр, постоянный для всех объектов в заданное время. 73. Treatment (воздействие) – экспериментальное или наблюдаемое вмешательство. 74. Two-Way Fixed Effect – TWFE – модель, учитывающая фиксированные эффекты по времени и объектам. 75. Unit fixed parameter (фиксированный параметр объекта) – параметр, постоянный для каждого объекта. 76. Variance (дисперсия) – мера разброса значений вокруг среднего. 77. X-Learner (X-модель) – метод для оценки гетерогенных эффектов. 78. z-value (z-значение) – это показатель того, насколько экстремальна наблюдаемая разница, выраженная в стандартных отклонениях. Например, в рамках Z-теста для средних это может быть разница между разностью средних двух генеральных совокупностей (судим о них по выборочным средним) и нулевой гипотезой, согласно которой разность средних двух генеральных совокупностей равна 0: где – сумма стандартных отклонений, μdiff = μ1 – μ2 – разность средних, H0 – нулевая гипотеза. Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Предметный указатель Латиница F-модель (F-Learner), 364 p-значение (p-value), 53 R-модель (R-Learner), 394 S-модель (S-Learner), 368 T-модель (T-Learner), 372 X-модель (X-Learner), 376 z-значение (z-value), 50 Кириллица Ассоциация (Association), 15 Блочное назначение воздействия (Block treatment assignment), 466 Бунтари (Defiers), 149 Бутстреп (Bootstrap), 195, 198, 207, 209, 211, 281 Взвешивание по обратной вероятности воздействия – IPTW (Inverse Probability of Treatment Weighting), 189 Вилка (Fork), 61 Воздействие, лечение (Treatment), 17 Всегда отказывающиеся (Never-takers), 149 Всегда принимающие (Always-takers), 149 Инструментальная переменная (Instrumental variable), 132 Коллайдер (Collider), 62 Контрольная группа (Control group), 25 Контрольная переменная (Control variable), 106 Контрфактический результат (Counterfactual result), 18 Корреляция (Correlation), 15 Локальный средний эффект воздействия – LATE (Local Average Treatment Effect), 155 Медиатор (Mediator), 72 Мешающие параметры (Nuisance Parameters), 385 Модель с временными и индивидуальными фиксированными эффектами – TWFE (Two Way Fixed Effect), 432 Гетерогенные эффекты воздействия (Heterogeneous Treatment Effects), 312 Графовые причинно-следственные модели, графовые каузальные модели, графы причинно-следственных связей (Causal graph models), 58 Непараметрическое двойное/несмещенное машинное обучение (Non Parametric Double/Debiased ML), 394 Несмещенное/ортогональное машинное обучение (Debiased/Orthogonal Machine Learning), 383 Несоблюдение требований (Non compliance), 152 Нечеткий разрывной регрессионный дизайн (Fuzzy RDD), 264 Двойное робастное оценивание (Doubly Robust Estimation), 206 Дисперсия (Variance), 40 Доверительный интервал (Confidence interval), 43 Панельные данные (Panel data), 224 Обходной путь (Backdoor path), 61 Ортогонализация (Orthogonalization), 388, 502 Оценка склонности (Propensity score), 186
Предметный указатель  593 Перевернутая вилка (Inverted fork), 62 Плацебо-дисперсия (Placebo Variance), 496 Послушные исполнители (Compliers), 149 Потенциальный результат (potential outcome), 18 Пошаговое назначение воздействия (Staggered adoption treatment assignment), 467 Предположение об игнорировании/ взаимозаменяемости/отсутствии спутывающих факторов (Ignorability/ exchangeability/unconfoundedness assumption), 227 Предположение о позитивности причинно-следственного вывода (Positivity assumption), 190 Причинно-следственная связь, каузация (Causation), 15 Причинно-следственная функция потерь (Causal loss function), 395 Причинно-следственный эффект (Causal effect), 25 Разность разностей (Difference-in-Difference или diff-in-diff), 215 Разрывной регрессионный дизайн (Regression Discontinuity Design), 263 Рандомизированные контролируемые испытания, или РКИ (Randomised Controlled Trials – RCT), 34 Рандомизированные эксперименты (Randomised experiments), 34 Синтетический контроль (Synthetic Control), 243 Смещение из-за опущенной переменной (Omitted variable bias), 87 Смещение из-за отбора (Selection bias), 70 Смещение из-за спутывающих факторов (Confounding bias), 66 Смещение (Bias), 21 Сопоставление по оценке склонности (Propensity score matching), 200 Сопоставление (Matching), 169 Спутывающий фактор, конфаундер (Confounding), 66 Средний эффект воздействия для тех, кто подвергся воздействию – ATT (Average treatment effect on the treated), 19 Средний эффект воздействия – ATE (Average treatment effect), 19 Теорема Фриша–Во–Ловелла (Frisch-Waugh-Lovell theorem), 386 Тест Маккрари (The McCrary Test), 278 Тестовая группа (Test group), 25 Условие о параллельности трендов (Parallel trends assumption), 221 Условный средний эффект воздействия – CATE (Conditional Average Treatment Effect), 315 Фиксированные эффекты (Fixed Effects), 230 Фиксированный параметр времени, временной фиксированный параметр (Time fixed parameter), 437 Фиксированный параметр объекта, индивидуальный фиксированный параметр (Unit fixed parameter), 437 Формула, или мантра, Ангриста (Angrist formula), 86 Центральная предельная теорема (Central limit theorem), 43 Цепочка (Chain), 59 Четкий разрывной регрессионный дизайн (Sharp RDD), 264 Эффект обусловленности положительными результатами – COP-effect (Conditional-on-Positives effect), 124 Эффект овчины (Sheepskin Effect), 275 Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk
Книги | Books | Архив (https://t.me/BIG_Disk) @BIG_Disk Матеус Факур, Артем Груздев Причинно-следственный анализ для смелых и честных Главный редактор Зам. главного редактора Мовчан Д. А. Яценков В. С. Перевод Корректор Верстка Дизайн обложки Груздев А. В. Синяева Г. И. Чаннова А. А. Мовчан А. Г. editor@dmkpress.com Гарнитура PT Serif. Печать цифровая. Усл. печ. л. 48,26. Тираж 200 экз. Веб-сайт издательства: www.dmkpress.com