--- title: "Getting started with int3ract" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with int3ract} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 6, fig.height = 4, dpi = 96 ) ``` ## Why a region, and not a coefficient An interaction coefficient answers a question few people are actually asking. It says how the effect of one variable changes per unit of another, but not where along the moderator's range that effect is large enough to be distinguished from zero. Reporting the effect at the mean of the moderator, or at plus and minus one standard deviation, picks three arbitrary points out of a continuum. The Johnson-Neyman technique (Johnson and Neyman 1936; Johnson and Fay 1950) answers the question directly: it reports the *region of significance*, the stretch of the moderator over which the focal effect is distinguishable from zero. **int3ract** implements that technique, extends it to three-way interactions over a two-dimensional moderator grid (JN3), and applies it to Bayesian models by working on posterior draws instead of point estimates. The package has one entry point, `JN()`, which dispatches on the fitted object. Models carrying point estimates and a covariance matrix are analysed with Wald tests; objects carrying draws are analysed as conditional posteriors. Which of the two happens follows from the object you hand in, not from which function you call. ```{r} library(int3ract) set.seed(1402) dat <- data.frame(x = rnorm(100), z = rnorm(100), w = rnorm(100)) dat$y <- dat$x + 0.5 * dat$z - 0.5 * dat$w + 0.5 * dat$x * dat$z * dat$w + rnorm(100, sd = 4) ``` ## Two-way interactions Fit the model as usual, then hand it to `JN()` along with the two variables involved in the interaction. The interaction term itself is found automatically, provided it is named in the standard R fashion (`x:z`). ```{r} fit2 <- lm(y ~ x * z, data = dat) jn2 <- JN(fit2, theta_1 = "x", theta_2 = "z") jn2 ``` Note that both variables get a turn as the focal one. In a two-way interaction neither variable is privileged: `x` moderated by `z` and `z` moderated by `x` are two views of the same model, and which one is interesting is a question about your theory, not about the fit. Here the effect of `x` is positive for values of `z` up to about 1.37, and the effect of `z` is nowhere distinguishable from zero. `summary()` adds the ranges over which the analysis was carried out: ```{r} summary(jn2) ``` For two-way Wald analyses the boundaries are solved for exactly, from the quadratic that the delta method produces. They do not depend on how finely the moderator grid is sampled, so `range_size` affects only the smoothness of the figures, not the numbers reported here. ### The figures `plot()` draws one figure per variable — two for a two-way analysis. Pass `which` for a single one. ```{r fig.alt = "Conditional effect of x across the range of z, with a confidence band shaded by significance and a histogram of observed z values below."} plot(jn2, which = "x") ``` The panel underneath is not decoration. A region of significance that covers moderator values nobody in the sample actually had is not evidence of much, and it is the standard failure mode of this technique. The histogram shows how much empirical support each part of the range has. Observed values are taken from `lm`, `glm` and **lme4** models automatically; for any other input you can supply them through the `support` argument. Turn the panels off with `jn_style(show_density = FALSE)`. ### Getting at the numbers `jn_regions()` returns the regions as a data frame, which is the form you want when the results go into a table or a further computation: ```{r} jn_regions(jn2) ``` `data_share` is the proportion of observations falling inside the region — 88% here, so this is a region with real support behind it. `as.data.frame()` gives the full grid instead: the conditional effect, its standard error, `p` value and confidence limits at every moderator value evaluated. ```{r} head(as.data.frame(jn2), 3) ``` `coef()` and `vcov()` return the coefficients and covariance submatrix the analysis was built from. ## Three-way interactions With a third variable the moderator becomes two-dimensional, and the region of significance becomes an area on a grid. Everything else is the same; pass `theta_3`. ```{r} fit3 <- lm(y ~ x * z * w, data = dat) jn3 <- JN(fit3, theta_1 = "x", theta_2 = "z", theta_3 = "w", range_size = 20) jn3 ``` Each of the three variables takes its turn as the focal one, moderated by the other two jointly. `print()` and `summary()` slice the grid at a few values of the first moderator to keep the report readable; `summary(jn3, at = ...)` and `jn_regions(jn3, at = ...)` let you choose those slices yourself. Here the grid is sampled rather than solved, so `range_size` does matter: it sets the resolution in each dimension. The default is finer than the 20 used above, which is set low here only to keep the vignette quick. ```{r fig.width = 6, fig.height = 5, fig.alt = "Heatmap of the conditional effect of x over the two-dimensional grid of z and w, crosshatched where the effect is not significant, with marginal histograms of observed values along both axes."} plot(jn3, which = "x") ``` The heatmap shows the conditional effect over the grid, crosshatched where it is not distinguishable from zero, with the observed values of both moderators along the margins. Testing every cell of a grid is a lot of tests. `control_fdr = TRUE` applies the Benjamini-Hochberg step-up procedure across the grid, controlling the false discovery rate: ```{r} jn3_fdr <- JN(fit3, theta_1 = "x", theta_2 = "z", theta_3 = "w", range_size = 20, control_fdr = TRUE) head(jn_regions(jn3_fdr), 3) ``` ## Mixed-effects models **lme4** models work the same way. By default the analysis uses the fixed effects. ```{r, eval = requireNamespace("lme4", quietly = TRUE)} d <- dat d$g <- rep(letters[1:5], each = 20) fit_mer <- lme4::lmer(y ~ x * z + (1 | g), data = d) JN(fit_mer, theta_1 = "x", theta_2 = "z") ``` `fixed_only = FALSE` adds one analysis per group, returning a `JN_list` that carries the same `print()`, `summary()` and `plot()` methods as a single result. Note that this uses the conditional modes, which are predictions rather than estimates, so the group-level analyses are descriptive. ## Bayesian models Hand `JN()` a matrix of posterior draws — or an `mcmc` object, an `mcmc.list`, or a data frame — and the analysis is carried out on conditional posteriors instead of Wald tests. Columns are matched by name, using the same `x:z` convention. ```{r bayes, eval = requireNamespace("MCMCpack", quietly = TRUE), message = FALSE} post <- MCMCpack::MCMCregress(y ~ x * z, data = dat, burnin = 500, mcmc = 2000, verbose = 0) jnb <- JN(post, theta_1 = "x", theta_2 = "z", theta_1_vals = seq(-3, 3, 0.5), theta_2_vals = seq(-3, 3, 0.5)) jnb ``` Nothing about the call changed except the object. Extra columns that are not part of the interaction — the intercept and `sigma2` here — are ignored. Because a matrix of draws carries no record of the data behind it, the regions come without a `data_share` and the figures without their histograms. Pass the observed moderator values through `support` to get both back. A two-way Bayesian analysis can be drawn two ways. The default overlays the conditional posterior densities: ```{r, eval = requireNamespace("MCMCpack", quietly = TRUE), fig.alt = "Overlaid conditional posterior densities of the effect of x at a sequence of values of z."} plot(jnb, which = "x") ``` `type = "band"` gives the posterior mean with its credible band against the moderator, which is directly comparable to the frequentist figure: ```{r, eval = requireNamespace("MCMCpack", quietly = TRUE), fig.alt = "Posterior mean effect of x across the range of z with a shaded credible band."} plot(jnb, which = "x", type = "band") ``` The `thresholds` argument sets the posterior quantiles that decide significance. The default is `c(0.025, 0.975)`, the Bayesian counterpart of a two-sided test at `alpha = 0.05`. If you are reproducing results from version 1.0.x, be aware that the old `JNK_bayes()` default was effectively `c(0.5, 0.5)`, which marked very nearly every cell significant. ## Appearance and saving `jn_style()` collects the colour, pattern and density-panel settings in one place, and is passed to `plot()`, `autoplot()` and `jn_plots()`: ```{r fig.alt = "The same conditional effect plot for x, drawn in different colours and without the data-density panel."} plot(jn2, which = "x", style = jn_style(sig_color = "steelblue", non_sig_color = "grey70", show_density = FALSE)) ``` The figures are ordinary **ggplot2** objects, so you can extend them with `+` in the usual way. `jn_plots()` returns every figure of an analysis at once without drawing them, `autoplot()` returns a single one, and `jn_save()` writes them all to disk: ```{r} figs <- jn_plots(jn2) names(figs) ``` ```{r eval = FALSE} jn_save(jn2, folder = "figures", device = "pdf") ``` ## Stochastic actor-oriented models **RSiena** and **multiSiena** results are handled directly. Effects are addressed by integer position rather than by name, because effect names are not unique within a SAOM; if a position is out of range, the error lists the available effects. ```{r eval = FALSE} # siena07() results carry estimates and a covariance matrix -> Wald tests JN(saom_fit, theta_1 = 4, theta_2 = 7, theta_int_12 = 12, theta_1_vals = c(0, 6), theta_2_vals = c(-2, 2)) # sienaBayes() results carry draws -> conditional posteriors JN(bayes_fit, theta_1 = 4, theta_2 = 7, theta_int_12 = 12, theta_1_vals = seq(0, 6, 1), theta_2_vals = seq(-2, 2, 1)) ``` For `sienaBayes()` results the package works out for itself whether each parameter was estimated as shared across groups (`Eta`) or as varying between them (`Mu`), takes the corresponding draws, and reports which it used in `print()`, `summary()` and on the figures. `hyper_only = FALSE` adds one analysis per group, as `fixed_only = FALSE` does for **lme4**; it is skipped, with a message, when no parameter involved varies between groups, since the group analyses would be identical copies of the population one. ## Supporting a further model class `JN()` works on anything with a `jn_input()` method. Writing one means returning either point estimates with their covariance matrix, through `jn_wald()`, or a matrix of draws, through `jn_posterior()`: ```{r eval = FALSE} jn_input.myfit <- function(object, theta_1, theta_2, theta_3 = NULL, ...) { idx <- c(theta_1, theta_2, paste(theta_1, theta_2, sep = ":")) jn_wald(coefficients = object$estimates[idx], vcov = object$covariance[idx, idx], labels = c(theta_1, theta_2)) } ``` Nothing else has to change: `print()`, `summary()`, `plot()` and the rest work on the result immediately. This is also the route for models fitted by variational inference, where the variational parameters approximate a posterior and `jn_posterior()` is the natural return. ## Migrating from 1.0.x `JNK_freq()` and `JNK_bayes()` are deprecated in favour of `JN()`. They still work, delegate to `JN()`, return the 1.0.x list layout and warn once per session. They will be removed in a future release. **Two-way frequentist results from version 1.0.x should be regenerated.** Those versions computed the variance of the conditional effect using the covariance between the two main effects, where the delta method calls for the covariance between the focal main effect and the interaction. Confidence bands, `p` values and therefore the regions of significance of every two-way frequentist analysis are affected. Three-way analyses and all Bayesian analyses were computed correctly and are unchanged. See `NEWS.md` for the full list of changes. ## References Johnson PO, Neyman J (1936). "Tests of Certain Linear Hypotheses and Their Application to Some Educational Problems." *Statistical Research Memoirs*, 1, 57-93. Johnson PO, Fay LC (1950). "The Johnson-Neyman Technique, Its Theory and Application." *Psychometrika*, 15(4), 349-367. [doi:10.1007/BF02288864](https://doi.org/10.1007/BF02288864) Bauer DJ, Curran PJ (2005). "Probing Interactions in Fixed and Multilevel Regression: Inferential and Graphical Techniques." *Multivariate Behavioral Research*, 40(3), 373-400. [doi:10.1207/s15327906mbr4003_5](https://doi.org/10.1207/s15327906mbr4003_5) Krause RW (2026). *int3ract: Johnson-Neyman Technique and its Three-Way Extension for Frequentist and Bayesian Models in R.* arXiv:2604.22051. [doi:10.48550/arXiv.2604.22051](https://doi.org/10.48550/arXiv.2604.22051)