Two-way Repeated Measures ANOVA

In this guide, I cover how to conduct two-way repeated measures ANOVA with two within-subjects factors.

The data set

For this guide, we will use the data from Chapter 12, Table 1 in the AMCP package. In this hypothetical study, 10 subjects are participating in an experiment that tests how visual information can interfere with recognizing letters. Each subject performs a letter recognition task in two conditions, noise absent or present. Noise refers to visual information that is presented along with either a letter T or I that the subject needs to identify. Within each condition, visual information was either presented at 0° (directly in front of the participant), at 4° (slightly offset to one side), or at 8° (even more offset to the side). Thus, noise and degree represent our repeated measures. Finally, the dependent variable in this study is the latency in milliseconds needed to identify the letter.

library(AMCP)

# Load the data
data(chapter_12_table_1)

# Display part of the data
kableExtra::kable(head(chapter_12_table_1))
Absent0Absent4Absent8Present0Present4Present8
420420480480600780
420480480360480600
480480540660780780
420540540480780900
540660540480660720
360420360360480540

Perform ANOVA tests


As we encountered in the guide for one-way rm-ANOVA, we will want to convert our data set from wide format to long format before conducting the statistical test. This situation is a bit trickier because there are many more columns to deal with so I’ve written a separate walk through that can be found here in order to focus on the procedures for conducting the statistical tests.

library(tidyverse)
library(rstatix)
library(ggpubr)

# Create a new data frame with a subject id
data <- cbind(id = c(1:10), chapter_12_table_1)

# Convert the data from wide to long
data <-  data %>%
  gather(key = Condition.Angle,
         value = Latency,-id) %>%
  separate(col = Condition.Angle,
           into = c("Condition", "Angle"),
           sep = -1) %>%
  arrange(id,
          Condition,
          Angle) %>%
  convert_as_factor(Condition, Angle)

# Conduct repeated measures ANOVA
rm.mod <- anova_test(data = data,
                     dv = Latency, 
                     wid = id, 
                     within = c(Condition, Angle), 
                     effect.size = "pes")

# Print ANOVA table and  Mauchly's Test for Sphericity 
rm.mod
## ANOVA Table (type III tests)
## 
## $ANOVA
##            Effect DFn DFd      F        p p<.05   pes
## 1       Condition   1   9 33.766 2.56e-04     * 0.790
## 2           Angle   2  18 40.719 2.09e-07     * 0.819
## 3 Condition:Angle   2  18 45.310 9.42e-08     * 0.834
## 
## $`Mauchly's Test for Sphericity`
##            Effect     W     p p<.05
## 1           Angle 0.960 0.850      
## 2 Condition:Angle 0.894 0.638      
## 
## $`Sphericity Corrections`
##            Effect   GGe      DF[GG]    p[GG] p[GG]<.05   HFe      DF[HF]
## 1           Angle 0.962 1.92, 17.31 3.40e-07         * 1.218 2.44, 21.92
## 2 Condition:Angle 0.904 1.81, 16.27 3.45e-07         * 1.118 2.24, 20.12
##      p[HF] p[HF]<.05
## 1 2.09e-07         *
## 2 9.42e-08         *

Interpretation

The results of Mauchly’s test indicate that we have not violated the assumption of sphericity and do not require any corrections on the F-values. In addition, the results suggest a significant interaction between noise and condition. As a result, the next step will be to test for simple effects. In other words, we can decompose the significant interaction into testing the effect of angle at each level of noise and then test the effect of noise at each level of angle. Each of these tests can be thought of as a series of one-way repeated measures tests.

The effect of angle at each level of condition

Let’s begin by carrying out two one-way repeated measures ANOVAs, one for each level of Condition. To help visualize these analyses, we can plot the data with the ggline() function. The results of these analyses reveal significant effects of angle when conducted on on absent and present data separately. These significant effects can be further investigated with pair wise comparisons.

data %>% 
  group_by(Condition) %>% 
  anova_test(dv = Latency, wid = id, within = Angle, effect.size = "pes") %>% 
  get_anova_table() %>%
  adjust_pvalue(method = "bonferroni") %>%
  kableExtra::kable()
ConditionEffectDFnDFdFpp<.05pesp.adj
AbsentAngle2185.0460.018*0.3590.036
PresentAngle21877.0220.000*0.8950.000
# Generate Plot 1
ggline(data,
       "Angle", 
       "Latency",
       color = "Condition",
       add = "mean_se",
       palette = "jama",
       position = position_dodge(.2))

# Pairwise comparisons at each level of Condition
data %>% 
  group_by(Condition) %>%
  pairwise_t_test(
    Latency ~ Angle,
    paired = TRUE,
    p.adjust.method = "bonferroni") %>%
  kableExtra::kable()
Condition.y.group1group2n1n2statisticdfpp.adjp.adj.signif
AbsentLatency041010-2.449489793.70e-021.10e-01ns
AbsentLatency081010-3.497993097.00e-032.00e-02*
AbsentLatency481010-0.709299494.96e-011.00e+00ns
PresentLatency041010-8.573214191.27e-053.81e-05****
PresentLatency081010-9.925397493.80e-061.14e-05****
PresentLatency481010-5.666666793.07e-049.21e-04***

The effect of condition at each level of angle

Now we move towards carrying out three one-way repeated measures ANOVAs, one for each level of Angle. As before, we can plot the data with the ggline() function to help with the interpretation of the data. The results of these analyses reveal significant effects of condition when conducted separately at each level of angle. These significant effects are further investigated with pair wise comparisons.

data %>% 
  group_by(Angle) %>% 
  anova_test(dv = Latency, wid = id, within = Condition, effect.size = "pes") %>% 
  get_anova_table() %>%
  adjust_pvalue(method = "bonferroni") %>%
  kableExtra::kable()
AngleEffectDFnDFdFpp<.05pesp.adj
0Condition191.5522.44e-010.1477.32e-01
4Condition1919.7372.00e-03*0.6876.00e-03
8Condition19125.5871.40e-06*0.9334.10e-06
# Generate Plot 2
ggline(data,
       "Condition", 
       "Latency",
       color = "Angle",
       add = "mean_se",
       palette = "jama",
       position = position_dodge(.2))

# Pairwise comparisons at each level of Angle
data %>% 
  group_by(Angle) %>%
  pairwise_t_test(
    Latency ~ Condition,
    paired = TRUE,
    p.adjust.method = "bonferroni") %>%
  kableExtra::kable()
Angle.y.group1group2n1n2statisticdfpp.adjp.adj.signif
0LatencyAbsentPresent1010-1.24568292.44e-012.44e-01ns
4LatencyAbsentPresent1010-4.44261792.00e-032.00e-03**
8LatencyAbsentPresent1010-11.20656891.40e-061.40e-06****

A repeated measures ANOVA can also be analyzed under a mixed effects framework using the lme4 package. Mixed effects models contain a mixture of fixed and random effects and are also sometimes referred to as multi-level models or hierarchical linear models. Fixed effects can be thought of as factors in which all possible levels that a researcher is interested in are represented in the data. A random effect on the other hand is a factor for which the levels in the experimental data represent a sample of a larger set. Mixed effects models may be advantageous because they do not require the assumption of independence in ANOVA, the assumption of homogeneity of regression slopes in ANCOVA, and may be better suited for handling unbalanced designs or situations with missing data. While mixed effect models are estimated using slightly different procedures than more traditional models, there are cases in which the results will overlap. The balanced repeated measures ANOVA with two within-subjects factors is one of those cases.

The lmer() function from the lme4 package is designed to fit linear mixed-effects regression models via REML or maximum likelihood. Just like the base R lm() function, lmer() takes a formula specifying the dependent variable predicted by (~) a combination of fixed- and random- effect variables. Predictor variables encased in parentheses specify random effects, while variables without parentheses are specified as fixed effects.

The first part of the formula in the example below specifies to predict Latency from the interaction between Condition and Angle as fixed effects. Including just the interaction term is a shortcut for “Latency ~ Condition + Angle + Condition * Angle.” The term “(1|id)” specifies a random intercept for each subject. This allows each subject to have their own mean (starting point) modeled. The remaining terms with the colons indicate random intercepts for the interactions between subject and Condition (1|Condition:id) and subject and Angle (1|Angle:id). The following results will not only mirror those displayed under the rstatix approach, but will also mirror the results generated by SAS.

library(lme4)
library(lmerTest)
library(broom.mixed)

rm.mod <- lmerTest::lmer(Latency ~ Condition + Angle + Condition:Angle + 
                 (1|id) +
                 (1|Condition:id) + 
                 (1|Angle:id), 
               data=data)    

anova(rm.mod)
## Type III Analysis of Variance Table with Satterthwaite's method
##                 Sum Sq Mean Sq NumDF DenDF F value    Pr(>F)    
## Condition        39169   39169     1     9  33.766  0.000256 ***
## Angle            94468   47234     2    18  40.719 2.087e-07 ***
## Condition:Angle 105120   52560     2    18  45.310 9.424e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
summary(rm.mod)
## Linear mixed model fit by REML. t-tests use Satterthwaite's method [
## lmerModLmerTest]
## Formula: Latency ~ Condition + Angle + Condition:Angle + (1 | id) + (1 |  
##     Condition:id) + (1 | Angle:id)
##    Data: data
## 
## REML criterion at convergence: 616.1
## 
## Scaled residuals: 
##      Min       1Q   Median       3Q      Max 
## -1.49491 -0.46023  0.02919  0.50472  1.41413 
## 
## Random effects:
##  Groups       Name        Variance Std.Dev.
##  Angle:id     (Intercept) 1200     34.64   
##  Condition:id (Intercept) 2433     49.33   
##  id           (Intercept) 3600     60.00   
##  Residual                 1160     34.06   
## Number of obs: 60, groups:  Angle:id, 30; Condition:id, 20; id, 10
## 
## Fixed effects:
##                         Estimate Std. Error     df t value Pr(>|t|)    
## (Intercept)               462.00      28.97  19.79  15.947 9.28e-13 ***
## ConditionPresent           30.00      26.81  14.08   1.119  0.28183    
## Angle4                     48.00      21.73  28.60   2.209  0.03532 *  
## Angle8                     66.00      21.73  28.60   3.038  0.00505 ** 
## ConditionPresent:Angle4   120.00      21.54  18.00   5.571 2.75e-05 ***
## ConditionPresent:Angle8   204.00      21.54  18.00   9.470 2.05e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Correlation of Fixed Effects:
##             (Intr) CndtnP Angle4 Angle8 CnP:A4
## CondtnPrsnt -0.463                            
## Angle4      -0.375  0.199                     
## Angle8      -0.375  0.199  0.500              
## CndtnPrs:A4  0.186 -0.402 -0.496 -0.248       
## CndtnPrs:A8  0.186 -0.402 -0.248 -0.496  0.500
tidy(rm.mod)
## # A tibble: 10 × 8
##    effect   group        term            estim…¹ std.e…² stati…³    df   p.value
##    <chr>    <chr>        <chr>             <dbl>   <dbl>   <dbl> <dbl>     <dbl>
##  1 fixed    <NA>         (Intercept)       462.     29.0   15.9   19.8  9.28e-13
##  2 fixed    <NA>         ConditionPrese…    30.0    26.8    1.12  14.1  2.82e- 1
##  3 fixed    <NA>         Angle4             48.0    21.7    2.21  28.6  3.53e- 2
##  4 fixed    <NA>         Angle8             66.0    21.7    3.04  28.6  5.05e- 3
##  5 fixed    <NA>         ConditionPrese…   120.     21.5    5.57  18.0  2.75e- 5
##  6 fixed    <NA>         ConditionPrese…   204.     21.5    9.47  18.0  2.05e- 8
##  7 ran_pars Angle:id     sd__(Intercept)    34.6    NA     NA     NA   NA       
##  8 ran_pars Condition:id sd__(Intercept)    49.3    NA     NA     NA   NA       
##  9 ran_pars id           sd__(Intercept)    60.0    NA     NA     NA   NA       
## 10 ran_pars Residual     sd__Observation    34.1    NA     NA     NA   NA       
## # … with abbreviated variable names ¹​estimate, ²​std.error, ³​statistic

For these data, the lmer() function produces the exact same results as the anova_test() function. The

Back to tabs

References

Kassambara, Alboukadel. 2020a. Ggpubr: ’Ggplot2’ Based Publication Ready Plots. https://CRAN.R-project.org/package=ggpubr.

———. 2020b. Rstatix: Pipe-Friendly Framework for Basic Statistical Tests. https://CRAN.R-project.org/package=rstatix.

Maxwell, Scott, Harold Delaney, and Ken Kelley. 2020. AMCP: A Model Comparison Perspective. https://CRAN.R-project.org/package=AMCP.

Maxwell, Scott E, Harold D Delaney, and Ken Kelley. 2017. Designing Experiments and Analyzing Data: A Model Comparison Perspective. Routledge.

Selker, Ravi, Jonathon Love, and Damian Dropmann. 2020. Jmv: The ’Jamovi’ Analyses. https://CRAN.R-project.org/package=jmv.

Wickham, Hadley. 2019. Tidyverse: Easily Install and Load the ’Tidyverse’. https://CRAN.R-project.org/package=tidyverse.

Previous
Next