11 Fundamentals of Conditional Process Analysis
Thus far in this book, mediation and moderation have been treated as distinct, separate, and independent concepts with different analytical procedures and interpretations. Yet processes modeled with mediation analysis likely are contingent and hence moderated, in that they operate differently for different people or in different contexts or circumstances. A more complete analysis, therefore, should attempt to model the mechanisms at work linking \(X\) to \(Y\) while simultaneously allowing those effects to be contingent on context, circumstance, or individual differences. (p. 395)
11.1 Examples of conditional process models in the literature
You can look up the various examples in the literature on your own. The main point is
moderation can be combined with mediation in a number of different ways. But these examples [we skipped for the sake of brevity] only scratch the surface of what is possible. Think about the number of possibilities when you increase the number of mediators, distinguish between moderation of paths in a parallel versus serial multiple mediator model, or allow for multiple moderators of different paths or the same path, and so forth. The possibilities are nearly endless. But regardless of the configuration of moderated paths or complexity of the model, conditional process analysis involves the estimation and interpretation of direct and indirect effects, just as in a simple mediation analysis. However, when causal effects in a mediation model are moderated, they will be conditional on those moderators. Thus, an understanding of the concepts of the conditional direct effect and the conditional indirect effect is required before one should attempt to under- take a conditional process analysis. (p. 401, emphasis in the original)
11.2 Conditional direct and indirect effects
When a direct or indirect effect is conditional, analysis and interpretation of the results of the modeling process should be based on a formal estimate of and inference about conditional direct and/or conditional in- direct effects. In this section, [Hayes illustrated] the computation of conditional direct and indirect effects for example models that combine moderation and mediation. (p. 403)
11.3 Example: Hiding your feelings from your work team
Here we load a couple necessary packages, load the data, and take a glimpse()
.
## Observations: 60
## Variables: 4
## $ dysfunc <dbl> -0.23, -0.13, 0.00, -0.33, 0.39, 1.02, -0.35, -0.23, 0.39, -0.08, -0.23, 0.09, -0.29, -0.06…
## $ negtone <dbl> -0.51, 0.22, -0.08, -0.11, -0.48, 0.72, -0.18, -0.13, 0.52, -0.26, 1.08, 0.53, -0.19, 0.15,…
## $ negexp <dbl> -0.49, -0.49, 0.84, 0.84, 0.17, -0.82, -0.66, -0.16, -0.16, -0.16, -0.16, 0.50, 0.84, 0.50,…
## $ perform <dbl> 0.12, 0.52, -0.08, -0.08, 0.12, 1.12, -0.28, 0.32, -1.08, -0.28, -1.08, -0.28, -0.28, -0.88…
Load the brms package.
Recall that we fit mediation models with brms using multivariate syntax. In previous attempts, we’ve defined and saved the model components outside of the brm()
function and then plugged then into brm()
using their identifier. Just to shake things up a bit, we’ll just do all the steps right in brm()
, this time.
model11.1 <-
brm(data = teams, family = gaussian,
bf(negtone ~ 1 + dysfunc) +
bf(perform ~ 1 + dysfunc + negtone + negexp + negtone:negexp) +
set_rescor(FALSE),
chains = 4, cores = 4)
## Family: MV(gaussian, gaussian)
## Links: mu = identity; sigma = identity
## mu = identity; sigma = identity
## Formula: negtone ~ 1 + dysfunc
## perform ~ 1 + dysfunc + negtone + negexp + negtone:negexp
## Data: teams (Number of observations: 60)
## 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 Rhat Bulk_ESS Tail_ESS
## negtone_Intercept 0.025 0.063 -0.104 0.150 1.002 6391 2962
## perform_Intercept -0.012 0.060 -0.129 0.104 1.001 6329 2866
## negtone_dysfunc 0.622 0.172 0.274 0.963 1.002 6698 2815
## perform_dysfunc 0.367 0.182 0.008 0.721 1.000 4817 3385
## perform_negtone -0.438 0.135 -0.701 -0.166 1.002 4929 3156
## perform_negexp -0.019 0.120 -0.254 0.212 1.001 5368 3143
## perform_negtone:negexp -0.516 0.245 -1.004 -0.029 1.000 4989 3347
##
## Family Specific Parameters:
## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
## sigma_negtone 0.487 0.047 0.406 0.590 1.000 6107 2860
## sigma_perform 0.460 0.045 0.380 0.562 1.000 5601 3187
##
## 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).
Our model summary coheres nicely with Table 11.1 and the formulas on page 409. Here are the \(R^2\) distribution summaries.
## Estimate Est.Error Q2.5 Q97.5
## R2negtone 0.194 0.079 0.041 0.349
## R2perform 0.321 0.078 0.152 0.459
On page 410 Hayes reported two sample means. Compute them like so.
## [1] -0.008
## [1] -0.032
For our Figure 11.4 and other similar figures in this chapter, we’ll use spaghetti plots. Recall that with a spaghetti plots for linear models, we only need two values for the variable on the x-axis, rather than the typical 30+.
nd <-
crossing(negtone = c(-.8, .8),
negexp = quantile(teams$negexp, probs = c(.16, .50, .84))) %>%
mutate(dysfunc = mean(teams$dysfunc))
Here’s our Figure 11.4, which uses only the first 40 HMC iterations for the spaghetti-plot lines.
# `fitted()`
fitted(model11.1,
newdata = nd,
resp = "perform",
summary = F) %>%
# wrangle
as_tibble() %>%
gather() %>%
bind_cols(
nd %>%
expand(nesting(negtone, negexp),
iter = 1:4000)
) %>%
mutate(negexp = factor(str_c("expresivity = ", negexp),
levels = c("expresivity = -0.49", "expresivity = -0.06", "expresivity = 0.6"))) %>%
filter(iter < 41) %>%
# plot
ggplot(aes(x = negtone, y = value, group = iter)) +
geom_line(color = "skyblue3",
size = 1/4) +
coord_cartesian(xlim = c(-.5, .5),
ylim = c(-.6, .6)) +
labs(x = expression(paste("Negative Tone of the Work Climate (", italic(M), ")")),
y = "Team Performance") +
theme_bw() +
theme(panel.grid = element_blank(),
strip.background = element_rect(color = "transparent", fill = "transparent")) +
facet_wrap(~negexp)
Also, the plot theme in this chapter is a nod to the style John Kruschke frequently uses in his papers and texts.
Using Hayes’s notation from the top of page 412, we can express \(M\)’s conditional effect on \(Y\) as
\[\theta_{M \rightarrow Y} = b_1 + b_3 W,\]
where \(M\) is negtone
, \(Y\) is perform
, and \(W\) is negexp
. We can extract our posterior summaries for \(b_1\) and \(b_3\) like so.
## Estimate Est.Error Q2.5 Q97.5
## perform_negtone -0.4378715 0.1351663 -0.701215 -0.16589701
## perform_negtone:negexp -0.5157503 0.2453382 -1.004435 -0.02860408
11.4 Estimation of a conditional process model using PROCESS
We just fit the model in the last section. No need to repeat.
11.5 Quantifying and visualizing (conditional) indirect and direct effects.
The analysis presented thus far has been piecemeal, in that [Hayes] addressed how to estimate the regression coefficients for each equation in this conditional process model and how to interpret them using standard principles of regression analysis, moderation analysis, and so forth. But a complete analysis goes further by integrating the estimates of each of the effects in the model (i.e., \(X \rightarrow M, \theta_{M \rightarrow Y}\)) to yield the direct and indirect effects of \(X\) on \(Y\). That is, the individual effects as quantified with the regression coefficients (conditional or otherwise) in equations 11.10 and 11.11 are not necessarily of immediate interest or relevance. Estimating them is a means to an end. What matters is the estimation of the direct and indirect effects, for they convey information about how \(X\) influences \(Y\) directly or through a mediator and how those effects are contingent on a moderator. (pp. 417–418)
11.5.0.1 The conditional indirect effect of \(X\).
One way to make a version of Table 11.2 is to work with the posterior_samples()
, simply summarizing the distributions with means.
post <-
posterior_samples(model11.1)
post %>%
mutate(a = b_negtone_dysfunc,
b1 = b_perform_negtone,
b3 = `b_perform_negtone:negexp`) %>%
expand(nesting(a, b1, b3),
w = c(-0.531, -0.006, 0.600)) %>%
mutate(conditional_effect = b1 + b3 * w,
conditional_indirect_effect = a * (b1 + b3 * w)) %>%
select(-(b1:b3)) %>%
pivot_longer(-w) %>%
group_by(w, name) %>%
summarise(mean = mean(value) %>% round(digits = 3)) %>%
pivot_wider(names_from = name, values_from = mean)
## # A tibble: 3 x 4
## # Groups: w [3]
## w a conditional_effect conditional_indirect_effect
## <dbl> <dbl> <dbl> <dbl>
## 1 -0.531 0.622 -0.164 -0.103
## 2 -0.006 0.622 -0.435 -0.271
## 3 0.6 0.622 -0.747 -0.465
That kind of summary isn’t the most Bayesian of us.
post %>%
mutate(a = b_negtone_dysfunc,
b1 = b_perform_negtone,
b3 = `b_perform_negtone:negexp`) %>%
expand(nesting(a, b1, b3),
w = c(-0.531, -0.006, 0.600)) %>%
mutate(conditional_effect = b1 + b3 * w,
conditional_indirect_effect = a * (b1 + b3 * w)) %>%
select(-(b1:b3)) %>%
pivot_longer(-w) %>%
mutate(label = str_c("W = ", w),
w = fct_reorder(label,
w)) %>%
ggplot(aes(x = value)) +
geom_vline(xintercept = 0, color = "grey50", linetype = 2) +
geom_histogram(color = "white", fill = "skyblue3") +
scale_y_continuous(NULL, breaks = NULL) +
xlab("posterior") +
theme_bw() +
theme(panel.grid = element_blank(),
strip.background = element_rect(color = "transparent", fill = "transparent")) +
facet_grid(w~name)
Here the posterior distribution for each is on full display.
11.5.0.2 The direct effect.
The direct effect of \(X\) on \(Y\) (i.e., dysfunc
on perform
) for this model is b_perform_dysfunc
in brms. Here’s how to get its summary values from posterior_summary()
.
## Estimate Est.Error Q2.5 Q97.5
## 0.367 0.182 0.008 0.721
11.5.1 Visualizing the direct and indirect effects.
For Figure 11.7 we’ll use the first 400 HMC iterations.
post <-
post %>%
mutate(`-0.7` = b_negtone_dysfunc * (b_perform_negtone + `b_perform_negtone:negexp` * -0.7),
`0.7` = b_negtone_dysfunc * (b_perform_negtone + `b_perform_negtone:negexp` * 0.7))
post %>%
select(b_perform_dysfunc, `-0.7`:`0.7`) %>%
pivot_longer(-b_perform_dysfunc) %>%
mutate(negexp = name %>% as.double(),
iter = rep(1:4000, times = 2)) %>%
filter(iter < 401) %>%
ggplot(aes(x = negexp, group = iter)) +
geom_hline(aes(yintercept = b_perform_dysfunc),
color = "skyblue3",
size = .3, alpha = .3) +
geom_line(aes(y = value),
color = "skyblue3",
size = .3, alpha = .3) +
coord_cartesian(xlim = c(-.5, .6),
ylim = c(-1.25, .75)) +
labs(x = expression(paste("Nonverbal Negative Expressivity (", italic(W), ")")),
y = "Effect of Dysfunctional Behavior on Team Performance") +
theme_bw() +
theme(panel.grid = element_blank())
Since the b_perform_dysfunc
values are constant across \(W\), the individual HMC iterations end up perfectly parallel in the spaghetti plot. This is an example of a visualization I’d avoid making with a spaghetti plot for a professional presentation. But hopefully it has some pedagogical value, here.
11.6 Statistical inference
11.6.1 Inference about the direct effect.
We’ve already been expressing undertainty in terms of percentile-based 95% intervals and histograms. Here’s a plot of the direct effect, b_perform_dysfunc
.
library(tidybayes)
post %>%
ggplot(aes(x = b_perform_dysfunc)) +
geom_histogram(binwidth = .025, boundary = 0,
color = "white", fill = "skyblue3", size = 1/4) +
stat_pointintervalh(aes(y = 0),
point_interval = mode_hdi, .width = .95) +
scale_x_continuous(breaks = mode_hdi(post$b_perform_dysfunc, .width = .95)[1, 1:3],
labels = mode_hdi(post$b_perform_dysfunc, .width = .95)[1, 1:3] %>% round(3)) +
scale_y_continuous(NULL, breaks = NULL) +
xlab("The direct effect (i.e., b_perform_dysfunc)") +
theme_bw() +
theme(panel.grid = element_blank(),
panel.border = element_blank(),
axis.line.x = element_line(size = 1/4))
Since we’re plotting in a style similar to Kruschke, we switched from emphasizing the posterior mean or median to marking off the posterior mode, which is Kruschkes’ preferred measure of central tendency. We also ditched our typical percentile-based 95% intervals for highest posterior density intervals. The stat_pointintervalh()
function from the Matthew Kay’s tidybayes package made it easy to compute those values with the point_interval = mode_hdi
argument. Note how we also used tidybayes::mode_hdi()
to compute those values and plug them into scale_x_continuous()
.
11.6.2 Inference about the indirect effect.
Much like above, we can make a plot of the conditional indirect effect \(ab_3\).
post <-
post %>%
mutate(ab_3 = b_negtone_dysfunc * `b_perform_negtone:negexp`)
post %>%
ggplot(aes(x = ab_3)) +
geom_histogram(binwidth = .025, boundary = 0,
color = "white", fill = "skyblue3", size = 1/4) +
stat_pointintervalh(aes(y = 0),
point_interval = mode_hdi, .width = .95) +
scale_x_continuous(expression(paste("The indirect effect, ", italic(ab)[3])),
breaks = mode_hdi(post$ab_3, .width = .95)[1, 1:3],
labels = mode_hdi(post$ab_3, .width = .95)[1, 1:3] %>% round(3)) +
scale_y_continuous(NULL, breaks = NULL) +
theme_bw() +
theme(panel.grid = element_blank(),
panel.border = element_blank(),
axis.line.x = element_line(size = 1/4))
11.6.3 Probing moderation of mediation.
One of the contributions of Preacher et al. (2007) to the literature on moderated mediation analysis was their discussion of inference for conditional indirect effects. They suggested two approaches, one a normal theory-based approach that is an analogue of the Sobel test in unmoderated mediation analysis, and another based on bootstrapping. (p. 426)
One of the contributions of this project is moving away from NHST in favor of Bayesian modeling. Since we’ve already been referencing him with our plot themes, you might check out Kruschke’s textbook for more discussion on Bayes versus NHST.
11.6.3.1 Normal theory approach.
As we’re square within the Bayesian modeling paradigm, we have no need to appeal to normal theory for the posterior \(SD\)s or 95% intervals.
11.6.3.2 Bootstrap confidence intervals Two types of Bayesian credible intervals.
We produced the posterior means corresponding to those in Table 11.3 some time ago. Here they are, again, with percentile-based 95% intervals via tidybayes::mean_qi()
.
post %>%
mutate(a = b_negtone_dysfunc,
b1 = b_perform_negtone,
b3 = `b_perform_negtone:negexp`) %>%
expand(nesting(a, b1, b3),
w = c(-0.531, -0.006, 0.600)) %>%
mutate(`a(b1 + b3w)` = a * (b1 + b3 * w)) %>%
group_by(w) %>%
mean_qi(`a(b1 + b3w)`) %>%
select(w:.upper) %>%
mutate_if(is.double, round, digits = 3)
## # A tibble: 3 x 4
## w `a(b1 + b3w)` .lower .upper
## <dbl> <dbl> <dbl> <dbl>
## 1 -0.531 -0.103 -0.409 0.161
## 2 -0.006 -0.271 -0.526 -0.077
## 3 0.6 -0.465 -0.823 -0.172
If we wanted to summarize those same effects with posterior modes and 95% highest posterior density intervals, instead, we’d replace our mean_qi()
lnie with mode_hdi()
.
post %>%
mutate(a = b_negtone_dysfunc,
b1 = b_perform_negtone,
b3 = `b_perform_negtone:negexp`) %>%
expand(nesting(a, b1, b3),
w = c(-0.531, -0.006, 0.600)) %>%
mutate(`a(b1 + b3w)` = a * (b1 + b3 * w)) %>%
group_by(w) %>%
mode_hdi(`a(b1 + b3w)`) %>%
select(w:.upper) %>%
mutate_if(is.double, round, digits = 3)
## # A tibble: 3 x 4
## w `a(b1 + b3w)` .lower .upper
## <dbl> <dbl> <dbl> <dbl>
## 1 -0.531 -0.072 -0.374 0.195
## 2 -0.006 -0.261 -0.497 -0.06
## 3 0.6 -0.479 -0.793 -0.152
And we might plot these with something like this.
post %>%
mutate(a = b_negtone_dysfunc,
b1 = b_perform_negtone,
b3 = `b_perform_negtone:negexp`) %>%
expand(nesting(a, b1, b3),
w = c(-0.531, -0.006, 0.600)) %>%
mutate(`a(b1 + b3w)` = a * (b1 + b3 * w)) %>%
select(w:`a(b1 + b3w)`) %>%
mutate(label = str_c("W = ", w),
w = fct_reorder(label,
w)) %>%
ggplot(aes(x = `a(b1 + b3w)`)) +
geom_vline(xintercept = 0, color = "grey50", linetype = 2) +
geom_histogram(binwidth = .05, boundary = 0,
color = "white", fill = "skyblue3", size = 1/4) +
stat_pointintervalh(aes(y = 0),
point_interval = mode_hdi, .width = .95) +
scale_x_continuous("The conditional indirect effect", limits = c(-1.25, .75)) +
scale_y_continuous(NULL, breaks = NULL) +
coord_flip() +
theme_bw() +
theme(panel.grid = element_blank(),
panel.border = element_blank(),
axis.line.y = element_line(size = 1/4),
strip.text = element_text(hjust = 0),
strip.background = element_rect(color = "transparent", fill = "transparent")) +
facet_wrap(~w, nrow = 1)
This, of course, leads us right into the next section.
11.6.3.3 A Johnson-Neyman approach.
On page 429, Hayes discussed how Preacher et al. (2007)’s attempt to apply the JN technique in this context presumed
the sampling distribution of the conditional indirect effect is normal. Given that the sampling distribution of the conditional indirect effect is not normal, the approach they describe yields, at best, an approximate solution. To [Hayes’s] knowledge, no one has ever proposed a bootstrapping-based analogue of the Johnson-Neyman method for probing the moderation of an indirect effect.
However, our Bayesian HMC approach makes no such assumption. All we need to do is manipulate the posterior as usual. Here it is, this time using all 4000 iterations.
post %>%
transmute(iter = 1:n(),
`-0.8` = b_perform_negtone + `b_perform_negtone:negexp` * -0.8,
`0.8` = b_perform_negtone + `b_perform_negtone:negexp` * 0.8) %>%
pivot_longer(-iter) %>%
mutate(key = name %>% as.double()) %>%
ggplot(aes(x = key, y = value, group = iter)) +
geom_line(color = "skyblue3",
size = 1/6, alpha = 1/15) +
coord_cartesian(xlim = c(-.5, .6),
ylim = c(-1.25, .75)) +
labs(x = expression(italic(W)),
y = "The conditional indirect effect") +
theme_bw() +
theme(panel.grid = element_blank())
Glorious.
Session info
## R version 3.6.0 (2019-04-26)
## Platform: x86_64-apple-darwin15.6.0 (64-bit)
## Running under: macOS High Sierra 10.13.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/3.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/3.6/Resources/lib/libRlapack.dylib
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] tidybayes_1.1.0 brms_2.10.3 Rcpp_1.0.2 forcats_0.4.0 stringr_1.4.0 dplyr_0.8.3
## [7] purrr_0.3.3 readr_1.3.1 tidyr_1.0.0 tibble_2.1.3 ggplot2_3.2.1 tidyverse_1.2.1
##
## loaded via a namespace (and not attached):
## [1] colorspace_1.4-1 ellipsis_0.3.0 ggridges_0.5.1 rsconnect_0.8.15
## [5] ggstance_0.3.2 markdown_1.1 base64enc_0.1-3 rstudioapi_0.10
## [9] rstan_2.19.2 svUnit_0.7-12 DT_0.9 fansi_0.4.0
## [13] lubridate_1.7.4 xml2_1.2.0 bridgesampling_0.7-2 knitr_1.23
## [17] shinythemes_1.1.2 zeallot_0.1.0 bayesplot_1.7.0 jsonlite_1.6
## [21] broom_0.5.2 shiny_1.3.2 compiler_3.6.0 httr_1.4.0
## [25] backports_1.1.5 assertthat_0.2.1 Matrix_1.2-17 lazyeval_0.2.2
## [29] cli_1.1.0 later_1.0.0 htmltools_0.4.0 prettyunits_1.0.2
## [33] tools_3.6.0 igraph_1.2.4.1 coda_0.19-3 gtable_0.3.0
## [37] glue_1.3.1.9000 reshape2_1.4.3 cellranger_1.1.0 vctrs_0.2.0
## [41] nlme_3.1-139 crosstalk_1.0.0 xfun_0.10 ps_1.3.0
## [45] rvest_0.3.4 mime_0.7 miniUI_0.1.1.1 lifecycle_0.1.0
## [49] gtools_3.8.1 zoo_1.8-6 scales_1.0.0 colourpicker_1.0
## [53] hms_0.4.2 promises_1.1.0 Brobdingnag_1.2-6 parallel_3.6.0
## [57] inline_0.3.15 shinystan_2.5.0 gridExtra_2.3 loo_2.1.0
## [61] StanHeaders_2.19.0 stringi_1.4.3 dygraphs_1.1.1.6 pkgbuild_1.0.5
## [65] rlang_0.4.1 pkgconfig_2.0.3 matrixStats_0.55.0 HDInterval_0.2.0
## [69] evaluate_0.14 lattice_0.20-38 rstantools_2.0.0 htmlwidgets_1.5
## [73] labeling_0.3 tidyselect_0.2.5 processx_3.4.1 plyr_1.8.4
## [77] magrittr_1.5 R6_2.4.0 generics_0.0.2 pillar_1.4.2
## [81] haven_2.1.0 withr_2.1.2 xts_0.11-2 abind_1.4-5
## [85] modelr_0.1.4 crayon_1.3.4 arrayhelpers_1.0-20160527 utf8_1.1.4
## [89] rmarkdown_1.13 grid_3.6.0 readxl_1.3.1 callr_3.3.2
## [93] threejs_0.3.1 digest_0.6.21 xtable_1.8-4 httpuv_1.5.2
## [97] stats4_3.6.0 munsell_0.5.0 shinyjs_1.0