If, at some point in your life, you should come across anything better than justice, truth, self-control, courage—it must be an extraordinary thing indeed. —Marcus Aurelius, The Meditations
In this week’s lecture, we considered the procedures involved in performing a two-factor mixed and fully-within participants ANOVA. Using the hypothetical data on the Stroop effect presented in the lecture, in today’s lab session we will demonstrate how to analyse such ANOVA designs in R, including the calculation of the simple main effects and follow up procedures for breaking down simple main effects of factors with three or more levels. We will also demonstrate how to write up the results of two-factor mixed and fully within-participants designs.
If you get stuck at any point, be proactive and ask for help from one of the GTAs.
To get started, we first need to log into the R Studio Server.
You can access Lancaster Universities RStudio Server at http://psy-rstudio.lancaster.ac.uk. At present, you will need to be on campus, or connected to the VPN to access this. If you do not yet have Eduroam (the university WiFi) available on your personal device, please follow the instructions from the PSYC214 Announcement Page https://modules.lancaster.ac.uk/mod/forum/discuss.php?d=388256
If you are on Eduroam (or VPN if off campus) and have accessed the RStudio Server from the URL above, you will now see a login screen (see above). Please use your normal Lancaster University username (e.g., bloggsj). Your own individual RStudio Server password was sent in an email, prior to the first lab, by Kay Rawlins: email header ‘R Studio Server login details’. Please make sure you have this.
Once you are logged into the server, create a folder for today’s
session. Navigate to the bottom right panel and under the
Files
option select the New Folder
option.
Name the new folder psyc214_lab_8
. Please ensure
that you spell this correctly otherwise when you set the
directory using the command given below it will return an error.
Se we can save this session on the server, click File
on
the top ribbon and select New project
. Next, select
existing directory and name the working directory
~/psyc214_lab_8
before selecting
create project
.
Finally, open a script for executing today’s coding exercises.
Navigate to the top left pane of RStudio, select File
->
New File
-> R Script
. Working from a script
will make it easier to edit your code and you will be able to save your
work for a later date.
Let’s set our working directory:
setwd("~/psyc214_lab_8")
Now that you have created a folder for today’s session, it’s time to
add the Week 8 files. Head on over to the PSYC214 Moodle page, access
the lab folder for Week 8, and download the files
StroopMixed.csv
, StroopWithin.csv
, and
simple.R
to your desktop. Next, in the RStudio Server open
your new psyc214_lab_8 folder. When in the new folder, select the
Upload
tab. This will present a box that will ask where the
data is that you want to upload. Click on Browse
, find the
StroopMixed.csv
file on your desktop and click
OK
, then repeat these steps for the
StroopWithin.csv
and the simple.R
files.
Before moving on, let’s load the relevant libraries/functions that we will be using in today’s session.
library("tidyverse") # For data storage and manipulation
library("tidyr") # For tidy data
library("rstatix") # For descriptives statistics, outlier detection, running the ANOVAs etc.
source("simple.R") # Custom function for generating the simple main effects
Note that the custom function simple.R
that we
use to calculate the simple main effects must be present in your
directory whenever you seek to use it.
We begin by analysing the mixed-design Stroop experiment described in the lecture. Recall that in this experiment, a researcher wants to test the hypothesis that response inhibition—the ability to suppress task-irrelevant information—is impaired in patients with schizophrenia. She predicts that if this is true, then a group of patients with schizophrenia will show a larger Stroop effect than a group of healthy controls. Our researcher administers a multi-trial Stroop task to two groups of participants in a 2 (group: healthy vs. schizophrenia) \(\times\) 2 (trial type: congruent vs. incongruent) mixed design, where group is a between-participants factor and trial type is a within-participants factor.
The data set contains four columns:
Participant: represents the participant number, which ranges from 1–80, with \(N\) = 40 participants in each of the four conditions resulting from the combination of our two factors.
Group: represents whether the participant belongs to the healthy control group (Healthy) or the schizophrenia group (Schizophrenia).
Congruent: represents the mean response time, averaged across trials, for congruent trials in which the colour word and the ink it is presented in are the same.
Incongruent: represents the mean response time, averaged across trials, for incongruent trials in which the colour word and the ink it is presented in are different.
The first thing you need to do is load the data into RStudio. Make
sure that you name your data frame as stroopMixed
.
# *** ENTER YOUR OWN CODE HERE TO IMPORT THE DATA ***
# Import data
stroopMixed = read_csv("StroopMixed.csv")
(stroopMixed)
## # A tibble: 80 x 4
## Participant Group Congruent Incongruent
## <dbl> <chr> <dbl> <dbl>
## 1 1 Healthy 586 906
## 2 2 Healthy 608 773
## 3 3 Healthy 609 887
## 4 4 Healthy 621 775
## 5 5 Healthy 642 699
## 6 6 Healthy 593 707
## 7 7 Healthy 616 850
## 8 8 Healthy 721 763
## 9 9 Healthy 631 760
## 10 10 Healthy 639 740
## # … with 70 more rows
The next thing we need to do is convert the Participant
variable into a factor. We can do this with the following code:
# Convert "Participant into factor
stroopMixed$Participant = factor(stroopMixed$Participant)
Once you have done this, you need to convert the Group
variable into a factor. We’ll let you do this yourself using your own
code:
# *** ENTER YOUR OWN CODE HERE TO CONVERT "GROUP" INTO A FACTOR ***
# Convert "Group" into factor
stroopMixed$Group = factor(stroopMixed$Group, levels = c("Healthy","Schizophrenia"))
The next thing we need to do is convert our data from wide format
into long format. Richard discussed the distinction between these two
formats in his Week 4 lab session and we covered it again in our Week 5
lab session. In brief, we need to create a new variable called
TrialType
that contains the different levels of this
factor, congruent and incongruent, which are currently located in
different columns. We can do this using the following piece of code
which users the gather()
function encountered
previously:
# Convert data into long format
stroopMixedLong = gather(stroopMixed,TrialType,RT,Congruent:Incongruent,factor_key = TRUE)
Let’s unpack what this is doing. The first argument to
gather()
, stroopMixed
, is the name of our
current data set in wide format. The second argument tells R we want a
new column called TrialType
. The third argument,
RT
, tells R the name of our dependent variable. The fourth
argument tells R to take the columns Congruent
through to
Incongruent
and combine them into a column labeled
TrialType
(our second argument). The final argument,
factor_key = TRUE
, tells R that we want this new column to
be a factor. The results of this transformation are allocated to a new
data frame called stroopMixedLong
. This contains our data
in long format and we will be using this version of the data set from
henceforth.
Next, we will generate some descriptive statistics (mean, standard deviation, and confidence intervals). You did this for a between-participants factorial design in last week’s seminar, so you can once again generate the code for this yourself.
# *** ENTER YOUR OWN CODE HERE TO GENERATE DESCRIPTIVE STATISTICS ***
If you have executed the code correctly, then you should see the following output:
# Get descriptive statistics
mixedDescriptives = stroopMixedLong %>%
# Organise the output by the "Group" and "TrialType" factors
group_by(Group, TrialType) %>%
# Request means, standard deviations, and confidence intervals
get_summary_stats(RT, show = c("mean","sd","ci"))
# Print the results
(mixedDescriptives)
## # A tibble: 4 x 7
## Group TrialType variable n mean sd ci
## <fct> <fct> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 Healthy Congruent RT 40 623. 39.9 12.7
## 2 Healthy Incongruent RT 40 770. 51.4 16.4
## 3 Schizophrenia Congruent RT 40 625. 50.8 16.2
## 4 Schizophrenia Incongruent RT 40 889. 44.3 14.2
At this stage, we would ordinarily perform various checks including identifying possible outliers and checking that our data satisfy the normality assumption. We won’t perform those checks today, not because they are not important (they most certainly are!), but rather because we don’t have the time to do so. There is one check that we will perform and that is to see if the homogeneity of variance assumption has been violated. In a mixed design like ours, the homogeneity of variance assumption only applies to the between-participants factors, but not the within-participants factors.
Our between-participants factor is Group
(healthy
vs. schizophrenia) but remember that each group receives each level of
our within-participants factor TrialType
(congruent
vs. incongruent). So, we need to test whether the variances are equal or
not for the healthy and schizophrenia groups for congruent and
incongruent trials separately. The following code will generate what we
need:
stroopMixedLong %>%
# Organise the output by the "TrialType" factor
group_by(TrialType) %>%
# Generate Levene's test on the "Group" factor
levene_test(RT ~ Group)
## # A tibble: 2 x 5
## TrialType df1 df2 statistic p
## <fct> <int> <int> <dbl> <dbl>
## 1 Congruent 1 78 3.95 0.0505
## 2 Incongruent 1 78 0.747 0.390
Levene’s test is non-significant for congruent trials, p = .051 (although notice that it is only marginally short of significance), and also for incongruent trials, p = .390. So, we can safely assume that the homogeneity of variance assumption has been satisfied.
To run our ANOVA, we are going to use the anova_test
function from the rstatix
package. This is the
function that I recommend you use for mixed or fully-within participants
factorial designs. The code required to run the ANOVA is given
below:
# Create the mixed design ANOVA model
stroopMixedModel = anova_test(data = stroopMixedLong, dv = RT, wid = Participant, between = Group, within = TrialType, detailed = TRUE)
# Print the model summary
(stroopMixedModel)
## ANOVA Table (type II tests)
##
## Effect DFn DFd SSn SSd F p p<.05 ges
## 1 (Intercept) 1 78 84525386.6 136336.6 48358.096 1.06e-110 * 0.996
## 2 Group 1 78 147440.3 136336.6 84.353 4.74e-14 * 0.301
## 3 TrialType 1 78 1692705.3 205941.9 641.108 2.27e-39 * 0.832
## 4 Group:TrialType 1 78 137886.3 205941.9 52.224 2.91e-10 * 0.287
To create the model, the first argument we supplied to
anova_test
was the name of our data,
stroopMixedLong
. The second argument we supplied was our
dependent variable, RT
. The third argument we supplied was
Participant
, which is the column containing the
individuals/participants identifier. The fourth argument we supplied was
our between-participants factor, Group
. The fourth argument
we supplied was our within-participants factor,
TrialType
.
Notice that the resulting ANOVA table is different in format to those
given in the lecture. The format given in the lecture follows a standard
convention, but anova_test
frustratingly uses a different
format. The main effects of Group
, TrialType
,
and the Group:TrialType
interaction are each given on
separate rows. The row corresponding to each effect contain the
between-group degrees of freedom (DFn), the error degrees of freedom
(DFd), the between-group sums of squares (SSn), the error sums of
squares (SSd), the \(F\) ratio (F), and
the p (p) value. On the bright side, this organisation makes it
easier to locate the correct degrees of freedom when reporting the
different outcomes.
Looking at our ANOVA table, we can see that there is a significant main effect Group, p \(<\) .001. Inspecting our descriptive statistics, we can see that this arose because mean response times are longer for the schizophrenia group than the healthy group. There is also a significant main effect of Trial Type, p \(<\) .001, which arose because mean response times are longer for incongruent than congruent trials. Finally, there is a significant interaction between the two factors, p \(<\) .001. We will defer interpretation of this interaction until later, once we have calculated the simple main effects and plotted the data. By the way, you can just ignore the first row of the table with the outcome named “intercept”.
Before we calculate the simple main effects, notice that the ANOVA
table generated by anova_test
does not give us the
between-group mean squares or the error mean squares that are used to
calculate the \(F\) ratios. This is a
rather silly omission, and I’m not sure why the creators of the
rstatix
package thought it would be wise to exclude these.
Now, you don’t need these values to interpret and report your ANOVA, but
I do need to extract these values, so I can show you which error term is
being used to test the different ANOVA outcomes and the simple main
effects that we will calculate shortly.
So, we are going to calculate the mean squares for ourselves. We will first calculate the between-group mean squares. The between-group mean square is simply the between-group sums of squares (SSn) divided by its corresponding degrees of freedom (DFn). We can calculate this as follows:
# Calculate the between-group mean sum of squares
mixedMeanSquareBetween = stroopMixedModel$SSn/stroopMixedModel$DFn
# Exclude the intercept (row 1 of 4) from the results
mixedMeanSquareBetween[2:4]
## [1] 147440.3 1692705.3 137886.3
The first value is the between-group mean square for the main effect of Group (147440.3), the second value is the between-group mean square for the main effect of Trial Type (1692705.3), and the third value is the between-group mean square for the interaction (137886.3).
Next, we will calculate the error mean squares. The error mean square is simply the error sums of squares (SSd) divided by its corresponding degrees of freedom (DFd). We can calculate this as follows:
# Calculate the error mean sum of squares
mixedMeanSquareError = stroopMixedModel$SSd/stroopMixedModel$DFd
# Exclude the intercept (row 1 of 4) from the results
mixedMeanSquareError[2:4]
## [1] 1747.906 2640.281 2640.281
The first value is the error mean square for the main effect of Group (1747.906), the second value is the error mean square for the main effect of Trial Type (2640.281), and the third value is the error mean square for the interaction (2640.281). Recall that in the lecture, we saw that in a mixed design, the error term for the within-participants factor is used to test both the main effect of that factor and any interaction involving that factor. Sure enough, we can see that the error term for testing the main effect of our within-participants factor, Trial Type, is the same as the one used to test the interaction (i.e., 2640.281).
Remember that the \(F\) ratio for each outcome is calculated by dividing its between-group mean square by its error mean square. So, let’s calculate the \(F\) ratios for ourselves and check they align with what has been given to us in our ANOVA table. We can do that with the following code:
# Calculate F ratios
mixedFRatios = mixedMeanSquareBetween/mixedMeanSquareError
# Print the results, excluding the intercept
(mixedFRatios[2:4])
## [1] 84.35256 641.10811 52.22411
The first \(F\) ratio is for the main effect of Group (84.353), the second is for the main effect of Trial Type (641.108), and the third is for the interaction (52.224). If you compare these \(F\) values with those given in the ANOVA table, you will see that they are identical.
To be clear, when conducting a mixed ANOVA in the future, you don’t need to go through the steps of calculating the mean squares. I have gone through these steps with you because I need to make it clear what error terms we are using to test the different ANOVA outcomes and our simple main effects.
Since the interaction is significant, we need to calculate the simple
main effects. Last week, when calculating the simple main effects for a
fully-between participants design, we used the
testInteractions()
function in the phia
package. Unfortunately, this package cannot be used with mixed and fully
within-participants designs. Accordingly, I have created a custom
function called simple()
that will compute the simple main
effects for you.
Recall from the lecture that for mixed and within-participants factorial designs, we use an approach to calculating the simple main effects known as the pooled error terms approach. What this means is that the simple main effects of each factor are calculated using the same error term. However, the error term we use will differ depending on the factor for which we are calculating the simple main effects (contrast this approach with that we used for calculating the simple main effects in a fully between-participants design, where we used the same error term to calculate the simple main effects of all factors).
Before we can calculate the simple main effects, there are a few things we need to do. First, we need to store our ANOVA table in a dataframe:
# Get the mixed ANOVA table
mixedAnovaTable = get_anova_table(stroopMixedModel)
Next, we need to calculate the cell totals for each of the four conditions and the number of observations (i.e., scores) in each cell:
# Get cell totals and counts
mixedCellTotals = stroopMixedLong %>%
# Organise the output by the"Group" and "TrialType" factors
group_by(TrialType, Group) %>%
# Request cell totals and number of observations (i.e., scores)
summarise(sum = sum(RT),n = n())
# Print the results
(mixedCellTotals)
## # A tibble: 4 x 4
## # Groups: TrialType [2]
## TrialType Group sum n
## <fct> <fct> <dbl> <int>
## 1 Congruent Healthy 24919 40
## 2 Congruent Schizophrenia 24999 40
## 3 Incongruent Healthy 30799 40
## 4 Incongruent Schizophrenia 35576 40
Then, we need to specify which simple main effects we want to generate. We are first going to calculate the simple main effects of the factor Group at Trial Type. This means, we are going to:
To do this, we need to declare Group as the “fixed” factor (we are always comparing the healthy and schizophrenia groups) and Group as the “across” factor (the comparison between the healthy and schizophrenia groups occurs “across” the congruent and incongruent levels of the Trial Type factor):
# Create "fixed" and "across" factors
fixed = "Group"
across = "TrialType"
To get the simple main effects, we pass the cell totals, ANOVA table,
and the fixed and across factors to our function
simple()
:
# Simple main effects of Group at Trial Type
mixedSmeGroup = simple(mixedCellTotals,mixedAnovaTable,fixed,across)
(mixedSmeGroup)
## Levels Sum of Squares Degrees of Freedom Mean Square F
## 1 Congruent 80.0 1 80.000 0.0457690620606991
## 2 Incongruent 285246.6 1 285246.612 163.193373876457
## 3 Error term 136336.6 78 1747.906
## P
## 1 0.831154407738753
## 2 8.24544582814774e-21
## 3
As described in the lecture, to calculate the simple main effects of a given factor in a mixed design, we use the mean square error for the main effect of that factor from the original ANOVA table as the error term. The mean square error in the simple main effects table is given in column four (Mean Square) of row three (Error term). You will notice that this is the error mean square for the main effect of Group that we calculated earlier from our initial ANOVA table (i.e., 1747.906). If you divide the between-group mean squares for the effect of Group at congruent and incongruent trials by this value, it will give you the \(F\) ratios shown in the simple main effects table.
Looking at the simple main effects, we can see that the simple main effect of Group at congruent trials is not significant, \(p\) = 0.831. This indicates that mean response times for congruent trials did not differ between the healthy and schizophrenia groups. However, the simple main effect of Group at incongruent trials is significant, \(p\) < .001. Looking at the descriptive statistics we generated earlier, we can see that this is because mean response times are longer on incongruent trials for the schizophrenia group than the healthy group.
Next, we are going to calculate the simple main effects of the factor Trial Type at Group. This means, we are going to:
To do this, we now need to declare Trial Type as the “fixed” factor and Group as the “across” factor:
# Create "fixed" and "across" factors
fixed = "TrialType"
across = "Group"
We then generate the simple main effects of Trial Type with the following:
# Simple main effects of Trial Type at Group
mixedSmeTrialType = simple(mixedCellTotals,mixedAnovaTable,fixed,across)
(mixedSmeTrialType)
## Levels Sum of Squares Degrees of Freedom Mean Square F
## 1 Healthy 432180.0 1 432180.000 163.687146541067
## 2 Schizophrenia 1398411.6 1 1398411.612 529.645070433654
## 3 Error term 205941.9 78 2640.281
## P
## 1 7.60975594750997e-21
## 2 1.63259229512973e-36
## 3
Remember, to calculate the simple main effects of a given factor in a mixed design, we use the mean square error for the main effect of that factor from the original ANOVA table as the error term. The mean square error in the simple main effects table is given in column four (Mean Square) of row three (Error term). You will notice that this is the error mean square for the main effect of Trial Type that we calculated earlier from our initial ANOVA table (i.e., 2640.281). If you divide the between-group mean squares for the effect of Trial Type at healthy and schizophrenia groups by this value, it will give you the \(F\) ratios shown in the simple main effects table.
Looking at the simple main effects, we can see that the simple main effect of Trial Type at healthy is significant, \(p\) \(<\) .001. Inspecting our descriptive statistics, we can see that this is because for the healthy group, mean response times are longer for incongruent than congruent trials. The simple main effect of Trial Type at schizophrenia is also significant, \(p\) \(<\) .001. Looking at our descriptive statistics, we can see that this is because for the schizophrenia group, mean response times are also longer for incongruent than congruent trials. Thus, both the healthy and schizophrenia groups show a Stroop effect (longer response times for incongruent than congruent trials), but the effect is larger for the schizophrenia group, which has a much smaller \(p\) value (and a correspondingly larger \(F\) ratio).
Figure 1 shows mean response times as a function of the group and trial type manipulations. These data were subjected to a 2 (group: healthy vs. schizophrenia) \(\times\) 2 (trial type: congruent vs. incongruent) mixed Analysis of Variance. There was a significant main effect of group, F(1, 78) = 84.35, p \(<\) .001, with longer response times in the schizophrenia group than the healthy group, a significant main effect of trial type, F(1, 78) = 641.11, p < .001, with longer response times for incongruent than congruent trials, and a significant interaction between the two factors, F(1, 78) = 52.22, p < .001.
To scrutinise the interaction, a simple main effects analysis was undertaken. For the simple main effects of group at trial type, response times on congruent trials did not differ significantly between the healthy and schizophrenia groups, F(1, 78) = .05, p = .831, whereas response times were significantly longer on incongruent trials for the schizophrenia group compared to the healthy group, F(1, 78) = 163.19, p \(<\) .001. For the simple main effects of trial type at group, response times were significantly longer on incongruent trials than congruent trials for the healthy group, F(1, 78) = 163.69, p \(<\) .001, and for the schizophrenia group, F(1, 78) = 529.65, p \(<\) .001, although the effect was larger for the schizophrenia group.
Hence, the interaction arose because the schizophrenia group demonstrated a larger Stroop effect than the healthy group and this was due to longer response times on incongruent, but not congruent, trials.
We turn now to an analysis of the fully within-participant Stroop experiment described in the lecture. In this experiment, a researcher wants to examine whether the magnitude of the Stroop effect decreases with practice at the task. The Stroop task is administered over three successive blocks of trials and the expectation is that the magnitude of the Stroop effect will decrease gradually across blocks. She administers a multi-trial Stroop task to a single group of participants in a 2 (trial type: congruent vs. incongruent) \(\times\) 3 (block: block 1 vs. block 2 vs. block 3) fully within-participants design.
The data set contains seven columns:
Participant: represents the participant number, which ranges from 1–40, with \(N\) = 40 participants.
Congruent_Block1: represents the mean response time, averaged across trials, for congruent trials in the first block of trials.
Congruent_Block2: represents the mean response time, averaged across trials, for congruent trials in the second block of trials.
Congruent_Block3: represents the mean response time, averaged across trials, for congruent trials in the third block of trials.
Incongruent_Block1: represents the mean response time, averaged across trials, for incongruent trials in the first block of trials.
Incongruent_Block2: represents the mean response time, averaged across trials, for incongruent trials in the second block of trials.
Incongruent_Block3: represents the mean response time, averaged across trials, for incongruent trials in the third block of trials.
There’s a bit to get through in this analysis, so I am going to supply the code for everything that follows. We begin, as always, by loading our data set:
# Import data
stroopWithin = read_csv("StroopWithin.csv")
(stroopWithin)
## # A tibble: 40 x 7
## Participant Congruent_Block1 Congruent_Block2 Congruent_Block3
## <dbl> <dbl> <dbl> <dbl>
## 1 1 627 687 563
## 2 2 558 681 674
## 3 3 635 709 575
## 4 4 626 673 663
## 5 5 610 610 643
## 6 6 568 655 715
## 7 7 635 641 573
## 8 8 530 677 545
## 9 9 613 621 638
## 10 10 542 607 636
## # … with 30 more rows, and 3 more variables: Incongruent_Block1 <dbl>,
## # Incongruent_Block2 <dbl>, Incongruent_Block3 <dbl>
As is the case for a fully within-participants design, the data are
in entirely wide format and we need to get them into long format for the
analysis. We are going to start by grouping the columns
Congruent_Block1
through to Incongruent_Block3
into a new variable called Group
using the
gather
function used previously:
# Convert data into long format
stroopWithinLong = stroopWithin %>%
gather(Group,RT,Congruent_Block1:Incongruent_Block3,factor_key = TRUE)
(stroopWithinLong)
## # A tibble: 240 x 3
## Participant Group RT
## <dbl> <fct> <dbl>
## 1 1 Congruent_Block1 627
## 2 2 Congruent_Block1 558
## 3 3 Congruent_Block1 635
## 4 4 Congruent_Block1 626
## 5 5 Congruent_Block1 610
## 6 6 Congruent_Block1 568
## 7 7 Congruent_Block1 635
## 8 8 Congruent_Block1 530
## 9 9 Congruent_Block1 613
## 10 10 Congruent_Block1 542
## # … with 230 more rows
The first argument to gather()
tells R we want to create
a variable called Group
. The second argument tells R that
RT
is the dependent variable. The third argument,
Congruent_Block1:Incongruent_Block3
, tells R that we want
to bundle the columns Congruent_Block1
through to
Incongruent_Block3
into the new variable,
Group
. The fourth argument, factor_key = TRUE
,
tells R that we want to make this variable a factor. The results of this
transformation have been assigned to a new data frame called
stroopWithinLong
.
Looking at the new data frame we have created, we can see that it is
not exactly what we want. Our new variable Group
actually
contains both of our independent variables. What we want is to separate
these independent variables into two separate columns. We can do that
with the separate
function:
# Divide Group into seperate columns for Trial Type and Block
stroopWithinLongSep = stroopWithinLong %>%
separate(Group, c("TrialType","Block"))
(stroopWithinLongSep)
## # A tibble: 240 x 4
## Participant TrialType Block RT
## <dbl> <chr> <chr> <dbl>
## 1 1 Congruent Block1 627
## 2 2 Congruent Block1 558
## 3 3 Congruent Block1 635
## 4 4 Congruent Block1 626
## 5 5 Congruent Block1 610
## 6 6 Congruent Block1 568
## 7 7 Congruent Block1 635
## 8 8 Congruent Block1 530
## 9 9 Congruent Block1 613
## 10 10 Congruent Block1 542
## # … with 230 more rows
This bit of code tells R to separate the variable Group
into two new variables, one called TrialType
and one called
Block
. The results are stored in a new data frame called
stroopWithinLongSep
, which is the data frame we will be
using henceforth. Looking at this data frame we can see that our
transformation has had the desired effect—we now have two new columns
corresponding to each of our two independent variables.
However, our two new variables are currently stored as characters
(they have the labels <chr>
beneath the variable
names) and we need to convert them to factors. We also need to convert
the Participant variable into a factor. So, let’s do that next:
# Convert variables into factors
stroopWithinLongSep$Participant = factor(stroopWithinLongSep$Participant)
stroopWithinLongSep$TrialType = factor(stroopWithinLongSep$TrialType)
stroopWithinLongSep$Block = factor(stroopWithinLongSep$Block)
Right, now is as good a time as any to get some descriptive statistics:
# Get descriptive statistics
withinDescriptives = stroopWithinLongSep %>%
# Organise the output by the"TrialType" and "Block" factors
group_by(TrialType, Block) %>%
# Request means, standard deviations, and confidence intervals
get_summary_stats(RT, show = c("mean","sd","ci"))
# Print the results
(withinDescriptives)
## # A tibble: 6 x 7
## TrialType Block variable n mean sd ci
## <fct> <fct> <chr> <dbl> <dbl> <dbl> <dbl>
## 1 Congruent Block1 RT 40 615. 48.9 15.6
## 2 Congruent Block2 RT 40 631. 50.8 16.3
## 3 Congruent Block3 RT 40 632. 53.8 17.2
## 4 Incongruent Block1 RT 40 832. 43.7 14.0
## 5 Incongruent Block2 RT 40 723. 41.2 13.2
## 6 Incongruent Block3 RT 40 650. 49.1 15.7
Time is limited, so we are once again going to skip over the usual checks, which you should otherwise always perform.
However, because one of our within-participants factors contains three levels (i.e., Block) there is one check that will be performed when we run the ANOVA. Specifically, this is a check to establish whether the sphericity assumption has been violated. Richard introduced you to this assumption when he discussed single-factor within-participants ANOVA, so I will only describe it briefly. The sphericity assumption states that for a within-participants design with three or more levels, the variance of the difference scores between one pair of levels should not differ significantly from the variances of the difference scores for every other possible pair of levels.
The assumption is tested using Mauchly’s test of sphericity, which is applied to any outcome involving a factor with three or more levels. If the test result is significant, then the assumption has been violated. If this occurs, then we must adopt the Greenhouse-Geisser correction for violations of this assumption.
To run our ANOVA, we are once again going to use the
anova_test
function from the rstatix
package.
The code required to run the ANOVA is given below:
# Create the within-participants design ANOVA model
stroopWithinModel = anova_test(data = stroopWithinLongSep, dv = RT, wid = Participant, within = c(TrialType, Block), detailed = TRUE)
(stroopWithinModel)
## ANOVA Table (type III tests)
##
## $ANOVA
## Effect DFn DFd SSn SSd F p p<.05 ges
## 1 (Intercept) 1 39 111105237.6 77509.9 55903.885 3.54e-63 * 0.995
## 2 TrialType 1 39 713623.2 84476.3 329.457 1.28e-20 * 0.568
## 3 Block 2 78 272694.5 221792.5 47.951 2.63e-14 * 0.335
## 4 TrialType:Block 2 78 403873.6 158005.4 99.687 3.25e-22 * 0.427
##
## $`Mauchly's Test for Sphericity`
## Effect W p p<.05
## 1 Block 0.949 0.368
## 2 TrialType:Block 0.952 0.394
##
## $`Sphericity Corrections`
## Effect GGe DF[GG] p[GG] p[GG]<.05 HFe DF[HF] p[HF]
## 1 Block 0.951 1.9, 74.2 1.01e-13 * 0.999 2, 77.9 2.72e-14
## 2 TrialType:Block 0.954 1.91, 74.44 2.61e-21 * 1.002 2, 78.17 3.25e-22
## p[HF]<.05
## 1 *
## 2 *
To create the model, the first argument we supplied to
anova_test
was the name of our data,
stroopWithinLongSep
. The second argument we supplied was
our dependent variable, RT
. The third argument we supplied
was Participant
, which is the column containing the
individuals/participants identifier. The fourth argument we supplied was
our two within-participants factors, TrialType
and
Block
.
The ANOVA output now contains three tables. The first is the main ANOVA table, which is organised in the same format as that produced for our mixed design example from earlier.
The second table presents the results of Mauchly’s test of sphericity, which has been applied to the factor Block, which has three levels, and the interaction between Trial Type and Block. In both instances, we can see that the test result is non-significant, which means the sphericity assumption has been met.
The third table gives the Greenhouse-Geisser corrected ANOVA table for the main effect of Block and the interaction between Trial Type and Block. Had we violated the sphericity assumption, it is these values that we would report when writing up our ANOVA, instead of the values taken from the original ANOVA table (the main effect of Trial Type would be drawn from the original ANOVA table as it only has two levels, and therefore the sphericity assumption does not apply to this outcome). Because we did not violate the sphericity assumption on this occasion, we can focus solely on the original ANOVA table.
You may at this point be wondering why Mauchly’s test of sphericity
and the Greenhouse-Geisser correction were not produced when we ran the
ANOVA for our mixed design example. The answer is simple; for that
design our within-participants factor only contained two levels, so the
sphericity assumption did not apply. The anova_test
function only generates these tests and corrections when at least one of
the within-participants factors has three or more levels.
Let’s redirect our attention to the original ANOVA table. We can see that there is a significant main effect Trial Type, p \(<\) .001. Looking at our descriptive statistics, we can see that this arose because response times are longer for incongruent than congruent trials. There is also a significant main effect of Block, p \(<\) .001, which arose because response times get quicker across blocks as participants obtain more experience with the task. There is also a significant interaction between the two factors, p \(<\) .001. We will defer interpretation of this interaction until later, once we have calculated the simple main effects and plotted the data.
As for our mixed design example, the ANOVA table does not include the mean square values for the outcomes. Previously, we calculated the between-group and error mean squares for ourselves, and verified that these produced the obtained \(F\) ratios when we divided the former by the latter. This time, we will just generate the error mean squares. Again, this is so I can show you which error term is being used to test each ANOVA outcome and the simple main effects of each factor.
# Calculate the error mean sum of squares
withinMeanSquareError = stroopWithinModel$ANOVA$SSd/stroopWithinModel$ANOVA$DFd
# Exclude the intercept (row 1 of 4) from the results
withinMeanSquareError[2:4]
## [1] 2166.059 2843.493 2025.710
The first value is the error mean square for the main effect of Trial Type (2166.059), the second value is the error mean square for the main effect of Block (2843.493), and the third value is the error mean square for the interaction (2025.710). Recall that in the lecture, we saw that in a fully within-participants design the two main effects and the interaction each have their own error terms, and sure enough we can see that the error term is different for each outcome.
Since our interaction is once again significant, we need to proceed to calculate the simple main effects. Before we can calculate the simple main effects, there are a few things we need to do. First, we need to store our ANOVA table in a dataframe:
# Get ANOVA table
withinAnovaTable = get_anova_table(stroopWithinModel)
Next, we need to calculate the cell totals for each of the six conditions and the number of observations (i.e., scores) in each cell:
# Calculate cell totals and counts
withinCellTotals = stroopWithinLongSep %>%
# Organise the output by the TrialType" and "Block" factors
group_by(TrialType, Block) %>%
# Request cell totals and number of observations (i.e., scores)
summarise(sum = sum(RT),n = n())
# Print the results
(mixedCellTotals)
## # A tibble: 4 x 4
## # Groups: TrialType [2]
## TrialType Group sum n
## <fct> <fct> <dbl> <int>
## 1 Congruent Healthy 24919 40
## 2 Congruent Schizophrenia 24999 40
## 3 Incongruent Healthy 30799 40
## 4 Incongruent Schizophrenia 35576 40
Then, we need to specify which simple main effects we want to generate. We are first going to calculate the simple main effects of the factor Trial Type at Block. This means, we are going to:
Notice that because Trial Type has only two levels, the simple main effects of this factor involve only pairwise comparisons. To generate these simple main effects, we need to declare Trial Type as the “fixed” factor (we are always comparing the congruent and incongruent trials) and Block as the “across” factor (the comparison between the congruent and incongruent trials occurs “across” the Block 1, Block 2, and Block 3 levels of the Block factor):
# Create "fixed" and "across" factors
fixed = "TrialType"
across = "Block"
To get the simple main effects of Trial Type, we pass the cell
totals, ANOVA table, and the fixed and across factors to our function
simple()
:
# Simple main effects of Trial Type at Block
withinSmeTrialType = simple(withinCellTotals,withinAnovaTable,fixed,across)
(withinSmeTrialType)
## Levels Sum of Squares Degrees of Freedom Mean Square F
## 1 Block1 941997.012 1 941997.012 434.889847531902
## 2 Block2 168820.312 1 168820.312 77.9389307918993
## 3 Block3 6679.512 1 6679.512 3.0837169695505
## 4 Error term 84476.296 39 2166.059
## P
## 1 9.3622143165968e-23
## 2 7.71130782774178e-11
## 3 0.0869308381837418
## 4
As described in the lecture, to calculate the simple main effects of a given factor in a within-participants design, we use the mean square error for the main effect of that factor from the original ANOVA table as the error term. The mean square error in the simple main effects table is given in column four (Mean Square) of row three (Error term). You will notice that this is the error mean square for the main effect of Trial Type that we calculated earlier from our initial ANOVA table (i.e., 2166.059). If you divide the between-group mean squares for the effect of Trial Type at Block 1, Block 2, and Block 3 by this value, it will give you the \(F\) ratios given in the simple main effects table.
Looking at the simple main effects, we can see that the simple main effect of Trial type at Block 1 is significant, \(p\) \(<\) .001. Looking at our table of descriptive statistics, we can see that this is because mean response times for incongruent trials were longer than for congruent trials. The simple main effect of Trial Type is also significant at Block 2, \(p\) \(<\) .001, again reflecting longer mean response times for incongruent than congruent trials. However, the simple main effect of Trial Type at Block 3 is not significant, \(p\) = .087. Thus, we obtained a statistically reliable Stroop effect for Blocks 1 and 2, but not for Block 3.
Next, we are going to calculate the simple main effects of the factor Block at Trial Type. This means, we are going to:
Notice that because Block has three levels, the simple main effects of this factor involve more than simple pairwise comparisons. To test these simple main effects, we now need to declare Block as the “fixed” factor and Trial Type as the “across” factor:
# Create "fixed" and "across" factors
fixed = "Block"
across = "TrialType"
We then generate the simple main effects of Block with the following:
# Simple main effects of Block at Trial Type
withinSmeBlock = simple(withinCellTotals,withinAnovaTable,fixed,across)
(withinSmeBlock)
## Levels Sum of Squares Degrees of Freedom Mean Square F
## 1 Congruent 7616.717 2 3808.358 1.33932389146507
## 2 Incongruent 668951.450 2 334475.725 117.628460979246
## 3 Error term 221792.467 78 2843.493
## P
## 1 0.267980807676142
## 2 2.82854524810842e-24
## 3
Remember, to calculate the simple main effects of a given factor in a within-participants design, we use the mean square error for the main effect of that factor from the original ANOVA table as the error term. The mean square error in the simple main effects table is given in column four (Mean Square) of row three (Error term). You will notice that this is the error mean square for the main effect of Block that we calculated earlier from our initial ANOVA table (i.e., 2843.493). If you divide the between-group mean squares for the effect of Block at congruent and incongruent trials by this value, it will give you the \(F\) ratios given in the simple main effects table.
Looking at the simple main effects, we can see that the simple main effect of Block at congruent trials is not significant, \(p\) = .268. Hence, mean response times do not differ reliably across blocks for congruent trials. However, the simple main effect of Block at incongruent trials is significant, \(p\) \(<\) .001. Since there are three levels in the factor Block, this significant simple main effect is like the outcome of a significant single-factor ANOVA with three levels—it tells us that there are differences between the group means, but it does not tell us where they are located. To find out, we need to perform some follow up tests. We have a few options available to us.
Suppose we hypothesised the interaction from the beginning and that we are only interested in the difference between Block 1 and Block 2, and Block 2 and Block 3 for incongruent trials, but we are not interested in the third comparison, which is between Block 1 and Block 3. In this case, we would have specified planned comparisons, because we do not intend to conduct all possible comparisons, only the ones relevant to testing our specific hypotheses. In this case, we could simply run two repeated measures \(t\)-tests, one comparing Block 1 and Block 2 at incongruent trials, and one comparing Block 2 and Block 3 at incongruent trials. Should we apply the Bonferroni correction to these comparisons? One rule of thumb is that if the number of comparisons is one less than the number of levels, i.e., (\(a\)-1) comparisons where \(a\) is the number of levels in the factor, then we do not need to apply the Bonferroni correction. In our case, our simple main effect has three levels and we are conducting two comparisons, which is one less than the number of levels, so we can proceed without using the Bonferroni correction (for those occasions where there are four or more levels, this rule of thumb does not apply and you should definitely use the Bonferroni correction). Alternatively, suppose we did not specify at the outset which comparisons we would perform if the interaction was significant. In this case, we would perform all three comparisons and we would use a post-hoc test, such as the Tukey test.
Four our purposes, we are going to perform planned comparisons. To perform these comparisons, we first need to filter our data so that they only contain responses on incongruent trials (we are excluding responses on congruent trials). The following code gives us what we want:
# Filter the data so they only contain responses on "Incongruent" trials
filter4SME = filter(stroopWithinLongSep, TrialType == "Incongruent")
(filter4SME)
## # A tibble: 120 x 4
## Participant TrialType Block RT
## <fct> <fct> <fct> <dbl>
## 1 1 Incongruent Block1 863
## 2 2 Incongruent Block1 870
## 3 3 Incongruent Block1 794
## 4 4 Incongruent Block1 872
## 5 5 Incongruent Block1 813
## 6 6 Incongruent Block1 856
## 7 7 Incongruent Block1 901
## 8 8 Incongruent Block1 818
## 9 9 Incongruent Block1 799
## 10 10 Incongruent Block1 829
## # … with 110 more rows
The new data frame filter4SME
is a version of our data
in which only congruent trials are included. We can then use the
pariwise_t_test
function that you have used many times to
run our \(t\)-tests (making sure to
specify paired = TRUE
as we want repeated measures
comparisons!):
filter4SME %>%
# Generate the t-tests for the two comparisons of interest
pairwise_t_test(RT ~ Block, paired = TRUE,
# Specify the comparisons we want
comparisons = list(c("Block1","Block2"),c("Block2","Block3")))
## # A tibble: 2 x 10
## .y. group1 group2 n1 n2 statistic df p p.adj p.adj.signif
## * <chr> <chr> <chr> <int> <int> <dbl> <dbl> <dbl> <dbl> <chr>
## 1 RT Block1 Block2 40 40 9.90 39 3.38e-12 6.76e-12 ****
## 2 RT Block2 Block3 40 40 7.03 39 1.96e- 8 1.96e- 8 ****
The comparison between Block 1 and Block 2 is significant, p \(<\) .001, as is the comparison between Block 2 and Block 3, p \(<\) .001. Looking at our descriptive statistics, we can see that this is because mean response times are faster for incongruent trials in Block 2 than in Block 1, and mean response times for incongruent trials are faster in turn in Block 3 than in Block 2. Thus, with practice on the Stroop task, participants responses on incongruent trials get gradually quicker.
Figure 2 shows mean response times as a function of the trial type and block manipulations. These data were subjected to a 2 (trial type: congruent vs. incongruent) \(\times\) 3 (block: block 1 vs. block 2 vs block 3) fully within-participants Analysis of Variance. There was a significant main effect of trial type, F(1, 39) = 329.46, p < .001, with response times being longer for incongruent than congruent trials, a significant main effect of block, F(2, 78) = 47.95, p < .001, with response times getting quicker across blocks, and a significant interaction between the two factors, F(2, 78) = 99.69, p < .001.
To scrutinise the interaction, a simple main effects analysis was undertaken. For the simple main effects of trial type at block, response times were significantly longer on incongruent trials than congruent trials in block 1, F(1, 39) = 434.89, p < .001, and block 2, F(1, 39) = 77.94, p < .001, but not in block 3, F(1, 39) = 3.08, p = .087. Turning to the simple main effects of block at trial type, response times did not differ significantly across blocks for congruent trials, F(2, 78) = 1.34, p = .268, but they did differ significantly across blocks for incongruent trials, F(2, 78) = 117.63, p < .001. Planned comparisons revealed that response times on incongruent trials were faster in block 2 than in block 1, t(39) = 9.90, p < .001, and faster in turn in block 3 than in block 2, t(39) = 7.03, p < .001.
Hence, the interaction arose because the magnitude of the Stroop effect decreased across blocks and this was due to the speeding up of responses on incongruent, but not congruent, trials.
Phew!!! We have covered a lot of ground in today’s session – well done for making it through the exercises. If you should happen to want more, then you could recreate the plots in Figures 1 and 2 by co-opting the code we used to generate line plots in lab session 6.
# jitt is used later to add a small amount of spatial jitter to our lines, data points, and error bars to prevent them from overlapping
jitt = position_dodge(0.1)
# Using the dataframe called descriptives, place "Group" on the x-axis, "mean" on the y-axis, and group the data
# according to the two levels of the "TrialType" factor
ggplot(data = mixedDescriptives, mapping = aes(x = Group, y = mean, group = TrialType)) +
# geom_line produces two lines: one for "congruent" trials and one for "incongruent" trials
geom_line(size = 1, position = jitt, aes(color = TrialType)) +
# geom_point adds mean data points to our lines
geom_point(size = 3, position = jitt, aes(color = TrialType)) +
# geom_errorbar adds error bars to our geom_points - here we are using the standard error
geom_errorbar(position = jitt, mapping = aes(ymin = mean - ci, ymax = mean + ci, color = TrialType), size = 1, width = .1) +
# Here we are manually setting the colour of the lines, data points, and error bars
scale_color_manual(values=c("#355C7D", "#F67280")) +
# Change y-axis lower and upper limits
ylim(500,1000) +
# Manually set the x- and y-axis titles
labs(x = "Group", y = "Mean RT (ms)") +
# Use black and white theme for background
theme_bw() +
# The theme function allows us to set various properties of our figure manually
theme(panel.grid.major = element_blank(), # Removes the major gridlines
panel.grid.minor = element_blank(), # Removes the minor gridlines
legend.box.background = element_rect(colour = "black"), # Adds a black border around the legend
legend.position = "bottom", # Positions the legend at the bottom of the graph
legend.direction = "vertical",
axis.title.x = element_text(size = 14, face = "bold"), # Adjusts font style and size of x-axis label
axis.text.x = element_text(size = 12, color = "black"), # Adjusts size and colour of x-axis text
axis.title.y = element_text(size = 14, face = "bold"), # Adjusts font style and size of y-axis label
axis.text.y = element_text(size = 12, color = "black"), # Adjusts size and colour of y-axis text
legend.title = element_text(size = 14, face = "bold")) # Adjusts font style and size of legend title
ggsave("StroopMixed.png", height = 6, width = 5, dpi = 300)
jitt = position_dodge(0.1)
# Using the dataframe called descriptives, place "Block" on the x-axis, "mean" on the y-axis, and group the data
# according to the two levels of the "TrialType" factor
ggplot(data = withinDescriptives, mapping = aes(x = Block, y = mean, group = TrialType)) +
# geom_line produces two lines: one for "congruent" trials and one for "incongruent" trials
geom_line(size = 1, position = jitt, aes(color = TrialType)) +
# geom_point adds mean data points to our lines
geom_point(size = 3, position = jitt, aes(color = TrialType)) +
# geom_errorbar adds error bars to our geom_points - here we are using the standard error
geom_errorbar(position = jitt, mapping = aes(ymin = mean -ci, ymax = mean + ci, color = TrialType), size = 1, width = .1) +
# Here we are manually setting the colour of the lines, data points, and error bars
scale_color_manual(values=c("#355C7D", "#F67280")) +
# Change y-axis lower and upper limits
ylim(500,850) +
# Manually set the x- and y-axis titles
labs(x = "Block", y = "Mean RT (ms)") +
# Use black and white theme for background
theme_bw() +
# The theme function allows us to set various properties of our figure manually
theme(panel.grid.major = element_blank(), # Removes the major gridlines
panel.grid.minor = element_blank(), # Removes the minor gridlines
legend.box.background = element_rect(colour = "black"), # Adds a black border around the legend
legend.position = "bottom", # Positions the legend at the bottom of the graph
legend.direction = "vertical",
axis.title.x = element_text(size = 14, face = "bold"), # Adjusts font style and size of x-axis label
axis.text.x = element_text(size = 12, color = "black"), # Adjusts size and colour of x-axis text
axis.title.y = element_text(size = 14, face = "bold"), # Adjusts font style and size of y-axis label
axis.text.y = element_text(size = 12, color = "black"), # Adjusts size and colour of y-axis text
legend.title = element_text(size = 14, face = "bold")) # Adjusts font style and size of legend title
ggsave("StroopWithin.png", height = 6, width = 5, dpi = 300)