Chapter 13 Mixed Models

13.1 To-Do

Look at the presentation (Google Slides) I made for Keith’s lab (the one Ashley asked me to make). It has a ton of information I can use here.

13.2 Preamble

I made mixed models its own section because it is not your typical analyses. I would highly recommend that any student starts with more traditional analyses such as ANOVA or linear regression before taking on mixed model analyses.

In order to do pipe friendly analyses broomExtra is a nice package to run at first. You can check out the GitHub repo to learn more about the functions (which also work with other statistical models beyond LMMs).

For a theoretical background I would recommend this chapter from the book “Just enough R”.

13.3 lmerControl

Within lme4 you can specify optimizers. The default is bobyqa the other common option is nloptwrap.

To learn more on optimizer checks see here

13.4 Code

# to speed up computation, let's use only 50% of the data
set.seed(123)

# linear model (model summaries across grouping combinations)
broomExtra::grouped_glance(
  data = dplyr::sample_frac(tbl = ggplot2::diamonds, size = 0.5),
  grouping.vars = c(cut, color),
  formula = price ~ carat - 1,
  ..f = stats::lm,
  na.action = na.omit
)

# linear mixed effects model (model summaries across grouping combinations)
broomExtra::grouped_glance(
  data = dplyr::sample_frac(tbl = ggplot2::diamonds, size = 0.5),
  grouping.vars = cut,
  ..f = lme4::lmer,4
  formula = price ~ carat + (carat | color) - 1,
  control = lme4::lmerControl(optimizer = "bobyqa")
  
  
  fm1 <- lmer(Reaction ~ Days + (Days | Subject), sleepstudy)

# df.residual in `glance`
broom.mixed::tidy(fm1)

broom.mixed::glance(fm1)

# fetch the p-values
parameters::model_parameters(fm1) %>%
  broomExtra::easystats_to_tidy_names()
)


# p-values across groups in mixed-model
iris %>%
  group_by(Species) %>%
  group_modify(., ~ broomExtra::easystats_to_tidy_names(as.data.frame(parameters::model_parameters(
    lme4::lmer(Sepal.Length ~ Petal.Length + Petal.Width + Petal.Length * Petal.Width + (1 | Sepal.Width),
               data = .x,
               control = lme4::lmerControl(optimizer = "bobyqa")
    )
  ))))

easystats_to_tidy_names has been retired from everything I see online. Below is the only code I could find to replace it. It won’t extract p-values. Might need to check if its now part of ipmisc instead of broomExtra. I don’t think its a required argument anymore. You can see the original issue on GitHub here.

iris %>%
  group_by(Species) %>%
  group_modify(., ~ as.data.frame(parameters::model_parameters(
    lme4::lmer(Sepal.Length ~ Petal.Length + Petal.Width + Petal.Length * Petal.Width + (1 | Sepal.Width),
               data = .x,
               control = lme4::lmerControl(optimizer = "bobyqa")
    )
  )))
  # linear mixed effects model
  broomExtra::grouped_tidy(
    data = dplyr::mutate(MASS::Aids2, interval = death - diag),
    grouping.vars = sex,
    ..f = lme4::lmer,
    formula = interval ~ age + (1 | status),
    control = lme4::lmerControl(optimizer = "bobyqa"),
    tidy.args = list(conf.int = TRUE, conf.level = 0.99)
  )

Here is code I used from a recent project

tmp <- clear.labels(df.nirs) %>%
  group_by(metric) %>%
  group_modify(., ~ broomExtra::easystats_to_tidy_names(as.data.frame(parameters::model_parameters(
    lme4::lmer(value ~ Connection*Task + group*Connection + group*Task + group*Connection*Task + (1|Subject),
               data = .x,
               control = lme4::lmerControl(optimizer = "bobyqa")
    )
  )))) %>%
  filter(term != "(Intercept)") %>% # remove intercept from report
  mutate(term =  str_replace_all(term, ":", "×") %>% 
           str_replace_all("Connection", "") %>%
           str_replace_all("Task", "") %>%
           str_replace_all("group.L", "Group") %>%
           str_replace_all("->", "→")
  ) %>%
  janitor::clean_names("upper_camel") %>%
  dplyr::rename(
    "p" = "PValue",
    "dferror" = "DfError"
  ) %>%
  filter((str_detect(Term, "Group")) & p < .1) %>% # Give me only the results that show group differences, set p @.1
  mutate_if(is.numeric, round, 5) # round to 5 digits (makes it easier to read)

View(tmp)

To visualize your mixed model you can use sjPlot as shown below or here

pacman::p_load(sjPlot, sjmisc, ggplot2)
data(efc)
theme_set(theme_sjplot())

# make categorical
efc$c161sex <- to_factor(efc$c161sex)

# fit model with interaction
fit <- lm(neg_c_7 ~ c12hour + barthtot * c161sex, data = efc)

plot_model(fit, type = "pred", terms = c("barthtot", "c161sex"))



plot_model(fit, type = "int")

plot_model(fit, type = "pred", terms = c("c161sex", "barthtot [0, 100]"))

fit <- lm(neg_c_7 ~ c12hour + c161sex * barthtot, data = efc)
plot_model(fit, type = "int")

plot_model(fit, type = "int", mdrt.values = "meansd")


# fit model with 3-way-interaction
fit <- lm(neg_c_7 ~ c12hour * barthtot * c161sex, data = efc)

# select only levels 30, 50 and 70 from continuous variable Barthel-Index
plot_model(fit, type = "pred", terms = c("c12hour", "barthtot [30,50,70]", "c161sex"))

plot_model(fit, type = "int")

13.5 Post-hoc in mixed models

This can be a bit tricky because you want to include your random effect.

You could use

  1. emmeans::emmeans()
  2. lmerTest::difflsmeans()
  3. multcomp::glht()

I would look at this StackOverflow post which describes some differences between each approach

This is a good breakdown.

13.5.1 Examples

# using glht package
summary(glht(YOUR MODEL, linfct=mcp(YOUR FIXED FACTOR="Tukey")))

13.6 Running your models

For starters I am a big fan of using something that allows me to run through several models to get an overall feel for the data. This usually involves either broom or rstatix package which allows for pipe friendly modelling. I will commonly do this in order to evaluate if random factors should be included in my final model. If you are running mixed models you can use broomExtra or broom.mixed. I would recommend broomExtra which depends on broom.mixed (so its hits 2 birds with one stone).

# Question: Is there a subject effect in our data?
# if there is then we introduce it as a random effect.

tbl.SubjectEffect <- df.nirs %>% 
  group_by(metric) %>% 
  do(tidy(lm(value ~ Subject, .))) # requires broom

tbl.SubjectEffect <- df.nirs %>% 
  group_by(metric) %>% 
  rstatix::anova_test(value~Subject)
# answer is yes. There is a subject effect that we must accomodate

For a very complete guide on running ANOVAs in R for your first time. This post is immaculate.

You can see some exercises here

13.7 Modeling your data

To test the normality of your data you can use a few different methods

  1. Plot your data
  2. Check skewness and kurtosis
  3. Shapiro test.

In order to visualize your linear model I use a few packages

  1. sjPlot and sjmisc
  2. jtools
  3. interactions
  4. ggeffects (used less)
  5. To visualize lmer mixed models see here
  6. To follow up a mixed-model here
# Load typical packages
pacman::p_load(sjPlot, sjmisc, ggplot2, lme4)

model <- lm(mpg ~ wt + cyl + gear + disp, data = mtcars)
r <- report::report(model)
table_short(r)

performance::check_model(Model) # will make some plots to check your model 

# Individually checking model. 
# Note: I will do this if I see something problematic after running `check_model`

parameters::model_parameters(model)

performance::check_collinearity(model)
performance::plot(check_collinearity(model))

I am still working on code that will do this when I run my mixed models in a loop. But the GitHub page should help

# Load typical packages
pacman::p_load(sjPlot, sjmisc, ggplot2, lme4)

model <- lm(mpg ~ wt + cyl + gear + disp, data = mtcars)
r <- report::report(model)
table_short(r)

performance::check_model(Model) # will make some plots to check your model 

# Individually checking model. 
# Note: I will do this if I see something problematic after running `check_model`

parameters::model_parameters(model)

performance::check_collinearity(model)
performance::plot(check_collinearity(model))

13.8 Looking at interactions

src: https://cran.r-project.org/web/packages/emmeans/vignettes/interactions.html

13.8.1 Packages

mertool is a gui for modelling –> https://www.jaredknowles.com/journal/2015/8/12/announcing-mertools tidymodels: https://www.tidymodels.org/learn/statistics/tidy-analysis/

To assess model performance I use the performance package. You can visualize your model using this vignette