MATH 80667A - Week 10

Blocking factor

library(emmeans)
library(hecedsm)
library(ggplot2)
library(afex)
library(lmerTest)
# Force sum-to-zero parametrization for unordered factors
options(contrasts = c("contr.sum", "contr.poly"))
## Blocking factor
# Note that this is fundamentally repeated measures
url <- "https://edsm.rbind.io/files/data/resting_metabolic_rate.txt"
# transform integers to factors (categorical)
resting <- read.table(url, header = TRUE) |>
  dplyr::mutate(
    subject = factor(subject), #blocking factor
    protocol = factor(protocol), #experimental factor
    rate = rate/1000)

# Fit model with blocking factor
model_block <- aov(rate ~ subject + protocol, data = resting)
# One-way ANOVA (no blocking)
model_raw <- aov(rate ~ protocol, data = resting)

# ANOVA tables with and without blocking factor
anova(model_block) |>
  knitr::kable()
anova(model_raw) |>
  knitr::kable()
Table 1: Analysis of variance table
Df Sum Sq Mean Sq F value Pr(>F)
subject 8 23.117 2.890 37.423 0.000
protocol 2 0.036 0.018 0.233 0.795
Residuals 16 1.235 0.077
Df Sum Sq Mean Sq F value Pr(>F)
protocol 2 0.036 0.018 0.018 0.982
Residuals 24 24.353 1.015
# Use ANOVA table to compute etasquared effect size
(etasq <- 0.0359/(23.1175 + 1.2355 + 0.0359))
[1] 0.00147
efs <- effectsize::eta_squared(
  model_block, 
  partial = TRUE,
  generalized = "subject")
# Interaction plot
ggplot(data = resting,
       aes(x = subject,
           y = rate,
           group = protocol,
           color = protocol)) +
  geom_line(linewidth = 1.5) +
  labs(subtitle = "mean resting metabolic rate",
       y = "",
       x = "subject identifier") +
  scale_color_grey()+
  theme_classic() +
  theme(legend.position = "bottom")
Figure 1: Interaction plot

Mixed model

Hosano et al. (2022)

The purpose of this is to show the equivalence of repeated measures and mixed models for tests of interest in balanced designs with a single repeated measurement per individual, here captured through individual-specific random effects.

We consider three different models for imscore as a function of waiting (between-subject factor) and ratingtype (within-subject factor). Each individual id gives two ratings. The id are nested withing waiting, but crossed with ratingtype.

  • a mixed ANOVA (within-between) full-factorial design (i.e., including the interaction term),
  • a MANOVA with the two measurements of imscore by ratingtype as response,
  • a linear mixed model with a random effect for id and both waiting, ratingtype and their interaction.
data(HOSM22_E3, package = 'hecedsm')
# Pivot from long to wide for MANOVA
HOSM22_E3w <- HOSM22_E3 |> tidyr::pivot_wider(
  names_from = ratingtype,
  values_from = imscore
)

xtabs(~ratingtype + waiting, HOSM22_E3) |>
  knitr::kable() # crossed factors
head(xtabs(~id + waiting, HOSM22_E3), n = 5) |>
  knitr::kable(row.names = TRUE) # id nested in waiting
head(xtabs(~id + ratingtype, HOSM22_E3), n = 5) |>
  knitr::kable(row.names = TRUE)
long short
experience 32 31
prediction 32 31
long short
1 0 2
2 2 0
3 2 0
4 0 2
5 2 0
experience prediction
1 1 1
2 1 1
3 1 1
4 1 1
5 1 1
mod <- aov_ez(id = "id",
              dv = "imscore",
              between = "waiting",
              within = "ratingtype",
              data = HOSM22_E3)

# Obtain MANOVA table
MANOVA_tab <- mod$Anova # same as below
Table 2: Type III Repeated Measures MANOVA Tests: Pillai test statistic
# Fit instead the model with MANOVA using multivariate linear regression
manova_mod <- lm(cbind(prediction, experience) ~ waiting,
                 data = HOSM22_E3w)
# For repeated measures, we need to reconstruct the missing factor(s)
# corresponding to the repeated measures via a data frame (idata)
# that contains the factor levels and the variable name
# and idesign that includes the additional factors to our models
car::Anova(manova_mod,
           idata = data.frame(
             ratingtype = factor(c("prediction","experience"))),
           idesign =~ratingtype, type = 3) 

Type III Repeated Measures MANOVA Tests: Pillai test statistic
                   Df test stat approx F num Df den Df  Pr(>F)    
(Intercept)         1     0.893      508      1     61 < 2e-16 ***
waiting             1     0.156       11      1     61  0.0014 ** 
ratingtype          1     0.387       38      1     61 5.4e-08 ***
waiting:ratingtype  1     0.001        0      1     61  0.7757    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Fitting the same 2x2 model (with interaction), including a random intercept per subject
library(lmerTest)
mixmod <- lmer(
  imscore ~ waiting*ratingtype + (1 | id),
  data = HOSM22_E3)
# We get the same table if we set type III
anova(mixmod, type = 2)
Type II Analysis of Variance Table with Satterthwaite's method
                   Sum Sq Mean Sq NumDF DenDF F value  Pr(>F)    
waiting              7.76    7.76     1    61   11.26  0.0014 ** 
ratingtype          26.47   26.47     1    61   38.39 5.5e-08 ***
waiting:ratingtype   0.06    0.06     1    61    0.08  0.7757    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Curley et al.

This is an incomplete block design: there are two (counterbalanced) combinations of anchor, vignette, and verdictsyst, but participants see 2 out of 8 combinations. Hence, no interaction between these and ID is possible.

data(C22, package = "hecedsm")
head(C22)
# A tibble: 6 × 7
  anchor       vignette id    vorder verdictsyst guilt  pjaq
  <fct>        <fct>    <fct> <fct>  <fct>       <dbl> <dbl>
1 strong-first 1        1     1      two         104.     67
2 strong-first 1        2     1      two          81.8   101
3 strong-first 1        3     1      two          87.8    65
4 strong-first 1        4     1      two          82.7    60
5 strong-first 1        5     1      two          76.8    75
6 strong-first 1        6     1      two          96.2    73
options(contrasts = c("contr.sum", "contr.poly"))
# balanced!
xtabs(~ anchor + vignette + verdictsyst, data = C22)
, , verdictsyst = two

              vignette
anchor          1  2
  weak-first   32 32
  strong-first 32 32

, , verdictsyst = three

              vignette
anchor          1  2
  weak-first   32 32
  strong-first 32 32
xtabs(~  interaction(anchor, vignette, verdictsyst) + id, data = C22)[,1:5]
                                          id
interaction(anchor, vignette, verdictsyst) 1 2 3 4 5
                      weak-first.1.two     0 0 0 0 0
                      strong-first.1.two   1 1 1 1 1
                      weak-first.2.two     0 0 0 0 0
                      strong-first.2.two   0 0 0 0 0
                      weak-first.1.three   0 0 0 0 0
                      strong-first.1.three 0 0 0 0 0
                      weak-first.2.three   1 1 1 1 1
                      strong-first.2.three 0 0 0 0 0
model <- lmer(
  guilt ~ anchor*vignette*verdictsyst + pjaq + (1|id),
  data = C22)
# pjaq is a covariate (so used to reduce error, plus the slope is of interest on it's own
# Cannot have interaction pjaq * id, because we get a single pjaq score per person

# No ambiguity for sum of square decomposition
anova(model)
Type III Analysis of Variance Table with Satterthwaite's method
                            Sum Sq Mean Sq NumDF DenDF F value  Pr(>F)    
anchor                        1601    1601     1   124    7.49  0.0071 ** 
vignette                      2251    2251     1   124   10.53  0.0015 ** 
verdictsyst                    296     296     1   124    1.39  0.2413    
pjaq                          4277    4277     1   123   20.00 1.7e-05 ***
anchor:vignette                  4       4     1   123    0.02  0.8959    
anchor:verdictsyst               3       3     1   123    0.01  0.9028    
vignette:verdictsyst          1238    1238     1   123    5.79  0.0176 *  
anchor:vignette:verdictsyst      4       4     1   124    0.02  0.8945    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# No three-way interaction
# A two-way interaction between vignette:verdictsyst
# Computing differences between anchors
emmeans(model, specs = "anchor") |> pairs()
 contrast                      estimate   SE  df t.ratio p.value
 (weak-first) - (strong-first)        5 1.83 124   2.736  0.0071

Results are averaged over the levels of: vignette, verdictsyst 
Degrees-of-freedom method: kenward-roger 
# Computing differences in verdict separately for each vignette
emmeans(model, specs = "verdictsyst", by = "vignette") |> pairs()
vignette = 1:
 contrast    estimate   SE  df t.ratio p.value
 two - three     11.1 4.14 179   2.678  0.0081

vignette = 2:
 contrast    estimate   SE  df t.ratio p.value
 two - three     -6.8 4.14 179  -1.640  0.1028

Results are averaged over the levels of: anchor 
Degrees-of-freedom method: kenward-roger 

Chocolate rating

book.url <- "http://stat.ethz.ch/~meier/teaching/book-anova"
chocolate <- read.table(file.path(book.url, "data/chocolate.dat"),
                        header = TRUE)
chocolate[,"rater"]      <- factor(chocolate[,"rater"])
chocolate[,"background"] <- factor(chocolate[,"background"])
str(chocolate)
'data.frame':   160 obs. of  4 variables:
 $ choc      : chr  "A" "A" "B" "B" ...
 $ rater     : Factor w/ 10 levels "1","2","3","4",..: 1 1 1 1 1 1 1 1 2 2 ...
 $ background: Factor w/ 2 levels "rural","urban": 1 1 1 1 1 1 1 1 1 1 ...
 $ y         : int  61 64 46 45 63 66 59 56 65 69 ...
# Fit the model (note that rater recycles the id 1:10, so we need to be careful here!
chocolate <- chocolate |> dplyr::mutate(id = factor(paste(background, rater)))
# This model is correct
model <- lmer(y ~ background*choc +
                (1 | rater:background) + (1 | rater:choc:background),
              data = chocolate)
# This is fine too (because of the distinct IDs)
model <- lmer(y ~ background*choc +
                (1 | id) + (1 | choc:id),
              data = chocolate)
# This is WRONG (compare degrees of freedom)
# model <- lmer(y ~ background*choc +
#                 (1 | rater) + (1 | choc:rater),
#               data = chocolate)
# Data are again balanced
anova(model)
Type III Analysis of Variance Table with Satterthwaite's method
                Sum Sq Mean Sq NumDF DenDF F value  Pr(>F)    
background         263     263     1    18   27.61 5.4e-05 ***
choc              4219    1406     3    54  147.74 < 2e-16 ***
background:choc     64      21     3    54    2.24   0.094 .  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# There is a no evidence of interaction
# The model output includes variance coefficients
summary(model)
Linear mixed model fit by REML. t-tests use Satterthwaite's method [
lmerModLmerTest]
Formula: y ~ background * choc + (1 | id) + (1 | choc:id)
   Data: chocolate

REML criterion at convergence: 921

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.2380 -0.4833 -0.0244  0.4950  2.2997 

Random effects:
 Groups   Name        Variance Std.Dev.
 choc:id  (Intercept)  9.42    3.07    
 id       (Intercept) 19.63    4.43    
 Residual              9.52    3.09    
Number of obs: 160, groups:  choc:id, 80; id, 20

Fixed effects:
                  Estimate Std. Error      df t value Pr(>|t|)    
(Intercept)         70.819      1.076  18.000   65.79  < 2e-16 ***
background1         -5.656      1.076  18.000   -5.25  5.4e-05 ***
choc1                5.431      0.729  54.000    7.45  7.7e-10 ***
choc2              -14.844      0.729  54.000  -20.35  < 2e-16 ***
choc3                7.881      0.729  54.000   10.81  4.1e-15 ***
background1:choc1   -0.394      0.729  54.000   -0.54     0.59    
background1:choc2   -0.519      0.729  54.000   -0.71     0.48    
background1:choc3   -0.944      0.729  54.000   -1.29     0.20    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Correlation of Fixed Effects:
            (Intr) bckgr1 choc1  choc2  choc3  bck1:1 bck1:2
background1  0.000                                          
choc1        0.000  0.000                                   
choc2        0.000  0.000 -0.333                            
choc3        0.000  0.000 -0.333 -0.333                     
bckgrnd1:c1  0.000  0.000  0.000  0.000  0.000              
bckgrnd1:c2  0.000  0.000  0.000  0.000  0.000 -0.333       
bckgrnd1:c3  0.000  0.000  0.000  0.000  0.000 -0.333 -0.333
# Look at best chocolate type overall
(emm <- emmeans(model, specs = "choc"))
 choc emmean  SE   df lower.CL upper.CL
 A      76.2 1.3 35.8     73.6     78.9
 B      56.0 1.3 35.8     53.3     58.6
 C      78.7 1.3 35.8     76.1     81.3
 D      72.3 1.3 35.8     69.7     75.0

Results are averaged over the levels of: background 
Degrees-of-freedom method: kenward-roger 
Confidence level used: 0.95 
emm |> contrast("pairwise")
 contrast estimate   SE df t.ratio p.value
 A - B       20.27 1.19 54  17.030  <.0001
 A - C       -2.45 1.19 54  -2.060  0.1803
 A - D        3.90 1.19 54   3.270  0.0097
 B - C      -22.72 1.19 54 -19.080  <.0001
 B - D      -16.37 1.19 54 -13.750  <.0001
 C - D        6.35 1.19 54   5.330  <.0001

Results are averaged over the levels of: background 
Degrees-of-freedom method: kenward-roger 
P value adjustment: tukey method for comparing a family of 4 estimates 
# C has the highest rating, but indistinguishable from A
# B is worst

# Compare variability - extract variance from model
(vars <- c(unlist(VarCorr(model)), sigma(model)^2))
choc:id      id         
   9.42   19.63    9.52 
# Compare these with the output of summary
# Correlation between same chocolate/rater
sum(vars[1:2])/sum(vars)
[1] 0.753
# Correlation between measurements from same rater, different chocolates
vars[2]/sum(vars)
   id 
0.509