7.2 An example: Climate change disasters and humanitarianism
Here we load a couple necessary packages, load the data, and take a glimpse()
.
disaster <- read_csv("data/disaster/disaster.csv")
glimpse(disaster)
## Observations: 211
## Variables: 5
## $ id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25...
## $ frame <int> 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1,...
## $ donate <dbl> 5.6, 4.2, 4.2, 4.6, 3.0, 5.0, 4.8, 6.0, 4.2, 4.4, 5.8, 6.2, 6.0, 4.2, 4.4, 5.8, 5.4, 3.4,...
## $ justify <dbl> 2.95, 2.85, 3.00, 3.30, 5.00, 3.20, 2.90, 1.40, 3.25, 3.55, 1.55, 1.60, 1.65, 2.65, 3.15,...
## $ skeptic <dbl> 1.8, 5.2, 3.2, 1.0, 7.6, 4.2, 4.2, 1.2, 1.8, 8.8, 1.0, 5.4, 2.2, 3.6, 7.8, 1.6, 1.0, 6.4,...
Here is how to get the ungrouped mean and \(SD\) values for justify
and skeptic
, as presented in Table 7.3.
disaster %>%
select(justify, skeptic) %>%
gather() %>%
group_by(key) %>%
summarise(mean = mean(value),
sd = sd(value)) %>%
mutate_if(is.double, round, digits = 3)
## # A tibble: 2 x 3
## key mean sd
## <chr> <dbl> <dbl>
## 1 justify 2.87 0.93
## 2 skeptic 3.38 2.03
And here we get the same summary values, this time grouped by frame
.
disaster %>%
select(frame, justify, skeptic) %>%
gather(key, value, -frame) %>%
group_by(frame, key) %>%
summarise(mean = mean(value),
sd = sd(value)) %>%
mutate_if(is.double, round, digits = 3)
## # A tibble: 4 x 4
## # Groups: frame [2]
## frame key mean sd
## <int> <chr> <dbl> <dbl>
## 1 0 justify 2.80 0.849
## 2 0 skeptic 3.34 2.04
## 3 1 justify 2.94 1.01
## 4 1 skeptic 3.42 2.03
Let’s open brms.
library(brms)
Anticipating Table 7.4 on page 234, we’ll name this first model model1
.
model1 <-
brm(data = disaster, family = gaussian,
justify ~ 1 + frame,
chains = 4, cores = 4)
print(model1)
## Family: gaussian
## Links: mu = identity; sigma = identity
## Formula: justify ~ 1 + frame
## Data: disaster (Number of observations: 211)
## Samples: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
## total post-warmup samples = 4000
##
## Population-Level Effects:
## Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
## Intercept 2.80 0.09 2.63 2.97 3083 1.00
## frame 0.13 0.13 -0.11 0.39 3342 1.00
##
## Family Specific Parameters:
## Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
## sigma 0.94 0.05 0.85 1.03 4000 1.00
##
## Samples were drawn using sampling(NUTS). For each parameter, Eff.Sample
## is a crude measure of effective sample size, and Rhat is the potential
## scale reduction factor on split chains (at convergence, Rhat = 1).
The ‘Estimate’ (i.e., posterior mean) of the model intercept is the expected justify
value for when frame
is 0. The ‘Estimate’ for frame
is the expected difference when frame
is a 1. If all you care about is the posterior mean, you could do
fixef(model1)["Intercept", 1] + fixef(model1)["frame", 1]
## [1] 2.935378
which matches up nicely with the equation on page 233. But this wouldn’t be very Bayesian of us. It’d be more satisfying if we had an expression of the uncertainty in the value. For that, we’ll follow our usual practice of extracting the posterior samples, making nicely-named vectors, and summarizing a bit.
post <-
posterior_samples(model1) %>%
mutate(when_x_is_0 = b_Intercept,
when_x_is_1 = b_Intercept + b_frame)
post %>%
select(when_x_is_0, when_x_is_1) %>%
gather() %>%
group_by(key) %>%
summarize(mean = mean(value),
sd = sd(value)) %>%
mutate_if(is.double, round, digits = 3)
## # A tibble: 2 x 3
## key mean sd
## <chr> <dbl> <dbl>
## 1 when_x_is_0 2.80 0.089
## 2 when_x_is_1 2.94 0.092
Hayes referenced a \(t\)-test and accompanying \(p\)-value in the lower part of page 233. We, of course, aren’t going to do that. But we do have the 95% intervals in our print()
output, above, which we can also look at like so.
posterior_interval(model1)["b_frame", ]
## 2.5% 97.5%
## -0.1143789 0.3934074
And we can always plot.
post %>%
ggplot(aes(x = b_frame)) +
geom_density(size = 0, fill = dutchmasters$little_street[1]) +
geom_vline(xintercept = 0, color = dutchmasters$little_street[11]) +
scale_x_continuous(breaks = c(-.3, 0, .6)) +
scale_y_continuous(NULL, breaks = NULL) +
theme_07 +
theme(legend.position = "none")
We’ll use the update()
function to hastily fit model2
and model3
.
model2 <-
update(model1, newdata = disaster,
formula = justify ~ 1 + frame + skeptic,
chains = 4, cores = 4)
model3 <-
update(model1, newdata = disaster,
formula = justify ~ 1 + frame + skeptic + frame:skeptic,
chains = 4, cores = 4)
Note our use of the frame:skeptic
syntax in model3
. With that syntax we didn’t need to make an interaction variable in the data by hand. The brms package just handled it for us. An alternative syntax would have been frame*skeptic
. But if you really wanted to make the interaction variable by hand, you’d do this.
disaster <-
disaster %>%
mutate(interaction_variable = frame*skeptic)
Once you have interaction_variable
in the data, you’d specify a model formula within the brm()
function like formula = justify ~ 1 + frame + skeptic + interaction_variable
. I’m not going to do that, here, but you can play around yourself if so inclined.
Here are the quick and dirty coefficient summaries for our two new models.
posterior_summary(model2)
## Estimate Est.Error Q2.5 Q97.5
## b_Intercept 2.1307082 0.12777249 1.8829093 2.3891387
## b_frame 0.1169393 0.11481339 -0.1048338 0.3386479
## b_skeptic 0.2011168 0.02921602 0.1444075 0.2557176
## sigma 0.8422032 0.04201156 0.7664790 0.9304656
## lp__ -268.3729553 1.41272791 -271.8371388 -266.5718957
posterior_summary(model3)
## Estimate Est.Error Q2.5 Q97.5
## b_Intercept 2.4528494 0.14887569 2.1677181 2.7432500
## b_frame -0.5687102 0.21703257 -0.9937620 -0.1406217
## b_skeptic 0.1042967 0.03788573 0.0320306 0.1788565
## b_frame:skeptic 0.2027146 0.05483136 0.0927857 0.3061208
## sigma 0.8176487 0.04040054 0.7433312 0.8996959
## lp__ -262.2631690 1.53602416 -266.1279187 -260.2129900
Just focusing on our primary model, model3
, here’s another way to look at the coefficients.
stanplot(model3) +
theme_07
By default, brms::stanplot()
makes coefficient plots which depict the parameters of a model by their posterior means (i.e., dots), 50% intervals (i.e., thick horizontal lines), and 95% intervals (i.e., thin horizontal lines). As stanplot()
returns a ggplot2 object, one can customize the theme and so forth.
We’ll extract the \(R^2\) iterations in the usual way once for each model, and then combine them for a plot.
# for each of the three models, we create a separare R2 tibble
R2_model1 <-
bayes_R2(model1,
summary = F) %>%
as_tibble()
R2_model2 <-
bayes_R2(model2,
summary = F) %>%
as_tibble()
R2_model3 <-
bayes_R2(model3,
summary = F) %>%
as_tibble()
# here we combine them into one tibble, indexed by `model`
R2s <-
R2_model1 %>%
bind_rows(R2_model2) %>%
bind_rows(R2_model3) %>%
mutate(model = rep(c("model1", "model2", "model3"), each = 4000))
# now we plot
R2s %>%
ggplot(aes(x = R2)) +
geom_density(aes(fill = model), size = 0, alpha = 2/3) +
scale_fill_manual(NULL,
values = dutchmasters$little_street[c(3, 4, 8)] %>% as.character()) +
scale_y_continuous(NULL, breaks = NULL) +
xlab(expression(paste(italic(R)^2, " distribution"))) +
coord_cartesian(xlim = 0:1) +
theme_07
Here’s the \(\Delta R^2\) distribution for model3
minus model2
.
R2_model2 %>%
rename(model2 = R2) %>%
bind_cols(R2_model3) %>%
rename(model3 = R2) %>%
mutate(dif = model3 - model2) %>%
ggplot(aes(x = dif)) +
geom_density(color = "transparent",
fill = dutchmasters$little_street[9]) +
scale_y_continuous(NULL, breaks = NULL) +
labs(title = expression(paste("The ", Delta, italic(R)^2)),
subtitle = "Like in the text, the posterior\nmean is about 0.05.",
x = NULL) +
theme_07
In addition to the \(R^2\), one can use information criteria to compare the models. Here we’ll use the LOO to compare all three.
loo(model1, model2, model3)
## LOOIC SE
## model1 572.28 24.03
## model2 529.46 22.37
## model3 517.87 21.69
## model1 - model2 42.82 16.14
## model1 - model3 54.41 18.93
## model2 - model3 11.59 8.29
The point estimate for both multivariable models were clearly lower than that for model1
. The point estimate for the moderation model, model3
, was within the double-digit range lower than that for model2
, which typically suggests better fit. But notice how wide the standard error was. There’s a lot of uncertainty, there. Hopefully this isn’t surprising. Our \(R^2\) difference was small and uncertain, too. We can also compare them with AIC-type model weighting, which you can learn more about starting at this point in this lecture or this related vignette for the loo package. Here we’ll keep things simple and weight with the LOO.
model_weights(model1, model2, model3,
weights = "loo")
## model1 model2 model3
## 1.526031e-12 3.029832e-03 9.969702e-01
The model_weights()
results put almost all the relative weight on model3
. This doesn’t mean model3
is the “true model” or anything like that. It just suggests that it’s the better of the three with respect to the data.
Here are the results of the equations in the second half of page 237.
post <- posterior_samples(model3)
post %>%
transmute(if_2 = b_frame + `b_frame:skeptic`*2,
if_3.5 = b_frame + `b_frame:skeptic`*3.5,
if_5 = b_frame + `b_frame:skeptic`*5) %>%
gather() %>%
group_by(key) %>%
summarise(mean = mean(value),
sd = sd(value)) %>%
mutate_if(is.double, round, digits = 3)
## # A tibble: 3 x 3
## key mean sd
## <chr> <dbl> <dbl>
## 1 if_2 -0.163 0.135
## 2 if_3.5 0.141 0.112
## 3 if_5 0.445 0.142
7.2.1 Estimation using PROCESS brms.
Similar to what Hayes advertised with PROCESS, with our formula = justify ~ 1 + frame + skeptic + frame:skeptic
code in model3
, we didn’t need to hard code an interaction variable into the data. brms handled that for us.
7.2.2 Interpreting the regression coefficients.
7.2.3 Variable scaling and the interpretation of \(b_{1}\) and \(b_{3}\).
Making the mean-centered version of our \(W\) variable, skeptic
, is a simple mutate()
operation. We’ll just call it skeptic_c
.
disaster <-
disaster %>%
mutate(skeptic_c = skeptic - mean(skeptic))
And here’s how we might fit the model.
model4 <-
update(model3, newdata = disaster,
formula = justify ~ 1 + frame + skeptic_c + frame:skeptic_c,
chains = 4, cores = 4)
Here are the summaries of our fixed effects.
fixef(model4)
## Estimate Est.Error Q2.5 Q97.5
## Intercept 2.8053071 0.07760306 2.65492192 2.9598509
## frame 0.1183069 0.11189621 -0.10400167 0.3375879
## skeptic_c 0.1054818 0.03774031 0.03023059 0.1791417
## frame:skeptic_c 0.2013565 0.05505230 0.09431049 0.3095814
Here are the \(R^2\) distributions for model3
and model4
. They’re the same within simulaiton variance.
bayes_R2(model3) %>% round(digits = 3)
## Estimate Est.Error Q2.5 Q97.5
## R2 0.249 0.043 0.162 0.331
bayes_R2(model4) %>% round(digits = 3)
## Estimate Est.Error Q2.5 Q97.5
## R2 0.25 0.044 0.16 0.335
If you’re bothered by the differences resulting from sampling variation, you might increase the number of HMC iterations from the 2000-per-chain default. Doing so might look something like this:
model3 <-
update(model3,
chains = 4, cores = 4, warmup = 1000, iter = 10000)
model4 <-
update(model4,
chains = 4, cores = 4, warmup = 1000, iter = 10000)
Before we fit model5
, we’ll recode frame
to a -.5/.5 metric and name it frame_.5
.
disaster <-
disaster %>%
mutate(frame_.5 = ifelse(frame == 0, -.5, .5))
Time to fit model5
.
model5 <-
update(model4, newdata = disaster,
formula = justify ~ 1 + frame_.5 + skeptic_c + frame_.5:skeptic_c,
chains = 4, cores = 4)
Our posterior summaries match up nicely with the output in Hayes’s Table 7.4.
fixef(model5)
## Estimate Est.Error Q2.5 Q97.5
## Intercept 2.8657962 0.05735938 2.75379721 2.9757871
## frame_.5 0.1158725 0.10926503 -0.09908657 0.3282183
## skeptic_c 0.2053597 0.02800294 0.14900726 0.2593083
## frame_.5:skeptic_c 0.2011685 0.05637500 0.08829815 0.3169702