20 Mixed Models with R

by Michael Clark

load("data/gpa.RData")
gpa <- gpa %>% as_tibble()

20.1 Standard regregression model

\[ gpa = b_{intercept} + b_{occ} \times occasion + \epsilon \]

Coefficients \(b\) for intercept and effect of time.

The error \(\epsilon\) is assumed to be normally distributed with \(\mu = 0\) and some standard deviation \(\sigma\).

\[ \epsilon \sim \mathscr{N}(0, \sigma) \]

alternate notation, with emphasis on the data generating process:

\[ gpa ~ \sim \mathscr{N}(\mu, \sigma)\\ \mu = b_{intercept} + b_{occ} \times occasion \]

20.2 mixed nodel

20.2.1 student specific effect (initial depiction)

\[ gpa = b_{intercept} + b_{occ} \times occasion + ( \textit{effect}_{student} + \epsilon )\\ \textit{effect}_{student} \sim \mathscr{N}(0, \tau) \]

focusing on the coefficients (rather than on sources of error):

\[ gpa = ( b_{intercept} + \textit{effect}_{student} ) + b_{occ} \times occasion + \epsilon \]

or (shorter)

\[ gpa = b_{int\_student} + b_{occ} \times occasion + \epsilon \]

\(\rightarrow\) this means student specific intercepts…

\[ b_{int\_student} \sim \mathscr{N}(b_{intercept}, \tau) \]

…that are normally distributed with the mean of the overall intercept (random intercepts model)

20.2.2 as multi-level model

two-part regression model (one at observation level, one at student level) (this is the same as above, just needs ‘plugging in’)

\[ gpa = b_{int\_student} + b_{occ} \times occasion + \epsilon\\ b_{int\_student} = b_{intercept} + \textit{effect}_{student} \]

! There is no student-specific effect for \(occasion\) (which is termed fixed effect), and there is no random component

gpa_lm <- lm(gpa ~ occasion, data = gpa)

gpa %>% 
  ggplot(aes(x = year - 1  + as.numeric(semester)/2, y = gpa, group = student)) +
  geom_line(alpha = .2) +
  geom_abline(slope = gpa_lm$coefficients[[2]],
              intercept =  gpa_lm$coefficients[[1]],
              color = clr2, size = 1) +
  labs(x = "semester") +
  coord_cartesian(ylim = c(1,4), expand = 0)

pander::pander(summary(gpa_lm), round = 3)
  Estimate Std. Error t value Pr(>|t|)
(Intercept) 2.599 0.018 145.7 0
occasion 0.106 0.006 18.04 0
Fitting linear model: gpa ~ occasion
Observations Residual Std. Error \(R^2\) Adjusted \(R^2\)
1200 0.3487 0.2136 0.2129

Student effect not taken into account.

20.2.3 Mixed Model

gpa_mixed <- lmer(gpa ~ occasion + (1 | student), data = gpa)

(Test automatic equation creation)

library(equatiomatic)
# Give the results to extract_eq
extract_eq(gpa_mixed,)

\[ \begin{aligned} \operatorname{gpa}_{i} &\sim N \left(\alpha_{j[i]} + \beta_{1}(\operatorname{occasion}), \sigma^2 \right) \\ \alpha_{j} &\sim N \left(\mu_{\alpha_{j}}, \sigma^2_{\alpha_{j}} \right) \text{, for student j = 1,} \dots \text{,J} \end{aligned} \]

term value se t p_value lower_2.5 upper_97.5
Intercept 2.599 0.022 119.8 0 2.557 2.642
occasion 0.106 0.004 26.1 0 0.098 0.114
group effect variance sd var_prop
student Intercept 0.064 0.252 0.523
Residual 0.058 0.241 0.477

Coefficients (fixed effects) for time and intercept are the same as lm()

Getting confidence intervals from a mixed model (since \(p\) values are not given (== 0 ?))

confint(gpa_mixed)
#>                  2.5 %    97.5 %
#> .sig01      0.22517423 0.2824604
#> .sigma      0.23071113 0.2518510
#> (Intercept) 2.55665145 2.6417771
#> occasion    0.09832589 0.1143027
mm_cinf <- mixedup::extract_vc(gpa_mixed)
mm_cinf %>% pander::pander()
Table continues below
  group effect variance sd sd_2.5
sd_(Intercept)|student student Intercept 0.064 0.252 0.225
sigma Residual 0.058 0.241 0.231
  sd_97.5 var_prop
sd_(Intercept)|student 0.282 0.523
sigma 0.252 0.477

student effect \(\tau\) = 0.252 / 0.064 (sd / var)

Percentage of student variation as share of the total variation (intraclass correlation): 0.064 / 0.122 = 0.5245902

20.2.4 Estimation of random effects

Random effect

mixedup::extract_random_effects(gpa_mixed) %>%  head(5) %>% knitr::kable()
group_var effect group value se lower_2.5 upper_97.5
student Intercept 1 -0.071 0.092 -0.251 0.109
student Intercept 2 -0.216 0.092 -0.395 -0.036
student Intercept 3 0.088 0.092 -0.091 0.268
student Intercept 4 -0.187 0.092 -0.366 -0.007
student Intercept 5 0.030 0.092 -0.149 0.210

Random intercept (intercept + random effect)

mm_coefs <- mixedup::extract_coef(gpa_mixed)
mm_coefs %>%  head(5) %>% knitr::kable()
group_var effect group value se lower_2.5 upper_97.5
student Intercept 1 2.528 0.095 2.343 2.713
student Intercept 2 2.383 0.095 2.198 2.568
student Intercept 3 2.687 0.095 2.502 2.872
student Intercept 4 2.412 0.095 2.227 2.597
student Intercept 5 2.629 0.095 2.444 2.814
library(merTools)
mm_intervals <- predictInterval(gpa_mixed) %>% as_tibble()
mm_mean_sd <- REsim(gpa_mixed) %>% as_tibble()

sd_level <- .95

mm_mean_sd %>%
  mutate(sd_leveled = sd * qnorm(1 - ((1 - sd_level)/2)),
         sig = (median + sd_leveled) >= 0 & (median - sd_leveled) <= 0) %>% 
  arrange(median) %>% 
  mutate(rank = row_number()) %>% 
  arrange(groupID) %>% 
  ggplot(aes(x = rank)) +
  geom_segment(aes(xend = rank, y = median - sd_leveled, yend = median +  sd_leveled, color = sig)) +
  geom_point(aes(y = median), size = .3, alpha = .5) +
  geom_hline(yintercept = 0, size = .4, linetype = 3) +
  scale_color_manual(values = c(`TRUE` = clr2, `FALSE` = clr0d), guide = "none") +
  labs(x = "Student", y = "Coefficient", caption = "interval estimates of random effects")

20.2.5 Prediction

gpa_predictions <- tibble(lm = predict(gpa_lm),
                            lmm_no_random_effects = predict(gpa_mixed, re.form = NA),
                            lmm_with_random_effects = predict(gpa_mixed)) %>% 
  bind_cols(gpa, .)

gpa_predictions %>% 
  ggplot(aes(x = lm)) +
  geom_point(aes(y = lmm_with_random_effects, color = "with_re")) +
  geom_point(aes(y = lmm_no_random_effects, color = "no_re")) +
  scale_color_manual(values = c(no_re = clr2, with_re = clr0d)) 

student_select <- 1:2
gpa_predictions %>%
  filter(student %in% student_select) %>% 
  ggplot(aes(x = occasion)) +
  geom_point(aes(y = gpa, color = student))+
  geom_abline(data = tibble(slope = c(gpa_lm$coefficients[[2]], mm_fixed$value[c(2,2)]), 
                             intercept =  c(gpa_lm$coefficients[[1]], mm_coefs$value[as.numeric(as.character(mm_coefs$group)) %in% student_select]),
                             model = c("lm", as.character(mm_coefs$group[as.numeric(as.character(mm_coefs$group)) %in% student_select]))),
              aes(slope = slope, intercept = intercept, color = model), size = .6) +
  scale_color_manual(values = c(lm = clr2, `1` = clr1, `2` = clr0d), 
                     labels = c( "lm", "lmm (student1)", "lmm (student2)"))

20.2.6 Cluster Level Covariates

If a cluster level covariate is added (eg. sex), \(b_{int\_student}\) turns into:

\[ b_{int\_student} = b_{intercept} + b_{sex} \times \textit{sex} + \textit{effect}_{student} \]

plugging this into the model will result in

\[ gpa = b_{intercept} + b_{occ} \times \textit{occasion} + b_{sex} \times \textit{sex} + ( \textit{effect}_{student} + \epsilon) \]

20.3 Add random slope

gpa_mixed2 <- lmer(gpa ~ occasion + (1 + occasion | student), data = gpa)

mixedup::extract_fixed_effects(gpa_mixed2) %>% knitr::kable()
term value se t p_value lower_2.5 upper_97.5
Intercept 2.599 0.018 141.592 0 2.563 2.635
occasion 0.106 0.006 18.066 0 0.095 0.118
mixedup::extract_vc(gpa_mixed2, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
student Intercept 0.045 0.213 0.491
student occasion 0.005 0.067 0.049
Residual 0.042 0.206 0.460
gpa_mixed2_rc <- mixedup::extract_random_coefs(gpa_mixed2)

correlation of the intercepts and slopes (negative, so students with a low starting score tend to increase a little more)

VarCorr(gpa_mixed2) %>% as_tibble() %>% knitr::kable() 
grp var1 var2 vcov sdcor
student (Intercept) NA 0.0451934 0.2125875
student occasion NA 0.0045039 0.0671114
student (Intercept) occasion -0.0014016 -0.0982391
Residual NA NA 0.0423879 0.2058832
gpa_lm_separate <- gpa %>% 
  group_by(student) %>% 
  nest() %>% 
  mutate(mod = map(data,function(data){lm(gpa ~ occasion, data = data)})) %>% 
  bind_cols(., summarise_model(.)) 
  
p_intercepts <- ggplot()  +
  geom_density(data = gpa_mixed2_rc %>% filter(effect == "Intercept"),
               aes(x = value, color = "mixed", fill = after_scale(clr_alpha(color)))) +
  geom_density(data = gpa_lm_separate,
               aes(x = intercept, color = "separate", fill = after_scale(clr_alpha(color)))) +
  labs(x = "intercept") +
  xlim(1.5, 4)

p_slopes <- ggplot()  +
  geom_density(data = gpa_mixed2_rc %>% filter(effect == "occasion"),
               aes(x = value, color = "mixed", fill = after_scale(clr_alpha(color)))) +
  geom_density(data = gpa_lm_separate, 
               aes(x = slope, color = "separate", fill = after_scale(clr_alpha(color)))) +
  labs(x = "slope")  +
  xlim(-.2, .4) 

p_intercepts + p_slopes +
  plot_layout(guides = "collect") &
  scale_color_manual("model", values = c(separate = clr0d, mixed = clr2)) &
  theme(legend.position = "bottom")

\(\rightarrow\) mixed model intercepts and slopes are less extreme

In both cases the mixed model shrinks what would have been the by-group estimate, which would otherwise overfit in this scenario. This regularizing effect is yet another bonus when using mixed models.

gpa_predictions <- tibble(lmm_with_random_slope = predict(gpa_mixed2)) %>% 
  bind_cols(gpa_predictions, .)

gpa_mixed2_rc_wide <- gpa_mixed2_rc %>% 
  dplyr::select(group_var, group, effect, value) %>% 
  pivot_wider(names_from = effect, values_from = value)

student_select <- 1:2
p_two_students <- gpa_predictions %>%
  filter(student %in% student_select) %>% 
  ggplot(aes(x = occasion)) +
  geom_point(aes(y = gpa, color = student))+
  geom_abline(data = tibble(slope = c(gpa_lm$coefficients[[2]], 
                                            gpa_mixed2_rc_wide$occasion[as.numeric(as.character(gpa_mixed2_rc_wide$group)) %in% student_select]), 
                             intercept =  c(gpa_lm$coefficients[[1]], 
                                            gpa_mixed2_rc_wide$Intercept[as.numeric(as.character(gpa_mixed2_rc_wide$group)) %in% student_select]),
                             model = c("lm", as.character(gpa_mixed2_rc_wide$group[as.numeric(as.character(gpa_mixed2_rc_wide$group)) %in% student_select]))),
              aes(slope = slope, intercept = intercept, color = model), size = .6) +
  scale_color_manual(values = c(lm = clr2, `1` = clr1, `2` = clr0d), 
                     labels = c( "lm", "lmm (student1)", "lmm (student2)"))

p_all_mod <- ggplot(data = gpa_predictions, aes(x = occasion, y = gpa)) +
  geom_abline(data = tibble(slope = c(gpa_mixed2_rc_wide$occasion, gpa_lm$coefficients[[2]]),
                            intercept = c(gpa_mixed2_rc_wide$Intercept, gpa_lm$coefficients[[1]]),
                            modeltype = c(rep("lmm (random slope)", length(gpa_mixed2_rc_wide$Intercept)), "lm")),
              aes(slope = slope, intercept = intercept, color = modeltype), size = .6) +
  scale_color_manual(values = c(`lmm (random slope)` = clr_alpha(clr0d ,.6), lm = clr2))

p_two_students + p_all_mod +
  plot_layout(guides = "collect") &
  xlim(0,5) & ylim(2.2, 4) &
  theme(legend.position = "bottom")

20.4 Cross Classified models

Setups where data are grouped by several factors but these are not nested (all participants get to see all images). These are crossed random effects.

load("data/pupils.RData")

pupils %>%  head() %>% knitr::kable()
PUPIL primary_school_id secondary_school_id achievement sex ses primary_denominational secondary_denominational
1 1 2 6.6 female highest no no
2 1 1 5.7 male lowest no yes
3 1 17 4.5 male 2 no no
4 1 3 4.4 male 2 no no
5 1 4 5.8 male 3 no yes
6 1 4 5.0 female 4 no yes
pupils_crossed <- lmer(
  achievement ~ sex + ses +
    ( 1 | primary_school_id ) + ( 1 | secondary_school_id ),
  data = pupils
)

mixedup::extract_fixed_effects(pupils_crossed) %>% knitr::kable()
term value se t p_value lower_2.5 upper_97.5
Intercept 5.924 0.123 48.303 0.000 5.684 6.164
sexfemale 0.261 0.046 5.716 0.000 0.171 0.350
ses2 0.132 0.118 1.122 0.262 -0.098 0.362
ses3 0.098 0.110 0.890 0.373 -0.118 0.314
ses4 0.298 0.105 2.851 0.004 0.093 0.503
ses5 0.354 0.101 3.514 0.000 0.156 0.551
seshighest 0.616 0.110 5.602 0.000 0.401 0.832
mixedup::extract_vc(pupils_crossed, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
primary_school_id Intercept 0.173 0.416 0.243
secondary_school_id Intercept 0.066 0.257 0.093
Residual 0.473 0.688 0.664
pupils_varicance_components_random_effects <- REsim(pupils_crossed) %>% as_tibble()

pupils_varicance_components_random_effects %>%
 mutate(sd_leveled = sd * qnorm(1 - ((1 - sd_level)/2)),
         sig = (median + sd_leveled) >= 0 & (median - sd_leveled) <= 0) %>% 
  group_by(groupFctr) %>% 
  arrange(groupFctr, median) %>% 
  mutate(rank = row_number()) %>% 
  arrange(groupFctr, groupID) %>% 
  ungroup() %>% 
  ggplot(aes(x = rank)) +
  geom_segment(aes(xend = rank, y = median - sd_leveled, yend = median +  sd_leveled, color = sig)) +
  geom_point(aes(y = median), size = .3, alpha = .5) +
  geom_hline(yintercept = 0, size = .4, linetype = 3) +
  facet_wrap(groupFctr ~ ., scales = "free_x") +
  scale_color_manual(values = c(`TRUE` = clr2, `FALSE` = clr0d), guide = "none") +
  labs(x = "Group", y = "Effect Range", caption = "interval estimates of random effects")

Note that we have the usual extensions here if desired. As an example, we could also do random slopes for student level characteristics.

20.5 Hierachical structure

These are setups, where different grouping factors are nested within each other (eg. cities, counties, states).

load("data/nurses.RData")
nurses %>% head() %>% knitr::kable()
hospital ward wardid nurse age sex experience stress wardtype hospsize treatment
1 1 11 1 36 Male 11 7 general care large Training
1 1 11 2 45 Male 20 7 general care large Training
1 1 11 3 32 Male 7 7 general care large Training
1 1 11 4 57 Female 25 6 general care large Training
1 1 11 5 46 Female 22 6 general care large Training
1 1 11 6 60 Female 22 6 general care large Training
nurses_hierach <- lmer(
  stress ~ age + sex + experience + treatment + wardtype + hospsize +
    ( 1 | hospital) + ( 1 | hospital:ward), # together same as ( 1 | hospital / ward)
  data = nurses
)

mixedup::extract_fixed_effects(nurses_hierach) %>% knitr::kable()
term value se t p_value lower_2.5 upper_97.5
Intercept 5.380 0.185 29.128 0.000 5.018 5.742
age 0.022 0.002 10.053 0.000 0.018 0.026
sexFemale -0.453 0.035 -12.952 0.000 -0.522 -0.385
experience -0.062 0.004 -13.776 0.000 -0.070 -0.053
treatmentTraining -0.700 0.120 -5.843 0.000 -0.935 -0.465
wardtypespecial care 0.051 0.120 0.424 0.671 -0.184 0.286
hospsizemedium 0.489 0.202 2.428 0.015 0.094 0.884
hospsizelarge 0.902 0.275 3.280 0.001 0.363 1.440
mixedup::extract_vc(nurses_hierach, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
hospital:ward Intercept 0.337 0.580 0.500
hospital Intercept 0.119 0.345 0.177
Residual 0.217 0.466 0.323
nurses_varicance_components_random_effects <- REsim(nurses_hierach) %>% as_tibble()

nurses_varicance_components_random_effects %>%
  mutate(sd_leveled = sd * qnorm(1 - ((1 - sd_level)/2)),
         sig = (median + sd_leveled) >= 0 & (median - sd_leveled) <= 0) %>% 
  group_by(groupFctr) %>% 
  arrange(groupFctr, median) %>% 
  mutate(rank = row_number()) %>% 
  arrange(groupFctr, groupID) %>% 
  ungroup() %>% 
  ggplot(aes(x = rank)) +
  geom_segment(aes(xend = rank, y = median - sd_leveled, yend = median +  sd_leveled, color = sig)) +
  geom_point(aes(y = median), size = .3, alpha = .5) +
  geom_hline(yintercept = 0, size = .4, linetype = 3) +
  facet_wrap(groupFctr ~ ., scales = "free_x") +
  scale_color_manual(values = c(`TRUE` = clr2, `FALSE` = clr0d), guide = "none") +
  ylim(-2,2) +
  labs(x = "Group", y = "Effect Range", caption = "interval estimates of random effects")

20.5.1 Crossed vs. nested

nurses_hierach2 <- lmer(
  stress ~ age + sex + experience + treatment + wardtype + hospsize +
    ( 1 | hospital ) + ( 1 | hospital:wardid ), # needs to be wardid now because ward is duplicated over hospitals (not unique)
  data = nurses
)

nurses_nested <- lmer(
  stress ~ age + sex + experience + treatment + wardtype + hospsize +
    ( 1 | hospital ) + ( 1 | wardid ),
  data = nurses
)

Nested:

mixedup::extract_fixed_effects(nurses_hierach2) %>% knitr::kable()
term value se t p_value lower_2.5 upper_97.5
Intercept 5.380 0.185 29.128 0.000 5.018 5.742
age 0.022 0.002 10.053 0.000 0.018 0.026
sexFemale -0.453 0.035 -12.952 0.000 -0.522 -0.385
experience -0.062 0.004 -13.776 0.000 -0.070 -0.053
treatmentTraining -0.700 0.120 -5.843 0.000 -0.935 -0.465
wardtypespecial care 0.051 0.120 0.424 0.671 -0.184 0.286
hospsizemedium 0.489 0.202 2.428 0.015 0.094 0.884
hospsizelarge 0.902 0.275 3.280 0.001 0.363 1.440
mixedup::extract_vc(nurses_hierach2, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
hospital:wardid Intercept 0.337 0.580 0.500
hospital Intercept 0.119 0.345 0.177
Residual 0.217 0.466 0.323

Crossed:

mixedup::extract_fixed_effects(nurses_nested) %>% knitr::kable()
term value se t p_value lower_2.5 upper_97.5
Intercept 5.380 0.185 29.128 0.000 5.018 5.742
age 0.022 0.002 10.053 0.000 0.018 0.026
sexFemale -0.453 0.035 -12.952 0.000 -0.522 -0.385
experience -0.062 0.004 -13.776 0.000 -0.070 -0.053
treatmentTraining -0.700 0.120 -5.843 0.000 -0.935 -0.465
wardtypespecial care 0.051 0.120 0.424 0.671 -0.184 0.286
hospsizemedium 0.489 0.202 2.428 0.015 0.094 0.884
hospsizelarge 0.902 0.275 3.280 0.001 0.363 1.440
mixedup::extract_vc(nurses_nested, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
wardid Intercept 0.337 0.580 0.500
hospital Intercept 0.119 0.345 0.177
Residual 0.217 0.466 0.323

20.6 Residual Structure

rescov <- function(model, data) {
  var.d <- crossprod(getME(model,"Lambdat"))
  Zt <- getME(model,"Zt")
  vr <- sigma(model)^2
  var.b <- vr*(t(Zt) %*% var.d %*% Zt)
  sI <- vr * Diagonal(nrow(data))
  var.y <- var.b + sI
  var.y  %>%
    as.matrix() %>%
    as_tibble() %>% 
    mutate(row = row_number()) %>% 
    pivot_longer(cols = -row, names_to = "column")
}

rescov(gpa_mixed, gpa) %>%
  mutate(x = as.numeric(column),
             y = as.numeric(row)) %>% 
  filter(between(x,0,30),
         between(y,0,30)) %>% 
  ggplot(aes(x = x,
             y = y,
             fill = value)) +
  geom_tile(aes(color = after_scale(clr_darken(fill))), size = .3, width = .9, height = .9) +
  scale_fill_gradientn(colours = c(clr0, clr_lighten(clr1), clr_lighten(clr2))) +
  scale_y_reverse() +
  coord_equal()

covariance matrix for a cluster (compound symmetry):

\[ \Sigma = \left[ \begin{array}{ccc} \color{#B35136}{\sigma^2 + \tau^2} & \tau^2 & \tau^2 & \tau^2 & \tau^2 & \tau^2 \\ \tau^2 & \color{#B35136}{\sigma^2 + \tau^2} & \tau^2 & \tau^2 & \tau^2 & \tau^2 \\ \tau^2 & \tau^2 & \color{#B35136}{\sigma^2 + \tau^2} & \tau^2 & \tau^2 & \tau^2 \\ \tau^2 & \tau^2 & \tau^2 & \color{#B35136}{\sigma^2 + \tau^2} & \tau^2 & \tau^2\\ \tau^2 & \tau^2 & \tau^2 & \tau^2 & \color{#B35136}{\sigma^2 + \tau^2} & \tau^2 \\ \tau^2 & \tau^2 & \tau^2 & \tau^2 & \tau^2 & \color{#B35136}{\sigma^2 + \tau^2} \\ \end{array}\right] \]

Types of covariance structures:

in a standard linear regression model, we have constant variance and no covariance:

\[ \Sigma = \left[ \begin{array}{ccc} \sigma^2 & 0 & 0 \\ 0 & \sigma^2 & 0 \\ 0 & 0 & \sigma^2 \\ \end{array}\right] \]

next, relax the assumption of equal variances, and estimate each separately. In this case of heterogeneous variances, we might see more or less variance over time, for example.

\[ \Sigma = \left[ \begin{array}{ccc} \sigma_1^2 & 0 & 0 \\ 0 & \sigma_2^2 & 0 \\ 0 & 0 & \sigma_3^2 \\ \end{array}\right] \]

we actually want to get at the underlying covariance/correlation. I’ll switch to the correlation representation, but you can still think of the variances as constant or separately estimated. So now we have something like this, where \(\rho\) represents the residual correlation among observations.

\[ \Sigma = \sigma^2 \left[ \begin{array}{ccc} 1 & \rho_1 & \rho_2 \\ \rho_1 & 1 & \rho_3 \\ \rho_2 & \rho_3 & 1 \\ \end{array}\right] \]

\(\rightarrow\) unstructured / symmetric correlation structure (compound symmetry)

Autocorrelation (lag of order one for residuals):

\[ \Sigma = \sigma^2 \left[ \begin{array}{cccc} 1 & \rho & \rho^2 & \rho^3 \\ \rho & 1 & \rho & \rho^2 \\ \rho^2 & \rho & 1 & \rho \\ \rho^3 & \rho^2 & \rho & 1 \\ \end{array}\right] \]

20.6.1 Heterogeneous variance

library(nlme)

gpa_hetero_res <- lme(
  gpa ~ occasion,
  data = gpa,
  random = ~ 1 | student,
  weights = varIdent(form = ~ 1 | occasion)
)

mixedup::extract_fixed_effects(gpa_hetero_res) %>% knitr::kable()
term value se z p_value lower_2.5 upper_97.5
Intercept 2.599 0.026 99.002 0 2.547 2.650
occasion 0.106 0.004 26.317 0 0.098 0.114
mixedup::extract_vc(gpa_hetero_res, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
student Intercept 0.094 0.306 0.404
Residual 0.138 0.372 0.596

alternative approach to heterogeneous variance models:

library(glmmTMB)

gpa_hetero_res2 <- glmmTMB(
  gpa ~ occasion + ( 1 | student ) + diag( 0 + occas | student ),
  data = gpa
)

Comparing results of {nlme} and {glmmTMB}

tibble(relative_val = c(1, coef(gpa_hetero_res$modelStruct$varStruct, unconstrained = FALSE))) %>% 
  mutate(absolute_val = (relative_val * gpa_hetero_res$sigma) ^ 2,
         `hetero_res (nlme)` = mixedup::extract_het_var(gpa_hetero_res, scale = 'var', digits = 5) %>%
           unname() %>% as.vector() %>% t() %>% .[,1],
         `hetero_res (glmmTMB)` = mixedup::extract_het_var(gpa_hetero_res2, scale = 'var', digits = 5) %>%
           dplyr::select(-group) %>% unname() %>% as.vector() %>% t() %>% .[,1]) %>% 
  knitr::kable()
relative_val absolute_val hetero_res (nlme) hetero_res (glmmTMB)
1.0000000 0.1381504 0.13815 0.13790
0.8261186 0.0942837 0.09428 0.09415
0.6272415 0.0543528 0.05435 0.05430
0.4311126 0.0256764 0.02568 0.02568
0.3484013 0.0167692 0.01677 0.01677
0.4324628 0.0258374 0.02584 0.02580

20.6.2 Autocorrelation

gpa_autocorr <- lme(
  gpa ~ occasion,
  data = gpa,
  random = ~ 1 | student,
  correlation = corAR1(form = ~ occasion)
)

mixedup::extract_fixed_effects(gpa_autocorr) %>% knitr::kable()
term value se z p_value lower_2.5 upper_97.5
Intercept 2.597 0.023 113.146 0 2.552 2.642
occasion 0.107 0.005 20.297 0 0.097 0.118
mixedup::extract_vc(gpa_autocorr, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
student Intercept 0.046 0.215 0.381
Residual 0.075 0.273 0.619
gpa_autocorr2 <- glmmTMB(
  gpa ~ occasion + ar1( 0 + occas | student ) + ( 1 | student ),
  # occas is cotegorical version of occasion
  data = gpa
)

mixedup::extract_fixed_effects(gpa_autocorr2) %>% knitr::kable()
term value se z p_value lower_2.5 upper_97.5
Intercept 2.598 0.023 111.077 0 2.552 2.643
occasion 0.107 0.005 19.458 0 0.096 0.118
mixedup::extract_vc(gpa_autocorr2, ci_level = 0) %>% knitr::kable()
group variance sd var_prop
student 0.093 0.305 0.159
student.1 0.000 0.000 0.000
Residual 0.028 0.167 0.048

20.7 Generalized Linear Mixed Models

load("data/speed_dating.RData")

sdating <- glmer(
  decision ~ sex + samerace + attractive_sc + sincere_sc + intelligent_sc +
    ( 1 | iid),
  data = speed_dating,
  family = binomial
)

mixedup::extract_fixed_effects(sdating) %>% knitr::kable()
term value se z p_value lower_2.5 upper_97.5
Intercept -0.743 0.121 -6.130 0.00 -0.981 -0.506
sexMale 0.156 0.164 0.954 0.34 -0.165 0.478
sameraceYes 0.314 0.075 4.192 0.00 0.167 0.460
attractive_sc 0.502 0.015 33.559 0.00 0.472 0.531
sincere_sc 0.089 0.016 5.747 0.00 0.059 0.120
intelligent_sc 0.143 0.017 8.232 0.00 0.109 0.177
mixedup::extract_vc(sdating, ci_level = 0) %>% knitr::kable()
group effect variance sd var_prop
iid Intercept 2.708 1.645 1

20.8 Issues/ Considderations

  • small number of clusters: problematic - similar to number of samples to compute variance / mean or similar (to get to the variance component we need enough groups). This also touches whether something should be a fixed or a random effect (random is always possible when the number of clusters is large enough)
  • small number of observations within clusters: no problem, but might prevent random slopes (for n == 1)
  • balanced design / missing data: not really a requirement, so as long as it is not extreme likely not an issue

20.8.1 Model comparison

Using AIC can help, but should not used to make the decision—reasoning about the implications of the used models should.

gpa_1 <- lmer(gpa ~ occasion + (1 + occasion | student), data = gpa)
gpa_2 <- lmer(gpa ~ occasion + sex + (1 + occasion | student), data = gpa)
gpa_3 <- lmer(gpa ~ occasion + (1 | student), data = gpa)

list(gpa_1 = gpa_1, gpa_2 = gpa_2, gpa_3 = gpa_3) %>%
  map_df(function(mod) data.frame(AIC = AIC(mod)), .id = 'model') %>% 
  arrange(AIC) %>% 
  mutate(`Δ AIC` = AIC - min(AIC)) %>% 
  knitr::kable()
model AIC Δ AIC
gpa_2 269.7853 0.000000
gpa_1 272.9566 3.171217
gpa_3 416.8929 147.107536

20.9 Formula summary

formula meaning
(1|group) random group intercept
(x|group) = (1+x|group) random slope of x within group with correlated intercept
(0+x|group) = (-1+x|group) random slope of x within group: no variation in intercept
(1|group) + (0+x|group) uncorrelated random intercept and random slope within group
(1|site/block) = (1|site)+(1|site:block) intercept varying among sites and among blocks within sites (nested random effects)
site+(1|site:block) fixed effect of sites plus random variation in intercept among blocks within sites
(x|site/block) = (x|site)+(x|site:block) = (1 + x|site)+(1+x|site:block) slope and intercept varying among sites and among blocks within sites
(x1|site)+(x2|block) two different effects, varying at different levels
x*site+(x|site:block) fixed effect variation of slope and intercept varying among sites and random variation of slope and intercept among blocks within sites
(1|group1)+(1|group2) intercept varying among crossed random effects (e.g. site, year)
equation formula
\(β_0 + β_{1}X_{i} + e_{si}\) n/a (Not a mixed-effects model)
\((β_0 + b_{S,0s}) + β_{1}X_i + e_{si}\) ∼ X + (1∣Subject)
\((β_0 + b_{S,0s}) + (β_{1} + b_{S,1s}) X_i + e_{si}\) ~ X + (1 + X∣Subject)
\((β_0 + b_{S,0s} + b_{I,0i}) + (β_{1} + b_{S,1s}) X_i + e_{si}\) ∼ X + (1 + X∣Subject) + (1∣Item)
As above, but \(S_{0s}\), \(S_{1s}\) independent ∼ X + (1∣Subject) + (0 + X∣ Subject) + (1∣Item)
\((β_0 + b_{S,0s} + b_{I,0i}) + β_{1}X_i + e_{si}\) ∼ X + (1∣Subject) + (1∣Item)
\((β_0 + b_{I,0i}) + (β_{1} + b_{S,1s})X_i + e_{si}\) ∼ X + (0 + X∣Subject) + (1∣Item)