Every test the package reports is one general linear hypothesis on
the contrast layer, evaluated by one piece of code. That covers each row
of an ANOVA table, each custom hypothesis and each contrast plot. This
vignette derives that construction and shows where each of its parts
comes from. It is background: nothing here is needed to use the package,
and vignette("semnova") is the place to start.
A multigroup model has one coefficient matrix per group. Write
The \(\mathbf{B}_g\) are stacked
into a three-dimensional array of dimension \((p + 1) \times q \times
n_{\text{groups}}\). In semnova that array is the contrast (pi)
layer’s coefficients, returned by
regression_coefficients().
The hypotheses of interest have the form
\[ \mathbf{H}_0: \mathbf{G} \cdot (\mathbf{L} \cdot \mathbf{B}_g \cdot \mathbf{M}) = \mathbf{K} \]
where each matrix selects one aspect of the design:
| Symbol | Dimensions | Selects |
|---|---|---|
| \(\mathbf{L}\) | \(h \times (p + 1)\) | predictors: an intercept, or a covariate’s effect |
| \(\mathbf{M}\) | \(q \times r\) | contrast variables: the columns belonging to a within-subject term |
| \(\mathbf{G}\) | \(d \times n_{\text{groups}}\) | groups: the between-subject contrasts |
| \(\mathbf{K}\) | \(d \times h \times r\) | the values tested against, almost always zero |
A main effect, an interaction and a covariate effect differ only in which of these is non-trivial, which is why one implementation covers them all.
The hypothesis is stated in matrices but tested as a linear restriction on a parameter vector, so it has to be rewritten in terms of \(\text{vec}(\mathbf{B})\).
For a single group, the standard identity for the vectorisation of a matrix product gives
\[ \text{vec}(\mathbf{L} \cdot \mathbf{B}_g \cdot \mathbf{M}) = (\mathbf{M}^\top \otimes \mathbf{L}) \cdot \text{vec}(\mathbf{B}_g), \]
which maps a vector of length \((p + 1) \cdot q\) to one of length \(h \cdot r\).
Stacking the groups, the same transformation is applied to each block of \(\text{vec}(\mathbf{B})\):
\[ \text{vec} \begin{bmatrix} \mathbf{L} \mathbf{B}_1 \mathbf{M} \\ \vdots \\ \mathbf{L} \mathbf{B}_{n_{\text{groups}}} \mathbf{M} \end{bmatrix} = \left( \mathbf{I}_{n_{\text{groups}}} \otimes (\mathbf{M}^\top \otimes \mathbf{L}) \right) \cdot \text{vec}(\mathbf{B}), \qquad \mathbf{B} = \begin{bmatrix} \mathbf{B}_1 \\ \vdots \\ \mathbf{B}_{n_{\text{groups}}} \end{bmatrix}. \]
Applying \(\mathbf{G}\) across groups completes the construction:
\[ \mathbf{R} = (\mathbf{G} \otimes \mathbf{I}_{h \cdot r}) \cdot (\mathbf{I}_{n_{\text{groups}}} \otimes (\mathbf{M}^\top \otimes \mathbf{L})) \]
so that the hypothesis becomes a single linear restriction,
\[ \mathbf{H}_0: \mathbf{R} \cdot \text{vec}(\mathbf{B}) = \mathbf{k}, \qquad \mathbf{k} = \text{vec}(\mathbf{K}), \]
with one row of \(\mathbf{R}\) per contrast being tested and one column per coefficient.
| Quantity | Dimensions |
|---|---|
| \(\mathbf{B}_g\) | \((p + 1) \times q\) |
| \(\mathbf{L}\) | \(h \times (p + 1)\) |
| \(\mathbf{M}\) | \(q \times r\) |
| \(\mathbf{G}\) | \(d \times n_{\text{groups}}\) |
| \(\text{vec}(\mathbf{B})\) | \(n_{\text{groups}} \cdot (p + 1) \cdot q\) |
| \(\mathbf{R}\) | \((d \cdot h \cdot r) \times (n_{\text{groups}} \cdot (p + 1) \cdot q)\) |
| \(\mathbf{k}\) | \(d \cdot h \cdot r\) |
contrast_matrix() builds \(\mathbf{R}\) in the three steps derived
above:
contrast_matrix
#> function (G, L, M, B_array = NULL)
#> {
#> if (!is.matrix(G) || !is.matrix(L) || !is.matrix(M)) {
#> stop("semnova: G, L, and M must all be matrices", call. = FALSE)
#> }
#> h <- nrow(L)
#> r <- ncol(M)
#> d <- nrow(G)
#> n_groups <- ncol(G)
#> Rg <- kronecker(t(M), L)
#> R_within <- kronecker(diag(n_groups), Rg)
#> R <- kronecker(G, diag(h * r)) %*% R_within
#> dim_out <- c(rows = d * h * r, cols = n_groups * ncol(L) *
#> nrow(M))
#> names(dim_out) <- c("rows", "cols")
#> if (!is.null(B_array)) {
#> if (dim(B_array)[1] != ncol(L) || dim(B_array)[2] !=
#> nrow(M) || dim(B_array)[3] != n_groups) {
#> stop("semnova: B_array dimensions (", paste(dim(B_array),
#> collapse = " x "), ") are not compatible with L/M/G (expected ",
#> ncol(L), " x ", nrow(M), " x ", n_groups, ")",
#> call. = FALSE)
#> }
#> return(list(R = R, dim = dim_out, vec_B = as.vector(B_array)))
#> }
#> list(R = R, dim = dim_out)
#> }
#> <bytecode: 0x5d5de133c0c8>
#> <environment: namespace:semnova>Supplying B_array returns \(\text{vec}(\mathbf{B})\) alongside it, so
the two halves of the restriction are produced by one call and cannot
disagree about their ordering:
set.seed(1)
# 3 predictors, 4 repeated measures, 2 groups
B_array <- array(rnorm(3 * 4 * 2), dim = c(3, 4, 2))
L <- matrix(c(0, 1, 0), nrow = 1) # one predictor contrast
M <- cbind(c(1, -1, 0, 0), c(0, 0, 1, -1)) # two within-subject contrasts
G <- matrix(c(1, -1), nrow = 1) # one group contrast
out <- contrast_matrix(G = G, L = L, M = M, B_array = B_array)
out$dim
#> rows cols
#> 2 24These are the dimensions tabulated above: \(d \cdot h \cdot r = 2\) rows and \(n_{\text{groups}} \cdot (p + 1) \cdot q = 24\) columns.
The construction can be checked against its own definition. Computing \(\mathbf{G} \cdot (\mathbf{L} \mathbf{B}_g \mathbf{M})\) directly, group by group, must reproduce \(\mathbf{R} \cdot \text{vec}(\mathbf{B})\):
direct <- Reduce(`+`, lapply(1:2, function(g) {
G[1, g] * as.vector(L %*% B_array[, , g] %*% M)
}))
all.equal(as.vector(out$R %*% out$vec_B), direct)
#> [1] TRUEThis vignette is rendered when the package is built, so that equality is verified on every build rather than asserted here once.
| Part | Supplied by |
|---|---|
| \(\mathbf{G}\), \(\mathbf{L}\), \(\mathbf{M}\), \(\mathbf{K}\) | hypothesis(), or
generate_anova_hypotheses() for one hypothesis per model
term |
| \(\mathbf{B}\) | regression_coefficients(), whose B_array
holds the contrast layer’s coefficients |
| \(\mathbf{R}\) | contrast_matrix() |
| the test | test_hypothesis() and
contrast_estimates(), which form the Wald statistic from
\(\mathbf{R}\), \(\text{vec}(\mathbf{B})\) and the
coefficients’ covariance matrix |
The restriction \(\mathbf{R} \cdot
\text{vec}(\mathbf{B}) - \mathbf{k}\) is what those functions
test, and the same quantity underlies the Wald test reported in the
ANOVA table, the per-contrast estimates in summary(), and
the intervals drawn by plot().