Contrasts in Multilevel/Mixed Models with an Interaction

We take off from where we left, and add an interaction term to the mixed model, to run customised linear contrasts. As usual, we are using the sleepstudy dataset:

{r}
# install.packages("lmerTest")
# install.packages("knitr")
# install.packages("multcomp")
# install.packages("dplyr")
# install.packages("ggplot2")
data("sleepstudy")
# Load packages
packs <- c("lmerTest", "knitr", "multcomp", "dplyr", "ggplot2")
lapply(packs, require, character.only = TRUE) 
kable(head(sleepstudy))
ReactionDaysSubject
249.56000308
258.70471308
250.80062308
321.43983308
356.85194308
414.69015308

We are generating an additional factor (i.e., a categorical variable), lunch, with levels 0 or 1, recording whether the participants had lunch or not before the experiment:

set.seed(88)
lunch <- sample(c(0,1), replace=TRUE, size=18)
sleepstudy$lunch <- factor(lunch)
kable(head(sleepstudy))
ReactionDaysSubjectlunch
249.560003080
258.704713080
250.800623081
321.439833080
356.851943080
414.690153081

At this point, we have 2 levels of variation, days of sleep deprivation and whether the participant had a full belly or not. We are going to investigate whether having lunch or not change the effect of Days on Reaction, by adding an interaction between the factors.

Before fitting the model, we are also changing the format of Days to factor:

# Change the data type of Days from number (continous) to factor (categorical)
sleepstudy$Days <- factor(as.character(sleepstudy$Days), 
                          levels = unique(sleepstudy$Days))
levels(sleepstudy$Days)
# Fit the model
m <- lmer(Reaction ~ Days*lunch - 1 + ( 1 | Subject), data=sleepstudy)
kable(coef(summary(m)))
EstimateStd. Errordft valuePr(>|t|)
Days0255.12438712.3844247.1920920.60043420.0000000
Days1256.15683914.5064176.8382017.65818130.0000000
Days2274.50590512.3847447.1946622.16484700.0000000
Days3286.58371914.5064076.8381319.75568110.0000000
Days4290.83809912.3830947.1791723.48670670.0000000
Days5298.80886014.5036576.8156520.60232740.0000000
Days6305.54958212.3817147.1659024.67749260.0000000
Days7316.43706114.5177976.9415821.79650640.0000000
Days8343.41778112.3815547.1646327.73624180.0000000
Days9359.81959014.5172376.9379124.78569470.0000000
lunch16.87338218.50998145.244460.37133390.7109299
Days1:lunch18.13666824.14985145.167270.33692420.7366607
Days2:lunch1-48.02140326.62930146.00451-1.80332970.0733985
Days3:lunch1-13.33845624.22097145.41194-0.55069860.5826854
Days4:lunch1-16.72242525.65177144.37249-0.65190130.5155013
Days5:lunch110.60389124.86291146.346620.42649430.6703744
Days6:lunch122.95564926.55690145.780600.86439490.3887911
Days7:lunch1-2.70904223.49512144.08581-0.11530230.9083660
Days8:lunch1-37.42062026.49472145.59599-1.41238020.1599719
Days9:lunch1-23.01644424.27124145.58290-0.94830130.3445476

The above output shows the main effects of Days and lunch and their interactions. We should interpret the main effect of Days without lunch (lunch = 0). In addition to the main effect of lunch, each day will receive an additional coefficient given by the interaction.

The output shows that the main effect of lunch and the interactions are not significant. But we are interested in the differences between all the days and the first day of the experiment, Day 1, so we want to compare all the other days to Day 1. We need again to build a contrasts matrix.

Building the Contrasts Matrix

First, we need to compute all the combinations of the levels of Days and lunch:

group <- paste0(sleepstudy$Days, sleepstudy$lunch)
group <- aggregate(model.matrix(m) ~ group, FUN=mean)
# group
rownames(group) <- group$group
(group <- group[,-1])
kable(head(group)) #displaying only the first 6 rows
Days0Days1Days2Days3Days4Days5Days6Days7Days8Days9lunch1Days1:lunch1Days2:lunch1Days3:lunch1Days4:lunch1Days5:lunch1Days6:lunch1Days7:lunch1Days8:lunch1Days9:lunch1
0010000000000000000000
0110000000001000000000
1001000000000000000000
1101000000001100000000
2000100000000000000000
2100100000001010000000

The first column specifies the interaction between Days and lunch for each row: we have Day 0 with lunch 0 and 1 (00 and 01), day 1 with lunch 0 and 1 (10 and 11), and so on. The other columns contains zeros and ones specifying the factors combination (1s mark the active level - Days 0 and lunch 1, on row 2) in the extended model matrix.

Now we can specify which comparisons we want to compute (all days vs Day 1):

contrasts <- rbind(group["11",] - group["01",],
                   group["11",] - group["21",],
                   group["11",] - group["31",],
                   group["11",] - group["41",],
                   group["11",] - group["51",],
                   group["11",] - group["61",],
                   group["11",] - group["71",],
                   group["11",] - group["81",],
                   group["11",] - group["91",])
kable(head(contrasts))
Days0Days1Days2Days3Days4Days5Days6Days7Days8Days9lunch1Days1:lunch1Days2:lunch1Days3:lunch1Days4:lunch1Days5:lunch1Days6:lunch1Days7:lunch1Days8:lunch1Days9:lunch1
11-11000000000100000000
11101-1000000001-10000000
112010-1000000010-1000000
1130100-1000000100-100000
11401000-1000001000-10000
115010000-1000010000-1000

Now the matrix specifies couples of contrasts that we want to estimate from the model.

Extracting the contrasts from the model

For extracting the contrasts from the model, we are going to use the glht function from the multcomp package.

# Transform into Matrix
contrast.matrix <- rbind("Lunch Day 0 vs Day 1"=as.numeric(contrasts[1,]),
                         "Lunch Day 2 vs Day 1"=as.numeric(contrasts[3,]),
                         "Lunch Day 3 vs Day 1"=as.numeric(contrasts[4,]),
                         "Lunch Day 4 vs Day 1"=as.numeric(contrasts[5,]),
                         "Lunch Day 5 vs Day 1"=as.numeric(contrasts[6,]),
                         "Lunch Day 6 vs Day 1"=as.numeric(contrasts[7,]),
                         "Lunch Day 7 vs Day 1"=as.numeric(contrasts[8,]),
                         "Lunch Day 8 vs Day 1"=as.numeric(contrasts[8,]),
                         "Lunch Day 9 vs Day 1"=as.numeric(contrasts[8,])
                         )

comparisons <- summary(glht(m, contrast.matrix))

comparisons

From this output, we can extract that the effect of Lunch is critical on Days 4-6, compared to Day 1.

Let’s format the results of the contrasts test into a table:

pq <- comparisons$test

mtests <- data.frame(pq$coefficients, pq$sigma, pq$tstat, pq$pvalues)
error <- attr(pq$pvalues, "error")
pname <- switch(comparisons$alternativ, 
                less = paste("Pr(<", 
                             ifelse(comparisons$df ==0, "z", "t"), ")", 
                             sep = ""), 
                 greater = paste("Pr(>", 
                                 ifelse(comparisons$df == 0, "z", "t"), ")", 
                                 sep = ""), 
                 two.sided = paste("Pr(>|", 
                                   ifelse(comparisons$df == 0, "z", "t"), "|)"
                                   , sep = ""))                                   
colnames(mtests) <- c("Estimate", "Std.Error", 
                      ifelse(comparisons$df ==0, "zvalue", "tvalue"), pname)

mtests <- mtests %>%
  tibble::rownames_to_column("Comparison") %>%
  mutate(`Pr(>|z|)`=ifelse(`Pr(>|z|)`< 0.001, 
                           "< 0.001", 
                           ifelse(`Pr(>|z|)` < 0.01, 
                                  "< 0.01",
                                  ifelse(`Pr(>|z|)` < 0.05, 
                                         "< 0.05",
                                         paste(round(`Pr(>|z|)`,
                                                     4)))))) %>%
  mutate_if(is.numeric, funs(round(.,
                                   digits=2)))
kable(mtests)
ComparisonEstimateStd.ErrorzvaluePr(>|z|)
Lunch Day 0 vs Day 19.1719.000.480.998
Lunch Day 2 vs Day 1-8.9514.31-0.630.9906
Lunch Day 3 vs Day 1-9.8219.01-0.520.997
Lunch Day 4 vs Day 1-45.1214.29-3.16< 0.05
Lunch Day 5 vs Day 1-64.2118.71-3.43< 0.01
Lunch Day 6 vs Day 1-49.4313.97-3.54< 0.01
Lunch Day 7 vs Day 1-41.7019.02-2.190.1578
Lunch Day 8 vs Day 1-41.7019.02-2.190.1578
Lunch Day 9 vs Day 1-41.7019.02-2.190.1578

This is one way of investigating interactions with mixed models. Remember that, the more comparisons one does, the more there is the chance of incurring in Type I error, and there is the need of correcting the p-values (that the function glht does automatically).

Teresa Del Bianco
Teresa Del Bianco
Postdoctoral Researcher

Scientist researching brain and child development and neurodiversity.

Related