1 Эксперименты
1.1 Короткое напоминание теории
1.1.1 Потенциальные исходы
Обозначения:
\(X_i\) – независимые переменные (covariates)
\(T_i\) – бинарная переменная воздействия (treatment variable): \[\begin{equation*} T_i = \begin{cases} 1, &\text{воздействие на объект i оказано}\\ 0, &\text{воздействие на объект i не оказано} \end{cases} \end{equation*}\]
\(Y_{i1}\), \(Y_{i0}\) – потенциальные исходы (potential outcomes)
Наблюдаемые исходы \(Y_i\) отличаются от потенциальных исходов. Потенциальные исходы являются гипотетическими случайными величинами, когда наблюдаемые исходы являются фактическими случайными величинами. Наблюдаемые исходы являются функцией от потенциальных исходов:
\(Y_i = T_i \cdot Y_{i1} + (1-T_i) \cdot Y_{i0}\)
Действительно при \(T=1\) мы получим \(Y_i = Y_{i1}\), а при \(T=0\) получим \(Y_i = Y_{i0}\).
Тогда наш эффект воздействия для конкретного наблюдения равен разнице между двумя состояниями мира для этого наблюдения (потенциальными исходами):
\(\tau_i = Y_{i1} - Y_{i0}\)
Фундаментальная проблема причинного вывода (Fundamental problem of causal inference):
Чтобы оценить эффект воздействия для конкретного индивида, мы должны знать потенциальные исходы сразу для двух его состояний мира, но реально мы наблюдаем только одно из них – либо \(Y_{i1}\), если индивид подвергся воздействию, либо \(Y_{i0}\), если он ему не подвергался. Оценка индивидуального эффекта требует доступа к данным, которых у нас физически не может быть.
1.1.2 Средние эффекты
Если с распределением индивидуального эффекта воздействия (treatment effect) работать не получается, будем довольствоваться средними величинами. Например, попробуем рассчитать средний эффект воздействия (average treatment effect):
\(ATE = \mathbb{E}[\tau_i] = \mathbb{E}[Y_{i1} - Y_{i0}] = \mathbb{E}[Y_{i1}] - \mathbb{E}[Y_{i0}] = \frac{1}{N_1}\sum \limits_{i=1}^{N_1}Y_{i1} - \frac{1}{N_0}\sum \limits_{i=1}^{N_0}Y_{i0}\)
Следующий шаг, который мы предпримем, попробуем рассчиать величину эффекта только для тритмент группы – средний эффект воздействия на задействованных (average treatment effect for the treatment group):
\(ATT = \mathbb{E}[\tau_i|T_i=1] = \mathbb{E}[Y_{i1} - Y_{i0}|T_i=1] = \mathbb{E}[Y_{i1}|T_i=1] - \mathbb{E}[Y_{i0}|T_i=1]\)
А теперь то же самое, но для контрольной группы – средний эффект воздействия на незадействованных (average treatment on the non-treated):
\(ATnT = \mathbb{E}[\tau_i|T_i=0] = \mathbb{E}[Y_{i1} - Y_{i0}|T_i=0] = \mathbb{E}[Y_{i1}|T_i=0] - \mathbb{E}[Y_{i0}|T_i=0]\)
Обратите внимание, как и в случае с определением эффекта воздействия на индивидуальном уровне, различные модификации средних эффектов воздействия снова требуют от нас знания обоих потенциальных исходов для каждого наблюдения. Таким образом, и средние, и индивидуальный эффект воздействия нельзя напрямую рассчиать, но мы будем пробовать их оценить.
Самая простая идея для оценки ATE, которая всем придет в голову, взять простую разницу в средних: \(\mathbb{E}[Y_1|T=1] - \mathbb{E}[Y_0|T=0]\).
Но тут всё не так просто, после небольших преобразований, с которыми можно ознакомиться в части 4.1.3 учебника мы получим следующее:
\(\mathbb{E}[Y_1|T=1] - \mathbb{E}[Y_0|T=0] = \underbrace{\mathbb{E}[Y_1] - \mathbb{E}[Y_0]}_{\text{ATE}} + \underbrace{\mathbb{E}[Y_0|T=1] - \mathbb{E}[Y_0|T=0]}_{\text{Selection Bias}} + \underbrace{(1-\pi)(ATT - ATnT)}_{\text{Heterogeneous treatment effect bias}}\)
- ATE – интересующий нас эффект
- Selection Bias – смещение, возникающее из-за того, что контрольная группа и группа воздействия различались, даже если бы на них не было оказано воздействие, то есть имеет место некоторый дисбаланс
- Heterogeneous treatment effect bias – различие в интенсивности эффекта для тритмент и контрольной группы, взвешенное на долю выборки \((1-\pi)\), которая попала в контрольную группу
Далее мы рассмотрим предпосылки, которые позволяют нивелировать влияние этих двух смещений и получить оценку ATE.
1.1.3 Предпосылки
- Экзогенность воздействия (Independence assumption)
Экзогенность воздействия (Independence assumption) означает, что распределение объекта в тритмент или контрольную группы осуществляется случайно и независимо от его изначальных характеристик. Данная предпосылка обычно обозначается следующим образом \((Y_1, Y_0, X)_i \perp T_i\)
Технически для нас это значит следующее:
- \(\mathbb{E}[Y_0|T=1] - \mathbb{E}[Y_0|T=0] = 0 \Rightarrow \text{Selection Bias}=0\)
- \(\mathbb{E}[Y_1|T=1] - \mathbb{E}[Y_1|T=0] = 0\)
- \((1-\pi)(ATT - ATnT) = (1-\pi)\left[(\mathbb{E}[Y_1|T=1]-\mathbb{E}[Y_0|T=1])-(\mathbb{E}[Y_1|T=0]-\mathbb{E}[Y_0|T=0])\right] = 0 \Rightarrow \text{Heterogeneous treatment effect bias}=0\)
То есть хорошая рандомизация, а следовательно, и выполнение предпосылок, позволяет нам очистить эффект воздействия от двух типов смещения, в этом случае:
\(ATE = \mathbb{E}[Y_1] - \mathbb{E}[Y_0] = \mathbb{E}[Y_1|T=1] - \mathbb{E}[Y_0|T=0] \xrightarrow{p} \frac{1}{N_1}\sum \limits_{i=1}^{N_1}Y_{i1} - \frac{1}{N_0}\sum \limits_{i=1}^{N_0}Y_{i0}\)
- Отсутствие “внешних эффектов” воздействия (SUTVA – Stable unit treatment value assumption)
Эта предпосылка подразумевает выполнение двух вещей. Во-первых, воздействие оказывается только на один объект и внешние эффекты у него отсутствуют. Во-вторых, воздействие гомогенно – существует только один тип тритмента.
1.2 Работа со случайными числами в R
В рамках курса нам неоднократно придется прибегнуть к генерации случайных чисел из какого-нибудь закона распределения, как правило, из нормального. Для работы со случайными числами из нормального распределения в R используется несколько функций: dnorm()
для плотности вероятности, pnorm()
для функции распределения, qnorm()
для квантилей распределения и rnorm()
для генерации случайных чисел. Почитать подробнее про них и посмотреть примеры можно тут или тут.
Если вы не уверены в синтаксисе какой-то функции, вы можете воспользоваться встроенной справкой в RStudio (справочная информация появится во вкладке Help
):
?rnorm
Если вам когда-нибудь понадобятся функции для других распределений, вы можете, конечно, их просто загуглить или ввести в окне Help
запрос “Distributions”:
?Distributions
Теперь, когда мы точно знаем, как устроены аргументы у функции rnorm(n;mean;sd)
, давайте попробуем достать 5 чисел из стандартного нормального распределения \(\mathcal{N} \sim \left(0;1\right)\):
rnorm(5,0,1)
[1] 0.8028321 0.1841247 -0.1678564 0.6213981 -1.4385412
Если вы повторяли за мной, то у вас, вероятно, получились другие значения. А теперь я тоже попробую ещё раз:
rnorm(5,0,1)
[1] -1.91234816 -0.05973835 -1.86892906 -0.40398381 1.19708204
Как и ожидалось, у меня тоже не совпало с предыдущим результатом. Существует по меньшей мере две причины, почему нам хотелось бы получать одинаковые результаты:
- Работа на семинарах, когда нам с вами было бы удобно сверяться
- Воспроизводимые научные исследования, когда любой читатель может реплицировать и проверить на корректность ваш результат
В R существует несколько алгоритмов, позволящих генерировать случайные числа (Random Number Generator (RNG), подробнее – ?RNGkind
). По умолчанию используется Mersenne twister (Вихрь Мерсенна), поскольку он работает наиболее быстро. Однако все эти алгоритмы на самом деле детерминированы, поэтому генерируемые ими числа корректнее называть “псевдослучайными”.
Генератор псевдослучайных чисел начинает свою работу с определенной точки в пространстве возможных чисел. Эту точку приянто называть начальное число или seed. Начальное число – это число (или вектор), используемое для инициализации генератора псевдослучайных чисел. Если вы хотите получить одинаковые результаты с помощью генератора случайных чисел, важно установить начальное число. Для этого в R используется функция set.seed()
. По умолчанию начальное число не установлено, если ничего не указать, то создается новое из текущего времени и идентификатора процесса на вашем устройстве. Следовательно, по умолчанию разные сеансы дают разные результаты моделирования.
Давайте снова попробуем сгенерировать 5 чисел из случайного нормального распределения, но в этот раз зафиксируем seed:
set.seed(123)
rnorm(5,0,1)
[1] -0.56047565 -0.23017749 1.55870831 0.07050839 0.12928774
И еще разок:
set.seed(123)
rnorm(5,0,1)
[1] -0.56047565 -0.23017749 1.55870831 0.07050839 0.12928774
Получилось! В качестве seed можно использовать любое число, главное, чтобы при репликации оно каждый раз было одинаковым.
Почитать про set.seed можно тут на русском и тут на английском.
1.3 Мини-симуляция
Симуляции – это “игрушечные” примеры, которые позволят нам на протяжении курса разбираться с разными методами. Игрушечные они потому, что за ними не стоят реальные данные. Данные для симуляций мы будем специальным образом заранее моделировать. Это бывает удобно, когда идеально подходящих данных нет, или они не лежат в открытом доступе. К тому же, живые данные часто могут быть зашумлены из-за других факторов, на которые нам не всегда будет удобно отвлекаться на занятиях. Но с живыми данными мы тоже обязательно будем работать! В том числе, они будут доступны вам в домашних заданиях :)
Теперь смоделируем небольшую симуляцию по мотивам изученного материала.
1.3.1 Подготовка данных
Давайте смоделируем гипотетическую ситуацию. Мы хотим оценить величину эффекта от использование сайта с расписанием cacs.ws на свободное время студента.
Предположим, что наша экспериментальная выборка состоит из 1000 человек:
<- 1000 N
Для простоты у них будет всего две характеристики (\(X\) – возраст и \(Z\) – время в пути от дома до ЭФ), имеющих влияние на потенциальный исход (\(Y_0\) и \(Y_1\) – свободное время). При этом предположим, что реальный эффект воздействия равен \(\tau=15\) минутам. Будем считать, что реальная зависимость потенциального исхода от ковариатов и тритмента выглядит следующим образом:
- \(Y_0 = 240 - 3 \cdot X - Z + 15 \cdot \underbrace{T}_{= 0} + \varepsilon = 120 - 3 \cdot X - Z + \varepsilon\) – потенциальный исход, если не было оказано воздействие
- \(Y_1 = 240 - 3 \cdot X - Z + 15 \cdot \underbrace{T}_{= 1} + \varepsilon = 120 - 3 \cdot X - Z + 15 + \varepsilon\) – потенциальный исход, если было оказано воздействие
Сгенерируем \(X\) и \(Z\) случайным образом:
set.seed(123)
<- runif(N, 18, 25)
X <- rnorm(N, 60, 20)
Z # Посмотрим на наши данные, нарисуем сразу 2 графика -- диаграмму разброса и гистограмму
par(mfrow=c(2,2)) # читаем буквально как матрицы -- 2 строки и 2 столбца
plot(X)
hist(X)
plot(Z)
hist(Z)
Также создадим случайные ошибки для каждого из типов индивидов:
set.seed(123)
<- rnorm(N, 0, 1)
e plot(e, col="red")
Вспомогательно пронумеруем наши наблюдения:
<- 1:N number
И соберем всё в общий датасет:
<- data.frame(number = number, X=X, Z=Z) # сшиваем столбики в data frame
data head(data, 10) # смотрим только первые несколько строк датасета
number X Z
1 1 20.01304 47.96214
2 2 23.51814 40.12603
3 3 20.86284 80.53570
4 4 24.18112 75.02123
5 5 24.58327 29.81667
6 6 18.31890 58.09705
7 7 21.69674 42.08104
8 8 24.24693 18.58498
9 9 21.86005 63.00240
10 10 21.19630 58.41577
1.3.2 Рандомизация
Что касается рандомизации, то наша глобальная задача, используя разные функции придумать способ, как случайным образом разбить выборку на 2 группы. Существует огромное множество вариантов, как это можно сделать. Мы обсудим лишь несколько из них. На практике вы можете пользоваться как этими вариантами, так и придумать что-то своё.
1.3.2.1 Использование порядкового номера
Первый вариант – просто взять половину наблюдений по их порядку расположения в датасете. Пусть воздействие будет оказано только на первую половину нашей выборки.
<- c(rep(1,N/2), rep(0,N/2))
T1 mean(T1) # проверяем долю тритмента, должна получиться ровно половина
[1] 0.5
$T1 <- T1 # добавляем переменную в датасет data
Тут мы воспользовались функцией c(...)
, которая позволяет объединить величины внутри неё в вектор, и функцией rep(x;times)
которая повторяет величину \(x\) столько раз, сколько указано в величине \(times\).
Второй вариант, который практически дублирует первый, но немного отличается – мы можем взять каждое второе наблюдение:
<- rep(0:1, N/2)
T2 mean(T2) # проверяем долю тритмента, должна получиться ровно половина
[1] 0.5
$T2 <- T2 # добавляем переменную в датасет data
1.3.2.2 Использование свойств распределений
Логично, что если мы хотим случайным образом присвоить наблюдениям разные группы, то мы можем начать с того, что присвоим им случайные числа, которые мы потом разными способами переведем в бинарный формат, который уже будет соотвествовать конкретной группе. Протестируем этот способ на примере трех распределений – нормального, равномерного и биномиального.
1.3.2.2.1 Равномерное распределение
set.seed(123)
<- runif(N) # генерим вспомогательную переменную из равномерного распределения
V1 <- as.numeric(V1 < median(V1)) #генерим тритмент, отсекая половину выборки по медиане; as.numeric используется, чтобы перейти от логического типа (true, false) к численному (1 и 0)
T3 mean(T3) # проверяем долю тритмента, должна получиться ровно половина
[1] 0.5
$T3 <- T3 # добавляем переменную в датасет data
1.3.2.2.2 Нормальное распределение
set.seed(123)
<- rnorm(N) # генерим вспомогательную переменную из нормального распределения
V2 <- as.numeric(V2>0) #генерим тритмент, отсекая половину выборки по знаку вспомогательной переменной; as.numeric используется, чтобы перейти от логического типа (true, false) к численному (1 и 0)
T4 summary(T4) # проверяем долю тритмента, должна получиться примерно половина
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.000 0.000 1.000 0.505 1.000 1.000
$T4 <- T4 # добавляем переменную в датасет data
1.3.2.2.3 Биномиальное распределение
set.seed(123)
<- rbinom(N, 1, 0.5) #генерим тритмент, отсекая половину выборки по знаку вспомогательной переменной; as.numeric используется, чтобы перейти от логического типа (true, false) к численному (1 и 0)
T5 summary(T5) # проверяем долю тритмента, должна получиться примерно половина
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.000 0.000 0.000 0.493 1.000 1.000
$T5 <- T5 # добавляем переменную в датасет data
1.3.2.3 Использование готовых пакетов
1.3.2.3.1 Пакет experiment
library('experiment')
set.seed(123)
<- randomize(data, group= c("Treat", "Control"))
rand <- as.numeric(rand$treatment == "Treat") # преобразовываем переменную в численный формат
T6 $T6 <- T6 # добавляем переменную в датасет
datahead(data, 10)
number X Z T1 T2 T3 T4 T5 T6
1 1 20.01304 47.96214 1 0 1 0 0 1
2 2 23.51814 40.12603 1 1 0 0 1 1
3 3 20.86284 80.53570 1 0 1 1 0 1
4 4 24.18112 75.02123 1 1 0 1 1 0
5 5 24.58327 29.81667 1 0 0 1 1 1
6 6 18.31890 58.09705 1 1 1 1 0 0
7 7 21.69674 42.08104 1 0 0 1 1 0
8 8 24.24693 18.58498 1 1 0 0 1 1
9 9 21.86005 63.00240 1 0 0 0 1 1
10 10 21.19630 58.41577 1 1 1 0 0 1
1.3.2.3.2 Пакет radomizr
library('randomizr')
set.seed(123)
<- complete_ra(N=N, m=N/2)
T7 $T7 <- T7 # добавляем переменную в датасет
datahead(data, 10)
number X Z T1 T2 T3 T4 T5 T6 T7
1 1 20.01304 47.96214 1 0 1 0 0 1 0
2 2 23.51814 40.12603 1 1 0 0 1 1 0
3 3 20.86284 80.53570 1 0 1 1 0 1 0
4 4 24.18112 75.02123 1 1 0 1 1 0 1
5 5 24.58327 29.81667 1 0 0 1 1 1 0
6 6 18.31890 58.09705 1 1 1 1 0 0 1
7 7 21.69674 42.08104 1 0 0 1 1 0 1
8 8 24.24693 18.58498 1 1 0 0 1 1 0
9 9 21.86005 63.00240 1 0 0 0 1 1 0
10 10 21.19630 58.41577 1 1 1 0 0 1 0
1.3.2.4 Хэш-функции
Существует ряд ситуаций, когда подходы к рандомизации, которые мы разобрали выше, работают не очень хорошо. Например, в вашу выборку добавилось несколько наблюдений, которые по какой-то причине добавились не в конец вашего датасета, а может у вас есть какая-то хитрая сортировка датасета. В этом случае рандомизация не будет воспроизводимой, а тритмент и контрольная группы будут различаться. Однако есть способ решить эту сложность.
Хэш-функция (hash function) – функция, осуществляющая преобразование массива входных данных произвольной длины в выходную битовую строку установленной длины, выполняемое определённым алгоритмом. Преобразование, производимое хэш-функцией, называется хэшированием.
Хэш-функции применяются в следующих случаях:
- при построении ассоциативных массивов;
- при поиске дубликатов в последовательностях наборов данных;
- при построении уникальных идентификаторов для наборов данных;
- при вычислении контрольных сумм от данных (сигнала) для последующего обнаружения в них ошибок (возникших случайно или внесённых намеренно), возникающих при хранении и/или передаче данных;
- при сохранении паролей в системах защиты в виде хэш-кода (для восстановления пароля - по хэш-коду требуется функция, являющаяся обратной по отношению к использованной хэш-функции);
- при выработке электронной подписи (на практике часто подписывается не само сообщение, а его “хэш-образ”);
- и др.
Для нас важно то, что с помощью хэш-функции мы сможем взаимно однозначно переводить данные формата строки в число.
Пример хэширования с помощью пакета digest
:
library("digest")
<- digest('econometrics', algo="murmur32")
hash hash
[1] "24b6de5a"
Пакет digest
применяет хеш-функцию к произвольным объектам R. В пакете реализовано много разных алгоритмов преобразований, однако мы будем использовать алгоритм “murmur32” (подробнее почитать можно тут). Этот алгоритм совершенно не подходит для криптографических целей, но зато идеально подходит нам, поскольку он 32-битный, что позволяет нам перевести полученный хэш-код в целое число.
Прежде чем начать разбирать пример, немного отвлечемся на техническую полезную вещь – функцию sapply()
.
Про семейство apply
функций рекомендую почитать подробнее на русском тут или тут, а на английском тут.
Прелесть функции sapply()
состоит в том, что она позволяет нам избежать громоздких циклов при написании кода и ускорить вычисления благодаря “векторной ориентированности” языка R. Например, функция позволяет нам найти минимальное и максимальное значение для каждой из ковариат:
sapply(list(X,Z), min)
[1] 18.003257 3.804506
sapply(list(X,Z), max)
[1] 24.99583 127.80742
То есть функция проводит однотипную операцию (по сути вложенную функцию) над каждым элементом списка. Если кто-то хорошо владеет python, то аналогичной функцией там является map()
.
Вернемся к нашей симуляции. Изначально мы это все затеяли, чтобы сделать хорошую рандомизацию. Сначала хэшируем номера наших наблюдений. Так делать не очень хорошо, обычно, для хэширования данных используют ФИО и/или СНИЛС, но мы не будем ради этого громоздить генерацию еще одной переменной. Для иллюстрации просто воспользуемся номерами, это тоже сработает.
<- sapply(data$number, function(x) {digest(x, algo="murmur32")}) # хэшируем строчки
hashes 1:20] hashes[
[1] "5e6216f3" "8b8b3789" "ad1bf356" "b2df92b2" "1d1b8e3a" "98d09cab"
[7] "2e366c5a" "4d110361" "91d6bfe0" "f568e543" "08bd74cd" "e979af4b"
[13] "a82e5827" "485cbd08" "bc4e5bd8" "95f68430" "7180d744" "da414596"
[19] "70aa2885" "afd0ffd2"
Далее нужно перевести получившиеся строчки в цифровой числовой. Функция strtoi()
конвертирует строковое представление числа, которое хранится в строке, в длинное целое.
<- strtoi(substring(hashes, 2), base=16)
result 1:20] result[
[1] 241309427 193673097 219935574 48206514 219909690 147889323 238447706
[8] 219218785 30851040 90760515 146633933 158969675 137254951 140295432
[15] 206461912 100041776 25220932 172049814 11151493 265355218
Далее, используя эти числа, нужно как-то разбить выборку на тритмент и контроль. Будем смотреть на последнюю цифру, если она меньше 5, то наблюдение попадет в тритмент, если больше – в контроль.
<- result %% 10 < 5 # берем остаток от деления на 10 (получится последняя цифра) и сравниваем его с 5
T8 $T8 <- as.numeric(T8) # переводим из логического типа в числовой и добавляем в датасет
data$T8[1:20] data
[1] 0 0 1 1 1 1 0 0 1 0 1 0 1 1 1 0 1 1 1 0
Проверим нашу рандомизацию на сбалансированность:
summary(data)
number X Z T1 T2
Min. : 1.0 Min. :18.00 Min. : 3.805 Min. :0.0 Min. :0.0
1st Qu.: 250.8 1st Qu.:19.78 1st Qu.: 46.232 1st Qu.:0.0 1st Qu.:0.0
Median : 500.5 Median :21.43 Median : 60.576 Median :0.5 Median :0.5
Mean : 500.5 Mean :21.48 Mean : 60.239 Mean :0.5 Mean :0.5
3rd Qu.: 750.2 3rd Qu.:23.23 3rd Qu.: 73.101 3rd Qu.:1.0 3rd Qu.:1.0
Max. :1000.0 Max. :25.00 Max. :127.807 Max. :1.0 Max. :1.0
T3 T4 T5 T6 T7
Min. :0.0 Min. :0.000 Min. :0.000 Min. :0.0 Min. :0.0
1st Qu.:0.0 1st Qu.:0.000 1st Qu.:0.000 1st Qu.:0.0 1st Qu.:0.0
Median :0.5 Median :1.000 Median :0.000 Median :0.5 Median :0.5
Mean :0.5 Mean :0.505 Mean :0.493 Mean :0.5 Mean :0.5
3rd Qu.:1.0 3rd Qu.:1.000 3rd Qu.:1.000 3rd Qu.:1.0 3rd Qu.:1.0
Max. :1.0 Max. :1.000 Max. :1.000 Max. :1.0 Max. :1.0
T8
Min. :0.000
1st Qu.:0.000
Median :1.000
Mean :0.508
3rd Qu.:1.000
Max. :1.000
1.3.3 Реализация исходов
Посчитаем потенциальные исходы:
<- 240 - 3*X - Z + 15*0 + e # T=0
Y0 <- 240 - 3*X - Z + 15*1 + e # T=1
Y1 $Y0 <- Y0 # добавляем переменные в датасет
data$Y1 <- Y1 data
А также наблюдаемые исходы:
$Y <- T1*Y1 + (1-T1)*Y0 data
Можно ли было бы сделать все то же самое за меньшее число строчек кода и время? Конечно, можно. Более того, если бы похожую процедуру вам пришлось бы по каким-то причинам проводить несколько раз, например, когда у вас несколько экспериментов, функция для вас просто была бы незаменимым помощником. Еще раз генерим данные о нашем умозрительном эксперименте, но на этот раз для общего развития попробуем переписать все то же самое с помощью функций (не пугайтесь, на семинарах мы отдельно еще раз обсудим техническую сторону вопроса про функции):
- Функция для генерации данных
<- function(N){
generate_data =N
N<- runif(N, 18, 25)
X <- rnorm(N, 60, 20)
Z <- rnorm(N/2, 0, 1)
e0 <- rnorm(N/2, 0, 1)
e1 <- 1:N
number <- 240 - 3*X - Z + 15*0 + e0
Y0 <- 240 - 3*X - Z + 15*1 + e1
Y1 <- data.frame(number = number, X=X, Z=Z, Y0=Y0, Y1=Y1)
data }
- Функция для рандомизации
<- function(data, type){
randomization <- nrow(data)
N if (type=='in order'){
<- c(rep(1,N/2), rep(0,N/2))
T $T <- T
dataelse if (type=='by turns'){
} <- rep(0:1, N/2)
T $T <- T
dataelse if (type=='unif'){
} <- runif(N)
V <- as.numeric(V1 < median(V1))
T $T <- T
dataelse if (type=='norm'){
} <- rnorm(N)
V <- as.numeric(V2>0)
T $T <- T
dataelse if (type=='binom'){
} <- rbinom(N, 1, 0.5)
T
}return(data)
}
- Функция для расчета наблюдаемого исхода
<- function(data){
outcome <- data
observed_data $Y <- data$T*observed_data$Y1 + (1 - data$T)*observed_data$Y0
observed_datareturn(observed_data)
}
А теперь посмотрим, сколько строчек займет наша симуляция с использованием функций:
set.seed(123)
<- generate_data(1000)
data set.seed(123)
<- randomization(data=data, type='in order')
data <- outcome(data=data)
data tail(data,10)
number X Z Y0 Y1 T Y
991 991 18.61054 75.36015 107.71110 125.93429 0 107.71110
992 992 19.56646 37.83344 144.39269 158.58181 0 144.39269
993 993 22.00504 44.27528 129.95640 144.11565 0 129.95640
994 994 20.80118 105.68233 71.17735 87.99218 0 71.17735
995 995 21.95826 38.13398 134.71123 149.89166 0 134.71123
996 996 23.80737 64.28959 104.36495 120.01488 0 104.36495
997 997 22.49480 77.85142 94.91935 111.10506 0 94.91935
998 998 20.74049 80.37516 97.68081 112.19320 0 97.68081
999 999 22.96706 81.78224 89.85344 105.76786 0 89.85344
1000 1000 18.76177 56.73742 126.51679 142.61883 0 126.51679
1.3.4 Считаем эффект
Когда данные подготовлены, мы забываем, что что-то о них знали :)
С данного момента мы действуем согласно предпосылке, что мы знаем только \(Y\), \(X\) и \(T\).
Поскольку мы знаем, что тритмент распределялся среди наблюдений случайно, мы можем сделать вывод, что предпосылка о независимости воздействия выполнена. Также мы знаем, что данные устроены так, что SUTVA тоже выполнена. Следовательно, можем использовать обычную разницу в средних, чтобы оценить эффнект воздействия:
\(\widehat{ATE} = \overline{Y_1} - \overline{Y_0}\)
<- mean(data[which(data$T == 1),]$Y) - mean(data[which(data$T == 0),]$Y)
ATE_hat ATE_hat
[1] 15.53169
Из курса ЭКМ-2 помним, что то же самое можно было бы получить, с помощью обычного парного МНК:
<- lm(Y ~ T, data=data)
model1 summary(model1)
Call:
lm(formula = Y ~ T, data = data)
Residuals:
Min 1Q Median 3Q Max
-78.525 -14.782 -0.268 14.040 64.919
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 115.0500 0.9369 122.80 <2e-16 ***
T 15.5317 1.3249 11.72 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 20.95 on 998 degrees of freedom
Multiple R-squared: 0.121, Adjusted R-squared: 0.1202
F-statistic: 137.4 on 1 and 998 DF, p-value: < 2.2e-16
Если обе предпосылки выполнены, то оценка должна быть несмещенной даже без контрольных переменных (ковариат). Попробуем их добавить и сравним оценки эффекта:
<- lm(Y ~ T + X + Z, data=data)
model2 summary(model2)
Call:
lm(formula = Y ~ T + X + Z, data = data)
Residuals:
Min 1Q Median 3Q Max
-2.97985 -0.63057 -0.01498 0.69005 3.04683
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 240.123033 0.347927 690.2 <2e-16 ***
T 14.874547 0.061883 240.4 <2e-16 ***
X -2.988124 0.015384 -194.2 <2e-16 ***
Z -1.005280 0.001546 -650.4 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.9783 on 996 degrees of freedom
Multiple R-squared: 0.9981, Adjusted R-squared: 0.9981
F-statistic: 1.732e+05 on 3 and 996 DF, p-value: < 2.2e-16
Видим, что результаты устойчивы, величина эффекта практически не изменилась, но существенно уменьшилась его стандартная ошибка. Вывод: контрольные переменные хорошо включать для снижения дисперсии оценок.
Далее в качестве упраженения посчитаем средние эффекты на разных группах:
<- mean(data[which(data$T == 1),]$Y1) - mean(data[which(data$T == 1),]$Y0)
ATT ATT
[1] 14.87723
<- mean(data[which(data$T == 0),]$Y1) - mean(data[which(data$T == 0),]$Y0)
ATnT ATnT
[1] 14.87723
Получили, то что и должны были получить \(ATT = ATnT\). Если мы вернемся в раздел, где мы подготовливали данные, то увидим, что в двух потенциальных исходах мы “зашифровали” одинаковый эффект:
- \(Y_0 = 120 - 3 \cdot X - Z + 15 \cdot \underbrace{T}_{= 0} + \varepsilon = 120 - 3 \cdot X - Z + \varepsilon\)
- \(Y_1 = 120 - 3 \cdot X - Z + 15 \cdot \underbrace{T}_{= 1} + \varepsilon = 120 - 3 \cdot X - Z + 15 + \varepsilon\)
Содержательно это иллюстрирует то, что эффект воздействия гомогенен, то есть \(\text{Heterogeneous treatment effect bias} = (1-\pi)(ATT - ATnT) = 0\).
1.3.5 Баланс ковариатов
Теперь проверим насколько наша рандомизация оказалась хорошей, то есть проверим контрольную и тритмент группу на “похожесть”. Сравним средние значения ковариат в двух группах.
1.3.5.1 Сравнение средних
mean(data[which(data$T == 1),]$X) - mean(data[which(data$T == 0),]$X)
[1] -0.02791759
mean(data[which(data$T == 1),]$Z) - mean(data[which(data$T == 0),]$Z)
[1] -0.5707093
Это не очень информативный и субъективный способ, но если мы вспомним, что порядок значений ковариат измерялся десятками, то можно сделать вывод, что группы в среднем похожи.
1.3.5.2 Тест Стьюдента
А теперь проведем тест на разницу в средних с помощью формального t-теста Стьюдента:
- \(H_0:\) среднее значение параметра в группах одинаковое, группы не различаются
- \(H_1:\) среднее значение параметра в группах разное, группы различаются
t.test(data[which(data$T == 1),]$X, data[which(data$T == 0),]$X)
Welch Two Sample t-test
data: data[which(data$T == 1), ]$X and data[which(data$T == 0), ]$X
t = -0.21924, df = 997.51, p-value = 0.8265
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-0.2777933 0.2219581
sample estimates:
mean of x mean of y
21.46699 21.49490
t.test(data[which(data$T == 1),]$Z, data[which(data$T == 0),]$Z)
Welch Two Sample t-test
data: data[which(data$T == 1), ]$Z and data[which(data$T == 0), ]$Z
t = -0.45032, df = 997.68, p-value = 0.6526
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-3.057656 1.916238
sample estimates:
mean of x mean of y
59.95331 60.52402
Согласно значению p-value мы принимаем \(H_0\) и делаем вывод, что группы одинаковые.
1.3.5.3 Использование готовых пакетов
library("tableone")
<- CreateTableOne(vars=c("X", "Z"), strata="T", data=data, test=TRUE)
table print(table)
Stratified by T
0 1 p test
n 500 500
X (mean (SD)) 21.49 (2.04) 21.47 (1.99) 0.827
Z (mean (SD)) 60.52 (19.86) 59.95 (20.22) 0.653
1.4 Про важность предпосылок Гаусса-Маркова
1.4.1 Предпосылки Гаусса-Маркова
- Линейная модель: \(Y_i=\tau T_i+\beta X_i+\varepsilon_i\)
- \(T_i\) не выражается линейно через \(X_i\)
- \(\mathbb{E}\left(\varepsilon_i\right)=0\)
- Гомоскедастичность, некоррелированность ошибок \(\mathbb{V}\left(\varepsilon_i\right)=\sigma I\)
- Экзогенность \(\operatorname{Cov}\left(X_i, \varepsilon_i\right)=0\)
- Оцениваем методом наименьших квадратов: \[ \left(Y_i-\hat{\tau} T_i-\hat{\beta} X_i\right)^2 \rightarrow \min _{\hat{\tau}, \hat{\beta}} \]
- Свойства оценки (теорема Гаусса-Маркова): несмещенность, эффективность, состоятельность
1.4.2 Пример
Мы располагаем данными из 1000 наблюдений, про которые известно:
- \(X_i \sim U[0,1]\) - контрольная переменная: успехи команды в прошлом году
- \(T_i \mid X_i \sim \operatorname{Bernoulli}\left(0.1+0.8 X_i\right)\) - бинарная переменная переменная интереса: решение тренера взять команду
- \(Y_i=f\left(X_i, T_i\right)+\varepsilon_i\) - переменная исхода: успехи команды в этом году
- В соответствии с предпосылками оценим \(Y_i \sim T_i+X_i\)
- То же ли самое получится, если \(T_i \sim \operatorname{Bernoulli}(0.5)\) и оценим \(Y_i \sim T_i\)?
1.4.2.1 Генерация данных
Сгенерим данные согласно условию:
library(stargazer)
set.seed(123)
<- runif(1000, 0, 1)
X
<- rnorm(1000)
Y0 <- 10 * X + rnorm(1000) Y1
1.4.2.2 Генерация тритмента
Случай 1: эндогенный тритмент
= rbinom(1000, 1, 0.1 + 0.8*X)
T_one = Y1 * T_one + Y0 * (1 - T_one)
Y_one
= data.frame(X=X, Y=Y_one, T=T_one) data_one
Случай 2: экзогенный тритмент
= rbinom(1000, 1, 0.5)
T_two = Y1 * T_two + Y0 * (1 - T_two)
Y_two
= data.frame(X=X, Y=Y_two, T=T_two) data_two
1.4.2.3 Оценка эффекта
Сравним оценки:
<- lm(Y ~ T, data=data_one)
model_one_1 <- lm(Y ~ T + X, data=data_one)
model_one_2 <- lm(Y ~ T, data=data_two)
model_two_1 <- lm(Y ~ T + X, data=data_two)
model_two_2
#stargazer(model_one_1, model_one_2, model_two_1, model_two_2, type = 'text')
# В обычной жизни вы просто можете пользоваться старгейзером, как в комментраии выше
# Чтобы конкретно на этой странице влезло 4 регрессии, я воспользуюсь другой функцией msummary
<- list(endogenous_without_control = model_one_1,
m_list endogenous_with_control = model_one_2,
exogenous_without_control = model_two_1,
exogenous_with_control = model_two_2)
library('modelsummary')
msummary(m_list, output = 'markdown', stars = TRUE, title = 'Сравнение моделей')
endogenous_without_control | endogenous_with_control | exogenous_without_control | exogenous_with_control | |
---|---|---|---|---|
(Intercept) | 0.022 | -1.847*** | 0.030 | -2.234*** |
(0.094) | (0.102) | (0.096) | (0.120) | |
T | 6.260*** | 4.859*** | 5.069*** | 5.007*** |
(0.131) | (0.114) | (0.137) | (0.109) | |
X | 5.206*** | 4.612*** | ||
(0.199) | (0.189) | |||
Num.Obs. | 1000 | 1000 | 1000 | 1000 |
R2 | 0.695 | 0.820 | 0.577 | 0.735 |
R2 Adj. | 0.695 | 0.819 | 0.577 | 0.735 |
AIC | 4299.4 | 3778.1 | 4390.7 | 3924.6 |
BIC | 4314.2 | 3797.7 | 4405.4 | 3944.3 |
Log.Lik. | -2146.714 | -1885.056 | -2192.343 | -1958.311 |
F | 2279.232 | 2264.086 | 1363.774 | 1385.366 |
RMSE | 2.07 | 1.59 | 2.17 | 1.71 |
Note: ^^ + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001
Выводы:
- По теореме Гаусса-Маркова мы должны были получить 2 состоятельные (значит одинаковые) оценки
- Они отличаются, потому что модель нелинейная: тренер решает тренировать только те команды, которые будут успешны, поэтому и эффект высокий
- Если бы эффект тренера всегда был одинаковым, теорема Гаусса-Маркова выполнялась бы и оценки действительно оказались бы одинаковыми.
1.4.2.4 Баланс ковариатов
- \(H_0:\) среднее значение параметра в группах одинаковое, группы не различаются
- \(H_1:\) среднее значение параметра в группах разное, группы различаются
<- CreateTableOne(vars=c("X", "Z"), strata="T", data=data_one, test=TRUE) table1
Warning in ModuleReturnVarsExist(vars, data): The data frame does not have: Z
Dropped
print(table1)
Stratified by T
0 1 p test
n 486 514
X (mean (SD)) 0.36 (0.25) 0.63 (0.26) <0.001
<- CreateTableOne(vars=c("X", "Z"), strata="T", data=data_two, test=TRUE) table2
Warning in ModuleReturnVarsExist(vars, data): The data frame does not have: Z
Dropped
print(table2)
Stratified by T
0 1 p test
n 514 486
X (mean (SD)) 0.49 (0.29) 0.50 (0.29) 0.463
Согласно значению p-value мы принимаем \(H_0\) для второго случая, где тритмент экзогенный, и делаем вывод, что группы одинаковые; для первого случая с эндогенным тритментом баланс ковариатов не выполняется.