When using linear mixed model (lmm) to fit some data, I usually use library(emmeans)
to test some specific interactions and simple effect analyses. library(emmeans)
is very handy to examine multiple effects of interests with one fitted lmm. Recently, my colleague (more importantly, my friend) Tobiasz introduced me applying priori contrasts in lmm, which could/should generate more accurate estimates of the interesting parameters.
In this blog, I will try to use priori contrasts in linear models with a simulated data set to test some specific hypothesis. (The methods used in this blog can be transferred to lmm easily.)
1 Preparation
library(tidyverse)
library(MASS)
Simulate a dataset:
# a 2*2 between-subjects design
N_subj <- 50 # each group
A_labels <- c("a1", "a2")
B_labels <- c("b1", "b2")
# true values (used for simulating data)
a1_b1 <- 30
a2_b1 <- 20
a1_b2 <- 25
a2_b2 <- 50
# sd for the residuals
e_sd <- 10
set.seed(20201111)
A <- rep(gl(n = length(A_labels), k = N_subj, labels = A_labels), times = 2)
B <- gl(n = length(B_labels), k = N_subj *2, labels = B_labels)
y_ <- rep(c(a1_b1, a2_b1, a1_b2, a2_b2), each = N_subj)
Y <- y_ + rnorm(length(y_), 0, e_sd)
(df <- tibble(A, B, Y))
## # A tibble: 200 x 3
## A B Y
## <fct> <fct> <dbl>
## 1 a1 b1 35.9
## 2 a1 b1 41.7
## 3 a1 b1 30.6
## 4 a1 b1 35.6
## 5 a1 b1 36.2
## 6 a1 b1 9.82
## 7 a1 b1 29.8
## 8 a1 b1 21.0
## 9 a1 b1 41.3
## 10 a1 b1 32.6
## # … with 190 more rows
2 Linear models with the default contrast (treatment contrast)
lm_default <- lm(Y ~ A * B, data = df)
summary(lm_default)
##
## Call:
## lm(formula = Y ~ A * B, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -25.003 -5.772 1.103 6.386 24.202
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 31.087 1.386 22.425 < 2e-16 ***
## Aa2 -11.797 1.960 -6.018 8.55e-09 ***
## Bb2 -7.713 1.960 -3.934 0.000116 ***
## Aa2:Bb2 37.535 2.773 13.538 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 9.802 on 196 degrees of freedom
## Multiple R-squared: 0.5813, Adjusted R-squared: 0.5749
## F-statistic: 90.72 on 3 and 196 DF, p-value: < 2.2e-16
With the default contrast, the true values of Intercept
and other coefficients are (compared with the fitted values):
coefficient | calculation | true value | estimate |
---|---|---|---|
Intercept |
a1_b1 |
30 | 31.09 |
Aa2 |
a2_b1 - a1_b1 |
-10 | -11.8 |
Bb2 |
a1_b2 - a1_b1 |
-5 | -7.71 |
Aa2:Bb2 |
(a2_b2 - a2_b1) - (a1_b2 - a1_b1) |
35 | 37.54 |
From the table above, we can see the fitted parameters of lm_default
match the values used for simulating data.
3 Linear model with custom contrasts
With default contrasts, we obtained the parameters estimating the mean of a1_b1
, the differences between a2_b1
and a1_b1
etc. What if we would like to test the differences between the two levels of A
under b1
and b2
separately (i.e., the simple effects)?
First, we need to create a new independent variable, which is the combinations of A
and B
, and write down the hypothesis matrix:
# level names
contrast_names <- c("a1_b1", "a2_b1", "a1_b2", "a2_b2")
# create the new independnet variable Custom
df <- df %>%
mutate(C_ = paste(A, B, sep="_"),
C_ = factor(C_, levels = contrast_names),
CH_ = C_)
hypothesis_matrix <- matrix(c( -1, 1, 0, 0, # simple effect 1: a2_b1 - a1_b1
0, 0, -1, 1, # simple effect 2: a2_b2 - a1_b2
-1/2, -1/2, 1/2, 1/2), nrow = 4) # the main effects of B*
fractions(hypothesis_matrix)
## [,1] [,2] [,3]
## [1,] -1 0 -1/2
## [2,] 1 0 -1/2
## [3,] 0 -1 1/2
## [4,] 0 1 1/2
*Why the third contrast is the main effects of B? The short answer is that then this model can explain as most of the variances as possible. For a long answer, please refer to Schad et al (2020, P26).
Second, we convert this hypothesis matrix to a contrast matrix, and assign it to the newly added independent variable C_
:
# apply the generalized matrix inverse
contrast_matrix <- fractions(t(ginv(hypothesis_matrix)))
rownames(contrast_matrix) <- levels(df$C_)
colnames(contrast_matrix) <- c("a2_b1-a1_b1", "a2_b2-a1_b2", "B2-1")
# apply the contrasts
(contrasts(df$C_) <- contrast_matrix)
## a2_b1-a1_b1 a2_b2-a1_b2 B2-1
## a1_b1 -1/2 0 -1/2
## a2_b1 1/2 0 -1/2
## a1_b2 0 -1/2 1/2
## a2_b2 0 1/2 1/2
Then we can fit the model with the addedly independent variable as usual:
# fit lm with the custom contrasts
lm_custom <- lm(Y ~ 1+ C_, data = df)
summary(lm_custom)
##
## Call:
## lm(formula = Y ~ 1 + C_, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -25.003 -5.772 1.103 6.386 24.202
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 30.7153 0.6931 44.314 < 2e-16 ***
## C_a2_b1-a1_b1 -11.7974 1.9605 -6.018 8.55e-09 ***
## C_a2_b2-a1_b2 25.7376 1.9605 13.128 < 2e-16 ***
## C_B2-1 11.0542 1.3863 7.974 1.25e-13 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 9.802 on 196 degrees of freedom
## Multiple R-squared: 0.5813, Adjusted R-squared: 0.5749
## F-statistic: 90.72 on 3 and 196 DF, p-value: < 2.2e-16
coefficient | calculation | true value | estimate |
---|---|---|---|
Intercept |
(a2_b2 + a2_b1 + a1_b2 + a1_b1)/4 |
31.25 | 30.72 |
C_a2_b1-a1_b1 |
a2_b1 - a1_b1 |
-10 | -11.8 |
C_a2_b2-a1_b2 |
a2_b2 - a1_b2 |
25 | 25.74 |
C_B1 |
(a1_b2 + a2_b2)/2 - (a1_b1 + a2_b1)/2 |
12.5 | 11.05 |
From the table above, we can see the fitted parameters of lm_custom
match the values used for simulating data. Specifically, the three “beta” parameters estimate the simple effects of A under B and the main effects of B.
4 Custom contrasts with library(hypr)
To create the above custom contrasts, we do not have to write down the hypothesis matrix and apply the generalized matrix inverse by ourselves. We can use library(hypr)
to obtain the contrasts directly from the hypothesis.
library(hypr)
is used to create the same contrasts as the previous section. First, we write down the hypothesis (but not the hypothesis matrix) and then we obtain the hypothesis matrix and contrast matrix directly, which are the same as the previous section.
library(hypr)
hypr_custom <- hypr(a2_b1-a1_b1 ~ 0, # simple effect 1: a2_b1 - a1_b1
a2_b2-a1_b2 ~ 0, # simple effect 2: a2_b2 - a1_b2
(a1_b1+a2_b1)/2-(a1_b2+a2_b2)/2 ~ 0, # the main effects of B
levels = contrast_names)
hypr_custom
## hypr object containing 3 null hypotheses:
## H0.1: 0 = a2_b1 - a1_b1
## H0.2: 0 = a2_b2 - a1_b2
## H0.3: 0 = 1/2*a1_b1 + 1/2*a2_b1 - 1/2*a1_b2 - 1/2*a2_b2
##
## Hypothesis matrix (transposed):
## [,1] [,2] [,3]
## a1_b1 -1 0 1/2
## a2_b1 1 0 1/2
## a1_b2 0 -1 -1/2
## a2_b2 0 1 -1/2
##
## Contrast matrix:
## [,1] [,2] [,3]
## a1_b1 -1/2 0 1/2
## a2_b1 1/2 0 1/2
## a1_b2 0 -1/2 -1/2
## a2_b2 0 1/2 -1/2
Next, we apply the contrast matrix to the newly added independent variable:
# CH_ was copied from C_ in last section
(contrasts(df$CH_) <- contr.hypothesis(hypr_custom))
## [,1] [,2] [,3]
## a1_b1 -0.5 0.0 0.5
## a2_b1 0.5 0.0 0.5
## a1_b2 0.0 -0.5 -0.5
## a2_b2 0.0 0.5 -0.5
## attr(,"class")
## [1] "hypr_cmat" "matrix" "array"
Then we fit the model:
lm_hypr <- lm(Y ~ 1+ CH_, data = df)
summary(lm_hypr)
##
## Call:
## lm(formula = Y ~ 1 + CH_, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -25.003 -5.772 1.103 6.386 24.202
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 30.7153 0.6931 44.314 < 2e-16 ***
## CH_1 -11.7974 1.9605 -6.018 8.55e-09 ***
## CH_2 25.7376 1.9605 13.128 < 2e-16 ***
## CH_3 -11.0542 1.3863 -7.974 1.25e-13 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 9.802 on 196 degrees of freedom
## Multiple R-squared: 0.5813, Adjusted R-squared: 0.5749
## F-statistic: 90.72 on 3 and 196 DF, p-value: < 2.2e-16
coefficient | calculation | true value | estimate |
---|---|---|---|
Intercept |
(a2_b2 + a2_b1 + a1_b2 + a1_b1)/4 |
31.25 | 30.72 |
CH_1 |
a2_b1 - a1_b1 |
-10 | -11.8 |
CH_2 |
a2_b2 - a1_b2 |
25 | 25.74 |
CH_3 |
(a1_b1 + a2_b1)/2 - (a1_b2 + a2_b2)/2 |
-12.5 | -11.05 |
As in the last two sections, the fitted values match the values used for simulating data.
Someone may notice that there is discrepancy between the results of lm_hypr
and lm_custom
. The main effects of B
in lm_hypr
and lm_custom
have opposite signs, though their absolute values are similar. This is because in lm_custom
, the main effect of B
(i.e., the third contrast) is defined as b2 - b1
, while in lm_hypr
the same effect is defined as b1 - b2
.
5 Simple effect analysss with R code (nested effects)
In the previous two sections, we applied custom contrasts to test the simple effects of A on the two levels of B separately. Instead of using custom contrasts, we can use R code in linear models to test these effects direclty:
lm_R <- lm(Y ~ B/A, df,
contrasts = list(A = contr.sdif(2),
B = contr.sdif(2)))
summary(lm_R)
##
## Call:
## lm(formula = Y ~ B/A, data = df, contrasts = list(A = contr.sdif(2),
## B = contr.sdif(2)))
##
## Residuals:
## Min 1Q Median 3Q Max
## -25.003 -5.772 1.103 6.386 24.202
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 30.7153 0.6931 44.314 < 2e-16 ***
## B2-1 11.0542 1.3863 7.974 1.25e-13 ***
## Bb1:A2-1 -11.7974 1.9605 -6.018 8.55e-09 ***
## Bb2:A2-1 25.7376 1.9605 13.128 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 9.802 on 196 degrees of freedom
## Multiple R-squared: 0.5813, Adjusted R-squared: 0.5749
## F-statistic: 90.72 on 3 and 196 DF, p-value: < 2.2e-16
coefficient | calculation | true value | estimate |
---|---|---|---|
Intercept |
(a2_b2 + a2_b1 + a1_b2 + a1_b1)/4 |
31.25 | 30.72 |
B2-1 |
(a1_b2 + a2_b2)/2 - (a1_b1 + a2_b1)/2 |
12.5 | 11.05 |
Bb1:A2-1 |
a2_b1 - a1_b1 |
-10 | -11.8 |
Bb2:A2-1 |
a2_b2 - a1_b2 |
25 | 25.74 |
As always, the fitted value match the values used for simulating data.
6 Summary
This post is kind of a super short summary of Schad et al (2020). For anyone who is interested in using linear model (or lmm), I highly recommend reading this paper. Probably applying priori contrasts in linear models (or lmm) is not that straightforward (at least at the beginning), even it may scare some users off. I do think it is a more beneficial approach, which at least (should) generates more accurate estimations.
References
Schad, D. J., Vasishth, S., Hohenstein, S., & Kliegl, R. (2020). How to capitalize on a priori contrasts in linear (mixed) models: A tutorial. Journal of Memory and Language, 110, 104038. https://doi.org/10.1016/j.jml.2019.104038