# adventr: more repeated-measures In adventr: Interactive R Tutorials to Accompany Field (2016), "An Adventure in Statistics"

library(forcats)
library(learnr)
library(tidyverse)

library(BayesFactor)
library(nlme)
library(WRS2)

ci_upp = ggplot2::mean_cl_normal(recall)$ymax ) implant_summary  ### Plotting the data Use the code box below to create an error bar chart with interference on the x-axis and recall on the y-axis. If you feel like it, try to superimpose the raw data. (Hint, you can adapt the code that you used in the adventr_15_rm tutorial.)   implant_plot <- ggplot2::ggplot(implant_tidy, aes(interference, recall)) implant_plot + stat_summary(fun.data = "mean_cl_normal", geom = "pointrange") + geom_point(colour = "#E69F00", position = position_jitter(width = 0.1, height = 0)) + labs(x = "Type of interference", y = "Recoognition (out of 20") + coord_cartesian(ylim = c(0, 10)) + scale_y_continuous(breaks = 0:10) + scale_x_discrete(labels = c("No interference", "Verbal", "Visual")) + #adds more descriptive labels to the x-axis theme_bw()  ## Predictors with several categories ### Fitting the model To fit the model we use the lme() function from nlme, because we are fitting a linear model with a categorical predictor, but we want to also model the dependency between scores. This function is explained in the tutorial adventr_mlm, just to recap it takes the following general form (I've retained only the key options): new_object <- lme(outcome ~ predictors, random = formula, data = tibble, method = "REML", na.action = na.fail) • new_model: an object created that contains information about the model. We can get summary statistics for this model by executing the name of the model. • outcome: the variable that contains the scores for the outcome measure (in this case recall). • predictor: the variable that contains information about to which condition a score belongs (in this case interference). • random: defines the random parts of the model. This takes a formula in the style ~ predictor|context. In this case our context is participants, which is represented by the variable id. If we want only to let intercepts vary by participant we could use ~ 1|id, but in fact we also want to let the effect of interference vary by participant so we'd use ~ interference|id. • tibble: the name of the tibble containing the data (in this case implant_tidy). • method: defines which estimation method is used. By default restricted maximum likelihood is used (REML), but if you want to compare models you should override the default and use maximum likelihood (method = "ML"). • na.action: If you have complete data (as we have here) exclude this option, but if you have missing values (i.e., ‘NA’s in the data frame) then by default the model will fail, so include na.action = na.exclude to exclude cases with missing values. We could, therefore, specify the model as: implant_rs <- nlme::lme(recall ~ interference, random = ~ interference|id, data = implant_tidy, method = "ML") anova(implant_rs) summary(implant_rs) intervals(implant_rs, which = "fixed")  The first line creates an object called implant_rs which predicts recall scores from the effect of intervention but with intercepts and slopes allowed to vary across participants (I appended _rs to the name to remind me it is a random slopes model). We then place this model into the anova() function to get some overall tests of the effect of intervention, and then use summary() to look at the parameters of the model and their t-statistics and p-values and intervals() to get 95% confidence intervals for the fixed effects (hence the which = "fixed"), which in this case are the intercept and the parameters for the effect of intervention. Try executing these commands in the code box.   implant_rs <- nlme::lme(recall ~ interference, random = ~ interference|id, data = implant_tidy, method = "ML") anova(implant_rs) summary(implant_rs) intervals(implant_rs, which = "fixed")  The output provides estimates of the model parameters (the b-values) and the significance of these values. The Y intercept ($b_0$) is 7.00, which is the value of recall when interference = 0. R will have coded the variable interference using dummy coding. It will do this alphabetically (unless you tell it something different), which means that it will have coded 'no_interference' as the base category (because n comes before v in the alphabet). Table 1 shows the scheme that will have been used. Note that 7.00 is the value of the mean in the no_interference condition that we calculated earlier. Condition <- c("No interference", "Verbal interference", "Visual interference") interference_verbal <- c(0, 1, 0) interference_visual <- c(0, 0, 1) knitr::kable(tibble(Condition, interference_verbal, interference_visual), caption = "Table 1: Dummy codes used by R (see text for details)")  quiz( question("How would we interpret the *Value* (-1) for *interferenceverbal*?", answer("The difference between the means of the verbal and no interference conditions is -1, which is not significant.", correct = T), answer("As the value of **interference** increased by 1 standard deviation, recall scores decreased by 1 standard deviation"), answer("Verbal interference explains -1% of the variance in recognition scores"), answer("The difference between the means of the verbal and visual conditions is -1, which is significant."), correct = "Correct - well done!", random_answer_order = TRUE, allow_retry = T ), question("How would we interpret the *Value* (-4.2) for *interferencevisual*?", answer("The difference between the means of the visual and no interference conditions is -4.2, which is significant.", correct = T), answer("As the value of **interference** increased by 1 standard deviation, recall scores decreased by -4.2 standard deviations"), answer("Visual interference explains -4.2% of the variance in recognition scores"), answer("The difference between the means of the verbal and visual conditions is -4.2, which is significant."), correct = "Correct - well done!", random_answer_order = TRUE, allow_retry = T ) )  The confidence intervals for the effect of interference tells us about whether the parameters likely to be 0 (assuming that this is one of the 95% of samples that yields confidence intervals containing the population value). Under this assumption, the true difference between recall scores when verbal interference is used compared to no interference lies between -3.352 and 1.352. This means it could be anything between a negative change to a positive change, or even no change. The p-value suggests that this difference is not significant, p = 0.406. In other words, we can not significantly predict recall scores from whether someone had no interference or verbal interference. Conversely, the true difference between recall scores when visual interference is used compared to no interference lies between -6.841 and -1.559. This means it is likely to be a negative change, and unlikely to be zero. The p-value suggests that this difference is significant, p = 0.011. In other words, we can significantly predict recall scores from whether someone had no interference or visual interference. ## Contrasts ### Assigning contrasts Just like for independent designs, we can use contrast coding by using the contrasts() function. This process was explained in adventr_16. I tend to create variables containing the codes that I want to use, and then assign these variables to the predictor that I want to add contrasts to by using these variables. This method is slightly long-winded but has the advantage that the variable names for the contrasts you create are used in the output. So, it enables you to set names for your contrasts, which make the output more interpretable. Let's imagine that the researchers at JIG:SAW had predicted that recall will be worse (i.e. lower scores) when any kind of interference was sent to participant's ID chips during recall than when no interference was used. We can operationalize these hypotheses as two dummy variables using contrast codes. Condition <- c("No interference", "Verbal interference", "Visual interference") Interference_vs_none <- c(-2, 1, 1) Verbal_vs_visual <- c(0, -1, 1) knitr::kable(tibble(Condition, Interference_vs_none, Verbal_vs_visual), caption = "Table 2: Contrast codes for the implanting data")  The first contrast (Interference_vs_none in Table 2) compares the no interference condition to any condition in which attempts were made to interfere with memories (i.e. the visual and verbal conditions combined), whereas the second (Verbal_vs_visual in Table 2) ignores the no interference conditions (by coding it with 0) and compares the conditions that used verbal and visual interference. These contrasts test these questions: 1. Does having interference (but ignoring if the interference was visual or verbal) sent to your ID chip result in different memory recall than having no interference? 2. Do attempts to implant memories with verbal information lead to different recall than attempts that use visual information? We can implement these contrasts in the same way as in adventr_16. All that we'll be changing is the names of the contrasts and the codes that we use. So, we could execute something like: interference_vs_none <- c(-2, 1, 1) verbal_vs_visual <- c(0, -1, 1) contrasts(implant_tidy$interference) <- cbind(interference_vs_none, verbal_vs_visual)
implant_rs <- nlme::lme(recall ~ interference, random = ~ interference|id, data = implant_tidy, method = "ML")
anova(implant_rs)
summary(implant_rs)
intervals(implant_rs, which = "fixed")


The first and second lines creates variables that contain the contrast codes that we want to use. The fourth line sets these contrasts for the variable interference (this will work only if interference is a factor, which is why we converted it at the start of this tutorial), and the remaining lines repeat the model specification that we previously used. Execute these commands in the code box:



interference_vs_none <- c(-2, 1, 1)
verbal_vs_visual <- c(0, -1, 1)
contrasts(implant_tidy$interference) <- cbind(interference_vs_none, verbal_vs_visual) implant_rs <- nlme::lme(recall ~ interference, random = ~ interference|id, data = implant_tidy, method = "ML") anova(implant_rs) summary(implant_rs) intervals(implant_rs, which = "fixed")  Note that the overall ANOVA doesn't change, but the model parameters do. You should find that the b for the first contrast is proportional to the difference between the average of the verbal and visual group (because the group sizes are equal we can take a straight average,$\frac{6.0 + 2.8}{2} = 4.40$) and the average of the control (7.00), which is$4.4-7.0 = -2.6$. The b is this value divided by the number of groups, 3 ($\frac{-2.6}{3} = -0.87$). The b for the first contrast is proportional to the difference between the means of the verbal and visual groups ($2.8-6.0 = -3.2$. The b is this value divided by the number of groups, 2 ($\frac{-3.2}{2} = -1.60$). ## Optional learning exercise This section is entirely unnecessary other than as a learning exercise to demonstrate how the multilevel model approach to repeated measures designs is a more flexible version of the analysis of variance approach (which, I'll refer to as RM-ANOVA). Skip to the next section if you're not interested. The RM-ANOVA approach is much more commonly taught as a way to compare means that are related (as they are in repeated-measures designs). The RM-ANOVA approach relies on a couple of restrictive assumptions: • Effects are constant across participants. That is, we expect overall levels of the outcome to differ across participants, but not the effect that our experimental manipulation has. In multilevel language this is akin to assuming a random intercept but a fixed slope. In our specific example, overall recall will differ across participants, but the effect of interference on recall will not. • Compound symmetry: it is generally assumed that the covariances between different levels of the repeated-measures variable should be equal. That is, the covariance between the no interference and verbal condition should be the same as between the no interference group and visual condition and the same as the covariance between the visual and verbal condition. Sometimes the less-restrictive assumption of sphericity is made, but let's stick with compound symmetry for now. To analyse the current example using RM-ANOVA, we'd execute (trust me): implant_aov <- aov(recall ~ interference + Error(id/interference), data = implant_tidy) summary(implant_aov)  These values match those in Output 16.8 of the book (which are from IBM SPSS Statistics). To fit the same model using a multilevel approach let's start with the model we already used and then add in the two restrictions listed above. The model we used was created by executing: implant_rs <- nlme::lme(recall ~ interference, random = ~ interference|id, data = implant_tidy, method = "ML") This model allows the effect of interference to vary across participants, whereas RM-ANOVA does not. So, let's restrict the model so that it retains a random intercept (overall recall can vary across participants) but not a random slope (the effect of intervention is constant across participants). We achieve this restriction by changing random = ~ interference|id to random = ~ 1|id (I've also changed the suffix of the model to be _ri for random intercept): implant_ri <- nlme::lme(recall ~ interference, random = ~ 1|id, data = implant_tidy, method = "ML") The second restriction is to assume that covariances across conditions are equal. We can add this restriction by including the statement correlation = corCompSymm(form = ~ interference|id) in the model above. This argument sets the covariance structure to have compound symmetry across the intereference variable (which is nested within participants). I'll change the suffix to this model to _cs (for compound symmetry): implant_cs <- nlme::lme(recall ~ interference, random = ~ 1|id, data = implant_tidy, method = "ML", correlation = corCompSymm(form = ~ interference|id)) We can summarise this model with the anova() function. The code is already in the box below. Remember, all we have done is taken our original multilevel model and applied the two constraints present in the RM-ANOVA approach. Execute this code: implant_cs <- nlme::lme(recall ~ interference, random = ~ 1|id, data = implant_tidy, method = "ML", correlation = corCompSymm(form = ~ interference|id)) anova(implant_cs)  Note that the results match the output from RM-ANOVA, which demonstrates that the RM-ANOVA approach is simply a form of the multilevel approach but with restrictive assumptions applied. ## Robust models The WRS2 package [@RN10205] wraps up a few of the many functions described by Wilcox to perform robust variants of tests [@RN5244]. The functions that compare several dependent means are: • rmanova(y, groups, blocks, tr = 0.2): is a test for dependent trimmed means. • rmmcp(y, groups, blocks, tr = 0.2): post hoc tests for trimmed means These functions have similar arguments: • y: the variable containing the outcome variable (in this case recall). • groups: the variable defining the different treatments (in this case interference). • blocks: the variable defining the variable within which scores are nested (in this case id). • tr = 0.2: determines the level of trim. The default is a 20% trim, which has been shown to perform well, but you could change this to 0.1 (10% trim) or any value up to 0.5. You can omit this argument if you're happy with a 20% trim. Because the functions do not allow us to specify a tibble within which variables are stored, we have to instead place tibble_name$ in front of each variable. For example, instead of entering recognition we must enter implant_tidy$recognition so that the function knows where to find the variable. So, we'd execute: WRS2::rmanova(implant_tidy$recall, implant_tidy$interference, implant_tidy$id, tr = 0.2)
WRS2::rmmcp(implant_tidy$recall, implant_tidy$interference, implant_tidy$id, tr = 0.2)  Try this in the code box (leave out tr = 0.2 if you're happy with a 20% trim, which you should be).   WRS2::rmanova(implant_tidy$recall, implant_tidy$interference, implant_tidy$id)
WRS2::rmmcp(implant_tidy$recall, implant_tidy$interference, implant_tidy$id)  Based on rmanova we'd conclude that the group means were significantly different,$F_t(2, 4) = 9.18, p = 0.032$, with recall scores similar in the no interference and verbal conditions,$\hat{\psi} = 0.67 [-2.46, 3.79], p = 0.244$, but significantly lower in the visual condition compared to both the no-interference,$\hat{\psi} = 4.33 [1.21, 7.46], p = 0.009$, and verbal,$\hat{\psi} = 3.67 [0.544, 6.79], p = 0.012$, conditions. ## Bayesian models ### Bayes factors Like in the previous tutorial (adventr_16) we can use the anovaBF() function from the BayesFactor package. Top recap, it takes the form: object = anovaBF(formula = outcome ~ group_variable, data = tibble, whichModels = "withmain", rscaleFixed = "medium") The function uses default priors that can be specified as a number or as "medium" (the default), "wide", and "ultrawide". These labels correspond to r scale values of 1/2,$^\sqrt{2}/_2\$, and 1. These priors are explained in adventr_11 and also read @RN9309.

The main difference with repeated-measures designs is that we need to add in the effect of the individual (id) and also include this as a random effect by specifying whichRandom = "name_of_id_variable". It's a good idea to save this model into a new object (lets call it implant_bf) because you can do useful things with it (we didn't do this for the rmanova() function, but you could).

r hint_text("Tip: if you are using a tibble (rather than a data frame) then place the tibble's name into the function data.frame(), to convert it to a data frame. This step is necessary because (at the time of writing) the BayesFactor package doesn't accept tibbles.")

For our data (using the default prior) we could execute:

implant_bf <- implant_tidy %>%
BayesFactor::anovaBF(formula = recall ~ interference + id, whichRandom = "id")
implant_bf


(Remember that at the start of the tutorial we converted id and interference into factors. We needed to do this because the Bayes factor package excepts a factor, not a character variable, as the random variable). The first line creates the object implant_bf as described above. Note that the formula includes our participant variable as an additive effect (+ id) and that we have also specified this variable as a random effect (whichRandom = "id"). The final line prints the object for us. Try this in the code box:



implant_bf <- implant_tidy %>%
BayesFactor::anovaBF(formula = recall ~ interference + id, whichRandom = "id")
implant_bf


The value of 2.40 means that the probability of the data given the alternative hypothesis is 2.4 that given the null hypothesis. Put another way, you should shift your belief in the alternative hypothesis relative to the null by a factor of 2.40. There is some evidence, but not strong evidence, for alternative hypothesis.

## Other resources

### Statistics

• The tutorials typically follow examples described in detail in @RN10163, so for most of them there's a thorough account in there. You might also find @RN4832 useful for the R stuff.
• There are free lectures and screen casts on my YouTube channel
• There are free statistical resources on my website www.discoveringstatistics.com