## 7.4 Probing an interaction

### 7.4.1 The pick-a-point approach.

#### 7.4.1.1 The pick-a-point approach ~~implimented by regression centering~~ working directly with the posterior.

Yes, if you wanted to use the regression centering approach, you could do that in brms. Just center the necessary variables in the way Hayes described in the text, refit the model, and `summarize()`

. I suspect this would be particularly approachable for someone new to R and to the ins and outs of data wrangling. But I’m going leave that as an exercise for the interested reader.

Now that we’ve already got a posterior for our model, we can just either algebraically manipulate the vectors yielded by `posterior_samples()`

or push predictions through `fitted()`

. To give a sense, we’ll start off with the 16th percentile for `skeptic`

. Recall we can get that with the `quantile()`

function.

` quantile(disaster$skeptic, probs = .16)`

```
## 16%
## 1.6
```

Now we just need to feed that value and different values of `frame`

into the posterior samples of the model coefficients. We then create a `difference`

score for the model-implied estimates given `frame`

is either 0 or 1 and then plot that `difference`

.

```
post %>%
mutate(Y_given_frame_0_skeptic_1.6 = b_Intercept + b_frame*0 + b_skeptic*1.6 + `b_frame:skeptic`*0*1.6,
Y_given_frame_1_skeptic_1.6 = b_Intercept + b_frame*1 + b_skeptic*1.6 + `b_frame:skeptic`*1*1.6) %>%
mutate(difference = Y_given_frame_1_skeptic_1.6 - Y_given_frame_0_skeptic_1.6) %>%
ggplot(aes(x = difference)) +
geom_density(color = "transparent",
fill = dutchmasters$little_street[9]) +
geom_vline(xintercept = 0, color = dutchmasters$little_street[7], linetype = 2) +
scale_y_continuous(NULL, breaks = NULL) +
labs(subtitle = "The effect of frame on justify\ngiven skeptic = 1.6",
x = NULL) +
theme_07
```

Note how nicely that distribution corresponds to the output in the lower left corner of Hayes’s Figure 7.8. If we wanted the values for other values of `skeptic`

(e.g., 2.8 and 5.2 as in the text), we’d just rinse, wash, and repeat. A nice quality of this method is it requires you to work explicitly with the model formula. But it’s also clunky if you want to do this over many values. The `fitted()`

function offers an alternative.

Recall how the default `fitted()`

settings are to return summaries of a model’s \(Y\)-variable given values of the predictor variables. In the previous section we put our prefered `frame`

and `skeptic`

values into a data object named `nd`

and used the `newdata`

argument to push those values through `fitted()`

. Buy default, this yielded the typical posterior means, \(SD\)s, and 95% intervals for the predictions. However, if one sets `summary = F`

, the output will differ. First. Let’s revisit what `nd`

looks like.

```
(
nd <-
tibble(frame = rep(0:1, times = 3),
skeptic = rep(quantile(disaster$skeptic,
probs = c(.16, .5, .84)),
each = 2)) %>%
# This will make our lives easier in just a bit
arrange(frame)
)
```

```
## # A tibble: 6 x 2
## frame skeptic
## <int> <dbl>
## 1 0 1.6
## 2 0 2.8
## 3 0 5.2
## 4 1 1.6
## 5 1 2.8
## 6 1 5.2
```

Here’s what happens when we use `summary = F`

.

```
f_model3 <-
fitted(model3, newdata = nd, summary = F)
f_model3 %>% str()
```

`## num [1:4000, 1:6] 2.63 2.74 2.75 2.75 2.49 ...`

`f_model3 %>% head()`

```
## [,1] [,2] [,3] [,4] [,5] [,6]
## [1,] 2.633868 2.755445 2.998599 2.402675 2.733944 3.396481
## [2,] 2.736530 2.798106 2.921260 2.332534 2.750577 3.586663
## [3,] 2.753872 2.818162 2.946742 2.288811 2.716716 3.572527
## [4,] 2.745839 2.836755 3.018588 2.318846 2.668332 3.367305
## [5,] 2.493795 2.651769 2.967716 2.421827 2.791521 3.530908
## [6,] 2.522040 2.637816 2.869368 2.381670 2.768278 3.541495
```

With `summary = F`

, `fitted()`

returned a matrix of 4000 rows (i.e., one for each posterior iteration) and 6 vectors (i.e., one for each row in our `nd`

data). So now instead of summary information, we have a full expression of the uncertainty in terms of 4000 draws. If you prefer working within the tidyverse and plotting with ggplot2, matrices aren’t the most useful data type. Let’s wrangle a bit.

```
f_model3 <-
f_model3 %>%
as_tibble() %>%
gather() %>%
select(-key) %>%
# We multiply 4000 (i.e., the # of iterations) by 3 because there are 3 distinct `skeptic` values
mutate(frame = rep(0:1, each = 4000*3),
# Note how we have `rep()` nested within `rep()`.
skeptic = rep(rep(quantile(disaster$skeptic, probs = c(.16, .5, .84)),
each = 4000),
# We repeate the first `rep()` output 2 times because ther are 2 values of `frame` we'd like them for
times = 2),
# We need an iteration index, `iter`, in order to help with `spread()`, below.
iter = rep(1:4000, times = 6)) %>%
spread(key = frame, value = value) %>%
mutate(difference = `1` - `0`,
# This isnt' necessary, but will help with the facet labels
skeptic = str_c("skeptic = ", skeptic))
f_model3 %>% head()
```

```
## # A tibble: 6 x 5
## skeptic iter `0` `1` difference
## <chr> <int> <dbl> <dbl> <dbl>
## 1 skeptic = 1.6 1 2.63 2.40 -0.231
## 2 skeptic = 1.6 2 2.74 2.33 -0.404
## 3 skeptic = 1.6 3 2.75 2.29 -0.465
## 4 skeptic = 1.6 4 2.75 2.32 -0.427
## 5 skeptic = 1.6 5 2.49 2.42 -0.0720
## 6 skeptic = 1.6 6 2.52 2.38 -0.140
```

And here’s a plot of what we’ve done.

```
f_model3 %>%
ggplot(aes(x = difference)) +
geom_density(color = "transparent",
fill = dutchmasters$little_street[9]) +
geom_vline(xintercept = 0, color = dutchmasters$little_street[7], linetype = 2) +
scale_y_continuous(NULL, breaks = NULL) +
labs(subtitle = "The effect of frame on justify given three different values of skeptic",
x = NULL) +
theme_07 +
facet_wrap(~skeptic)
```

And if you prefered summary information instead of plots, you’d use `summarize()`

as usual.

```
f_model3 %>%
group_by(skeptic) %>%
summarize(median = median(difference),
ll = quantile(difference, probs = .025),
ul = quantile(difference, probs = .975)) %>%
mutate_if(is.double, round, digits = 3)
```

```
## # A tibble: 3 x 4
## skeptic median ll ul
## <chr> <dbl> <dbl> <dbl>
## 1 skeptic = 1.6 -0.243 -0.537 0.052
## 2 skeptic = 2.8 -0.001 -0.23 0.23
## 3 skeptic = 5.2 0.488 0.191 0.779
```

### 7.4.2 The Johnson-Neyman technique.

The JN technique generalizes this approach over many values of \(W\) (i.e., `skeptic`

in this example) in order to get a sense of the trend and summarize regions of the trend in terms of \(p\)-value thresholds. Since we’re emphasizing modeling and deemphasizing null-hypothesis testing in this project, I’ll show a Bayesian version of the approach without the \(p\)-values.

#### 7.4.2.1 Implementation in ~~PROCESS~~ brms.

Since Figure 7.9 had `skeptic`

values ranging from 1 to 6 with ticks on the 0.5s, we’ll use a similar approach for our version. We’ll estimate posterior samples with `fitted()`

for `skeptic`

values ranging from .5 to 6.5, one for each 0.5—13 in total. But since we have two levels of `frame`

(i.e., 0 and 1), that really gives us 26. And we don’t just want 26 summaries; we want full posterior distributions for each of those 26.

We’ve got a lot of moving parts in the code, below. To help make sure everything adds up, we’ll save several important values as R objects.

```
iter <- 4000 # this number comes from the total number of post-warmup posterior iterations from the `brm()` function
n_frame_values <- 2 # there are 2 levels of `frame`, 0 and 1
n_skeptic_values <- 13 # we're choosing 13 in this example to follow some of the sensibilities in Figure 7.9. You'll see.
# as before, we'll make `nd` to feed in to `fitted()`
nd <-
tibble(frame = rep(0:1, each = n_skeptic_values),
skeptic = rep(seq(from = .5, to = 6.5, length.out = n_skeptic_values),
times = n_frame_values))
# after the initial `fitted()` action, we need a few steps to wrangle the data into a useful format
f_model3 <-
fitted(model3, newdata = nd, summary = F) %>%
as_tibble() %>%
gather() %>%
mutate(frame = rep(0:1, each = iter*n_skeptic_values),
skeptic = rep(rep(seq(from = .5, to = 6.5, length.out = n_skeptic_values),
each = iter),
times = n_frame_values)) %>%
select(-key) %>%
rename(estimate = value) %>%
mutate(iter = rep(1:iter, times = n_frame_values*n_skeptic_values)) %>%
spread(key = frame, value = estimate) %>%
mutate(difference = `1` - `0`)
# finally, here's the plot
f_model3 %>%
ggplot(aes(x = skeptic %>% as.character, y = difference)) +
geom_hline(yintercept = 0, color = dutchmasters$little_street[7]) +
geom_violin(size = 0, fill = dutchmasters$little_street[6]) +
stat_summary(fun.y = median,
fun.ymin = function(x){quantile(x, probs = .025)},
fun.ymax = function(x){quantile(x, probs = .975)},
color = dutchmasters$little_street[5]) +
labs(x = expression(paste("Climate Change Skepticism (", italic(W), ")")),
y = expression(atop(theta[paste(italic(X), " on ", italic(Y))], paste("Conditional Effect of Disaster Frame")))) +
theme_07
```

[Note. I got the `atop()`

trick for the label for the y-axis from Drew Steen’s answer to this stackoverflow question.]

This isn’t quite our version of Figure 7.9, but I’m hoping it’ll add some pedagogical value for what we’re doing. Since we used `summary = F`

in `fitted()`

, we got full posterior distributions for each of our 26 conditions. *Because Figure 7.9 is all about differences between each frame pair across the various values of skeptic*, we needed to make a

`difference`

score for each pair; this is what we did with the last `mutate()`

line before the plot code. This initial version of the plot shows the full posterior distribution for each `difference`

score. The posteriors are depicted with violin plots, which are density plots set on their side and symmetrically reflected as if by a mirror to give a pleasing leaf- or violin-like shape (though beware). The light dots and vertical lines are the posterior medians and 95% intervals for each.Going from left to right, it appears we have a clearly emerging trend. We can more simply express the trend by summarizing each posterior with medians and 95% intervals.

```
f_model3 %>%
group_by(skeptic) %>%
summarize(median = median(difference),
ll = quantile(difference, probs = .025),
ul = quantile(difference, probs = .975)) %>%
ggplot(aes(x = skeptic)) +
geom_hline(yintercept = 0, color = dutchmasters$little_street[7]) +
geom_vline(xintercept = c(1.171, 3.934), color = dutchmasters$little_street[7]) +
geom_ribbon(aes(ymin = ll, ymax = ul),
fill = dutchmasters$little_street[5],
alpha = 1/2) +
geom_line(aes(y = median),
color = dutchmasters$little_street[5], size = 1) +
scale_x_continuous(breaks = 1:6) +
coord_cartesian(xlim = c(1, 6),
ylim = c(-1, 1.5)) +
labs(x = expression(paste("Climate Change Skepticism (", italic(W), ")")),
y = expression(atop(theta[paste(italic(X), " on ", italic(Y))], paste("Conditional Effect of Disaster Frame")))) +
theme_07
```

Notice how the contour boundaries of the 95% intervals are a little clunky. That’s because our bowtie-shape is based on only 13 x-axis values. If you wanted a smoother shape, you’d specify more `skeptic`

values in the data object you feed into `fitted()`

’s `newdata`

argument. For linear effects, 30 or so usually does it.

Anyway, I got the values for the two vertical lines directly out of the text. It’s not clear to me how one might elegantly determine those values within the paradigm we’ve been using. But that leads to an important digression. The two vertical lines are quite \(p\)-value centric. They are an attempt to separate the x-axis into areas where the `difference`

trend either is or is not statistically-significantly different from zero. That is, we’re dichotomizing—or “trichotomizing”, depending on how you look at it—a continuous phenomenon. This is somewhat at odds with the sensibilities of the Bayesians associated with Stan and brms (e.g., here).

On page 259, Hayes wrote:

Although the JN technique eliminates the need to select arbitrary values of \(W\) when probing an interaction, it does not eliminate your need to keep your brain turned into the task and thinking critically about the answer the method gives you.

I think this is valuable advice, particularly when working within the Bayesian paradigm. Our version of Figure 7.9 gives some interesting insights into the moderation model, `model3`

. I’m just not so sure I’d want to encourage people to interpret a continuous phenomenon by heuristically dividing it into discrete regions.