UPDATE: 2022-10-28 05:31:59
このノートではRを使って因果推論に関する基礎的な理論から分析を実行するための方法をまとめている。ここでは回帰不連続デザインについて、下記の矢内先生の資料(資料のライセンスに基づく)を参考にしながら、回帰不連続デザインをまとめている。私の記事は分析の方法メインなので、矢内先生の資料を読む方が数理も学べて良いと思う。
上記の資料のライセンスは下記の通り。
The text of this work is licensed under the Creative Commons Attribution 4.0 International License. The R Code in this work is licensed under the MIT License.
回帰不連続デザインの数理については下記が詳しいので、ここでは扱わない。
必要なライブラリを読み込んでおく。
library(tidyverse)
library(broom)
library(rdd)
library(raincloudplots)
library(rdrobust)
ここで使用するサンプルデータは、参考資料と同じく法定飲酒年齢の引き下げと21歳の誕生日付近の死亡者数に関する研究のデータ。
このデータにはMLDAが21歳の場合のみが含まれており、このデータの1レコードは「誕生日によって区切られた年齢グループ」とのことで、1レコードが19歳の誕生日からおよそ30日単位のブロックになっているデータ。
# data: http://masteringmetrics.com/wp-content/uploads/2015/01/AEJfigs.dta
# agecell: 19歳の誕生日からおよそ30日単位のブロック(0-30/31-60/61-90/...)のようなイメージ
# all: 死者数
<- read_csv('~/Desktop/AEJfigs.csv', show_col_types = FALSE)
df df
## # A tibble: 48 × 2
## agecell all
## <dbl> <dbl>
## 1 19.1 92.8
## 2 19.2 95.1
## 3 19.2 92.1
## 4 19.3 88.4
## 5 19.4 88.7
## 6 19.5 90.2
## 7 19.6 96.2
## 8 19.6 89.6
## 9 19.7 93.4
## 10 19.8 90.9
## # … with 38 more rows
## # ℹ Use `print(n = ...)` to see more rows
回帰不連続デザインの解説で、(個人的に)よく見かける変数変換を行う。変数を中心化することで、基準値からのプラス・マイナスのズレで表現できる。
<- df %>%
df mutate(age = agecell - 21, # 21歳が基準
over = as.integer(agecell > 21), # 21歳以上を表す処置変数
age_sq = age^2)
df
## # A tibble: 48 × 5
## agecell all age over age_sq
## <dbl> <dbl> <dbl> <int> <dbl>
## 1 19.1 92.8 -1.93 0 3.73
## 2 19.2 95.1 -1.85 0 3.42
## 3 19.2 92.1 -1.77 0 3.12
## 4 19.3 88.4 -1.68 0 2.84
## 5 19.4 88.7 -1.60 0 2.57
## 6 19.5 90.2 -1.52 0 2.31
## 7 19.6 96.2 -1.44 0 2.07
## 8 19.6 89.6 -1.36 0 1.84
## 9 19.7 93.4 -1.27 0 1.62
## 10 19.8 90.9 -1.19 0 1.42
## # … with 38 more rows
## # ℹ Use `print(n = ...)` to see more rows
21という値が0に位置するので、イメージとしてはこのようになる。
<- data_1x1(
df_1x1 array_1 = df %>% pull(agecell),
array_2 = df %>% pull(age),
jit_distance = .09,
jit_seed = 1)
raincloud_1x1_repmes(
data = df_1x1,
colors = (c('dodgerblue', 'darkorange')),
fills = (c('dodgerblue', 'darkorange')),
line_color = 'gray',
# line_alpha = .3,
# size = 1,
# alpha = .5,
align_clouds = FALSE) + theme_bw()
パラメトリックRDでは、回帰式の関数形を仮定し、割当変数(running variable)の最小から最大までの範囲について回帰式の予測値を計算することで局所平均処置効果を計算する。 つまり、回帰不連続デザインでは回帰モデルの関数形を正しく特定する必要がある。誤った関数形で効果があったように見えても、正しい関数形では効果はないかもしれないため。ここでは参考資料に従い、4つのモデルを作成する。
<- alist(
models model1 = all ~ over + age,
model2 = all ~ over * age,
model3 = all ~ over + age + age_sq,
model4 = all ~ over * age + over * age_sq) %>%
enframe(name = "model_name", value = "formula")
models
## # A tibble: 4 × 2
## model_name formula
## <chr> <list>
## 1 model1 <language>
## 2 model2 <language>
## 3 model3 <language>
## 4 model4 <language>
用意したモデルをmap()
を利用して、一気に回帰係数を推定し、予測、結果の整形を行う。map()
が理解し難い場合はfor-loop
で4回繰り返したと思えばOK。
<- models %>%
rdd mutate(
model = map(.x = formula, .f = function(x){lm(formula = x, data = df)}),
pred = map(.x = model, .f = function(x){predict(x)}),
result = map(.x = model, .f = function(x){tidy(x)})
) rdd
## # A tibble: 4 × 5
## model_name formula model pred result
## <chr> <list> <list> <list> <list>
## 1 model1 <language> <lm> <dbl [48]> <tibble [3 × 5]>
## 2 model2 <language> <lm> <dbl [48]> <tibble [4 × 5]>
## 3 model3 <language> <lm> <dbl [48]> <tibble [4 × 5]>
## 4 model4 <language> <lm> <dbl [48]> <tibble [6 × 5]>
1つ目のモデルの結果を可視化すると、基準点を境に死亡数が高くなっていることがわかる。
%>%
df mutate(pred = rdd$pred[[1]]) %>%
ggplot(aes(x = agecell, color = as.factor(over))) +
geom_point(aes(y = all)) +
geom_line(aes(y = pred)) +
geom_vline(xintercept = 21, color = "gray") +
theme_bw() +
theme(legend.position = "none")
この場合の局所平均処置効果はover
の係数に該当するので、回帰係数を取り出すと、7.66
ほど10万人あたりの死亡数が高くなっていることになる。
%>%
rdd unnest(cols = result) %>%
filter(model_name == "model1", term == "over") %>%
select(term, estimate, std.error)
## # A tibble: 1 × 3
## term estimate std.error
## <chr> <dbl> <dbl>
## 1 over 7.66 1.44
他のモデルも可視化して、基準点でジャンプがあるか確認してみると、すべての関数形でジャンプが確認できるため、21歳を基準に10万人あたりの死亡数が高くなっていると思われる。
%>%
df select(agecell, over, all) %>%
mutate(
model1 = rdd$pred[[1]],
model2 = rdd$pred[[2]],
model3 = rdd$pred[[3]],
model4 = rdd$pred[[4]],
%>%
) pivot_longer(
cols = model1:model4,
names_to = "model",
values_to = "pred"
%>%
) ggplot(aes(x = agecell, color = as.factor(over))) +
geom_point(aes(y = all)) +
geom_line(aes(y = pred)) +
geom_vline(xintercept = 21, color = "gray") +
facet_wrap( ~ model) +
theme_bw() +
theme(legend.position = "none")
推定した4つのモデルの結果を比べると、4つ目のモデルの局所平均処置効果は9.54
となっており、関数形の違いで結果が異なってしまうことが回帰不連続デザインで起こりうる。つまり、誤った関数形を仮定すると、誤った推定結果を得ることにつながる。
%>%
rdd unnest(cols = result) %>%
filter(term == "over") %>%
select(term, estimate, std.error)
## # A tibble: 4 × 3
## term estimate std.error
## <chr> <dbl> <dbl>
## 1 over 7.66 1.44
## 2 over 7.66 1.32
## 3 over 7.66 1.34
## 4 over 9.55 1.99
ノンパラメトリックRDでは回帰式の関数形を考えず、推定対象となる範囲(バンド幅)を基準値の周辺に限定し、処置群と統制群の平均値を比較する。つまり、バンド幅の選び方によって推定値が変わる。ノンパラメトリックRDではバンド幅の選び方が重要。ただ、基準値付近の値を利用するので、サンプルサイズが大きい場合、バンド幅が狭いほどバイアスは小さくなる。
暫定的にバンド幅を下記の通り0.25刻みで用意し、
<- tibble(bandwidth = seq(from = 0.5, to = 1.5, by = 0.25))
df2 df2
## # A tibble: 5 × 1
## bandwidth
## <dbl>
## 1 0.5
## 2 0.75
## 3 1
## 4 1.25
## 5 1.5
このバンド幅でデータをフィルタリングする。
%>%
df2 mutate(
banding_data = map(.x = bandwidth, .f = function(x){df %>% filter(age > -1*x, age < x)})
%>% unnest(banding_data) %>%
) group_by(bandwidth) %>%
summarise(min = min(agecell),
max = max(agecell),
diff = max(agecell) - min(agecell),
sanplesize = n()
)
## # A tibble: 5 × 5
## bandwidth min max diff sanplesize
## <dbl> <dbl> <dbl> <dbl> <int>
## 1 0.5 20.5 21.5 0.904 12
## 2 0.75 20.3 21.7 1.40 18
## 3 1 20.1 21.9 1.89 24
## 4 1.25 19.8 22.2 2.38 30
## 5 1.5 19.6 22.4 2.88 36
フィルタリングしたデータをもとに回帰係数を計算すると、バンド幅が小さいほど標準誤差が大きく、バンド幅が大きくなると標準誤差は小さくなるし、バンド幅によって回帰係数の推定値も変化する。
<- df2 %>%
df3 mutate(
# formula = "all ~ over + age"として、map2で渡すときにas.formulaでも良い
formula = alist(all ~ over + age),
banding_data = map(.x = bandwidth, .f = function(x){df %>% filter(age > -1*x, age < x)}),
model = map2(.x = banding_data, .y = formula,
.f = function(x, y){lm(formula = y, data = x)}),
result = map(.x = model, .f = function(x){tidy(x)}),
pred = map2(.x = banding_data, .y = model,
.f = function(x, y){predict(y, newdata = x)}),
)%>%
df3 unnest(result) %>%
filter(term == "over") %>%
select(bandwidth, term, estimate, std.error)
## # A tibble: 5 × 4
## bandwidth term estimate std.error
## <dbl> <chr> <dbl> <dbl>
## 1 0.5 over 8.88 2.45
## 2 0.75 over 9.85 2.20
## 3 1 over 9.75 1.93
## 4 1.25 over 9.44 1.70
## 5 1.5 over 8.70 1.55
各バンド幅でフィルタリングされたデータや予測値を可視化すると各モデルの設定がわかりやすい。
%>%
df3 select(bandwidth, banding_data, pred) %>%
unnest(c(banding_data, pred)) %>%
ggplot(aes(x = agecell, color = as.factor(over))) +
geom_point(aes(y = all)) +
geom_line(aes(y = pred)) +
geom_vline(xintercept = 21, color = "gray") +
facet_wrap( ~ bandwidth) +
theme_bw() +
theme(legend.position = "none")
回帰不連続デザインを行えるRのrdrobustパッケージを利用して同じ分析を行う。バンド幅を0.5
で行うと、先程と同じく、基準値前後で6サンプル(Eff.
Number of
Obs.)が計算対象になっており、Coef.=8.878
局所局所平均処置効果を表している。
<- rdrobust(df$all, df$age, c = 0, h = 0.5 , kernel = "uniform")
rdd summary(rdd)
## Sharp RD estimates using local polynomial regression.
##
## Number of Obs. 48
## BW type Manual
## Kernel Uniform
## VCE method NN
##
## Number of Obs. 24 24
## Eff. Number of Obs. 6 6
## Order est. (p) 1 1
## Order bias (q) 2 2
## BW est. (h) 0.500 0.500
## BW bias (b) 0.500 0.500
## rho (h/b) 1.000 1.000
## Unique Obs. 24 24
##
## =============================================================================
## Method Coef. Std. Err. z P>|z| [ 95% C.I. ]
## =============================================================================
## Conventional 8.878 3.029 2.931 0.003 [2.942 , 14.815]
## Robust - - 2.055 0.040 [0.504 , 21.338]
## =============================================================================
rdrobustパッケージでは、カーネル関数を選択できる。回帰不連続デザインでのカーネル関数の役割はバンド幅のデータに対する重み付けである。下記のようなカーネル関数を利用して重み付けを行う。uniform
は重みが1
なので、手計算で行ってきた数値と一致する。デフォルトでは三角形の関数が利用され、線形で重みが付けられる。
layout(matrix(1:4, 2, 2, byrow = TRUE))
plot(density(0, bw = 1, kernel = "gaussian"), main = "gaussian")
plot(density(0, bw = 1, kernel = "rectangular"), main = "uniform")
plot(density(0, bw = 1, kernel = "triangular"), main = "triangular")
plot(density(0, bw = 1, kernel = "epanechnikov"), main = "epanechnikov")
バンド幅の推奨される設定方法として、Imbens and kalyanaramanが考案したIKバンドもしくは、Coverage Error rate Regression Discontinuity(CER)がある。詳細は下記が詳しい。
IKバンドはrdbwselect_2014()
のh
で計算でき、
list(
rdbwselect_2014(df$all, df$age, c = 0, bwselect = "IK")
)
## [[1]]
## Call:
## rdbwselect_2014(y = df$all, x = df$age, c = 0, bwselect = "IK")
##
## BW Selector IK
## Number of Obs 48
## NN Matches 3
## Kernel Type Triangular
##
## Left Right
## Number of Obs 24 24
## Order Loc Poly (p) 1 1
## Order Bias (q) 2 2
##
## h b
## [1,] 1.645225 0.9166231
CERバンド幅を使用するときは、bwselect = "cerrd"
と指定する。
<- rdrobust(df$all, df$age, c = 0, bwselect = "cerrd")
rdd2 summary(rdd2)
## Sharp RD estimates using local polynomial regression.
##
## Number of Obs. 48
## BW type cerrd
## Kernel Triangular
## VCE method NN
##
## Number of Obs. 24 24
## Eff. Number of Obs. 5 5
## Order est. (p) 1 1
## Order bias (q) 2 2
## BW est. (h) 0.406 0.406
## BW bias (b) 0.780 0.780
## rho (h/b) 0.521 0.521
## Unique Obs. 24 24
##
## =============================================================================
## Method Coef. Std. Err. z P>|z| [ 95% C.I. ]
## =============================================================================
## Conventional 10.179 4.106 2.479 0.013 [2.131 , 18.226]
## Robust - - 2.171 0.030 [0.994 , 19.495]
## =============================================================================
回帰不連続デザインではRDプロットと呼ばれる可視化方法がある。これはx軸をビニングしてサンプルを分割し、そのビンでの平均値を計算し、多項式を当てはめることで、分布形を調べることができる。
<- rdplot(df$all, df$age, c = 0, p = 4) rdplt
RDプロットの設定詳細はsumamry()
で確認でき、引数を調整することで、設定は変更可能である。
summary(rdplt)
## Call: rdplot
##
## Number of Obs. 48
## Kernel Uniform
##
## Number of Obs. 24 24
## Eff. Number of Obs. 24 24
## Order poly. fit (p) 4 4
## BW poly. fit (h) 1.932 1.932
## Number of bins scale 1 1
##
## Bins Selected 4 5
## Average Bin Length 0.483 0.386
## Median Bin Length 0.483 0.386
##
## IMSE-optimal bins 8 5
## Mimicking Variance bins 4 5
##
## Relative to IMSE-optimal:
## Implied scale 0.500 1.000
## WIMSE variance weight 0.889 0.500
## WIMSE bias weight 0.111 0.500
回帰不連続デザインを実行するにあたり条件付き回帰関数の連続性を満たす必要がある。グラフ化して確認する方法と検定で行う方法がある。
グラフで確認する場合は、基準値の前後がなめらかになっているかどうかを確認する。基準値の後に大きな変化があるようであれば、回帰不連続デザインの仮定が満たされない可能性がある。
set.seed(1989)
<- rnorm(500, 0, 5)
x hist(x, breaks = 30)
abline(v = 0)
検定を行う際は、Cattaneo, Jansson and Maが提案する方法で検定できる。この検定では、帰無仮説が右連続と左連続が一致する、つまり連続であるが設定され、対立仮説は連続ではない、である。つまり、連続であるためには、帰無仮説が棄却されると困るので、p値が大きければ帰無仮説を棄却できにくくなるので、そのほうが望ましい。
さきほどのx
で検定すると、p=0.2925
なので、帰無仮説が棄却できず、帰無仮説を採択することになる。つまり、連続性の仮定を満たしていることになる。
library(rddensity)
<- rddensity(x, c = 0)
res summary(res)
##
## Manipulation testing using local polynomial density estimation.
##
## Number of obs = 500
## Model = unrestricted
## Kernel = triangular
## BW method = estimated
## VCE method = jackknife
##
## c = 0 Left of c Right of c
## Number of obs 262 238
## Eff. Number of obs 142 88
## Order est. (p) 2 2
## Order bias (q) 3 3
## BW est. (h) 3.734 2.629
##
## Method T P > |T|
## Robust -1.0526 0.2925
##
##
## P-values of binomial tests (H0: p=0.5).
##
## Window Length / 2 <c >=c P>|T|
## 0.226 12 8 0.5034
## 0.452 24 19 0.5424
## 0.678 35 22 0.1112
## 0.903 42 28 0.1196
## 1.129 49 33 0.0970
## 1.355 51 39 0.2461
## 1.581 57 45 0.2760
## 1.807 65 55 0.4114
## 2.033 76 61 0.2315
## 2.258 82 72 0.4684
検定結果を可視化する際は、rdplotdensity()
を利用する。これをみると連続ではないようにも見える。P-values of binomial tests
の結果は、どれも0.05を基準とすると下回っていないため、連続性の仮定が満たされていると思われる。
<- rdplotdensity(res, X = x, type = "both") plt