- Last week: Correlation, OLS Regression, Logistic regression
- Today: Moderation effects. 2 x 2 ANOVA
2018-10-26 | disclaimer
ANOVA (again… .)
Assumptions underpinning ANOVA
two-way ANOVA + plot.
After today you should be able to complete the following sections:
Two-way ANOVA
Plots (you could already do this!)
You can complete the entire assignment I !! Easy marks to gain, if you can’t figure something out, move on to the next bit. Three different datasets, so you can really chunk it.
Work through the exercises first, if you have not done so! It will give you insight in how to tackle the assignment.
Questions in discussion board on Blackboard.
Before we move on:
Initially ANOVA was designed for balanced designs: equal N for each factor.
When unbalanced: different ways for calculating Sums of Squares.
If everything is uncorrelated as in A below then it does not matter at all. However, if the predictors correlate then there are several options. More info here and here.
Type I SS (“sequential”): Order-dependent. (Assuming we put A first in the order, then SS[A] = t + u + v + w; SS[B] = x + y; SS[A*B] = z)
Type II SS (“hierarchical”): SS[A] = t + w; SS[B] = x + y; SS[A*B] = z. Adjusts terms for all other terms except higher-order terms including the same predictors.
Type III SS (“marginal”, “orthogonal”): SS[A] = t; SS[B] = x; SS[A*B] = z. This assesses the contribution of each predictor over and above all others.
Pick the type corresponding to your hypothesis! If you have strong theoretical predictions for A and want to control for things then perhaps type I is your choice. (But most of the time the choice is between II and III.).
For the highest order interaction all will give the same answer.
Type II: most powerful when no interaction present. (note different interpretation of main effects when interaction is present.)
Type III: conservative (with regards to main effects).
Can anybody explain what an interaction effect is?
What is an example? And why should we particularly care in psychology?
Moderation = Interaction.
Moderation is not the same as mediation. (More about that soon.)
Multiplying of terms.
Check the relevant assumptions first.
Do you recall what these are? (look back to slides from week 3 if you do not remember)
Often you see these types of plots… .
Have you come across an interaction design in your internship or Ba.-Thesis.
What shape was it?
What other forms can interactions take?
Two-way / Three-way.
From datasets package. (ToothGrowth).
From vignette: The response is the length of odontoblasts (cells responsible for tooth growth) in 60 guinea pigs. Each animal received one of three dose levels of vitamin C (0.5, 1, and 2 mg/day) by one of two delivery methods, (orange juice or ascorbic acid (a form of vitamin C and coded as VC).
Load the data (ToothGrowth) and test the assumptions for a 2 x 3 ANOVA. Go back to your notes from lecture 3 if you need to. (hint: visual checks / homogeneity of variance). You’ll need to convert dose to a factor (as.factor)
What are the solutions, in case of violations?
Check your solution here
Expressions :
+ indicates inclusion of an explanatory variable in the model (not addition);
- indicates deletion of an explanatory variable from the model (not subtraction);
* indicates inclusion of explanatory variables and interactions (not multiplication);
/ indicates nesting of explanatory variables in the model (not division);
| indicates conditioning (not ‘or’), so that y~x | z is read as ‘y as a function of x given z’.
Factorial ANOVA: y~N*P*K
N, P and K are two-level factors to be fitted along with all their interactions
Three-way ANOVA: y~N*P*K - N:P:K
As above, but do not fit the three-way interaction, fit all two-way interactions.
If you are testing a higher order interaction, you need all the lower level effects.
So when testing a three-way interaction, you would include all two-way interactions and all main defects.
setwd("~/Dropbox/Teaching_MRes_Northumbria/Lecture5") require(car)
## Loading required package: car
## Loading required package: carData
ToothGrowth2<-ToothGrowth ToothGrowth2$dose<-as.factor(ToothGrowth$dose) # This ensures you get the same results as beloved SPSS. options(contrasts = c("contr.helmert", "contr.poly")) # change options. # need this for correct SS. - type command ANOVAtwobythree<-Anova(lm(len~supp*dose, data=ToothGrowth2), type = 3)
ANOVAtwobythree
## Anova Table (Type III tests) ## ## Response: len ## Sum Sq Df F value Pr(>F) ## (Intercept) 21236.5 1 1610.393 < 2.2e-16 *** ## supp 205.4 1 15.572 0.0002312 *** ## dose 2426.4 2 92.000 < 2.2e-16 *** ## supp:dose 108.3 2 4.107 0.0218603 * ## Residuals 712.1 54 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# alternative route. Notice that the options are different here! options(contrasts = c("contr.sum","contr.poly")) ANOVAtwobythree_alt<-lm(len~supp*dose, data=ToothGrowth2) summary(ANOVAtwobythree_alt)
## ## Call: ## lm(formula = len ~ supp * dose, data = ToothGrowth2) ## ## Residuals: ## Min 1Q Median 3Q Max ## -8.20 -2.72 -0.27 2.65 8.27 ## ## Coefficients: ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 18.8133 0.4688 40.130 < 2e-16 *** ## supp1 1.8500 0.4688 3.946 0.000231 *** ## dose1 -8.2083 0.6630 -12.381 < 2e-16 *** ## dose2 0.9217 0.6630 1.390 0.170190 ## supp1:dose1 0.7750 0.6630 1.169 0.247568 ## supp1:dose2 1.1150 0.6630 1.682 0.098394 . ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 3.631 on 54 degrees of freedom ## Multiple R-squared: 0.7937, Adjusted R-squared: 0.7746 ## F-statistic: 41.56 on 5 and 54 DF, p-value: < 2.2e-16
drop1(ANOVAtwobythree_alt, .~., test="F")
## Single term deletions ## ## Model: ## len ~ supp * dose ## Df Sum of Sq RSS AIC F value Pr(>F) ## <none> 712.11 160.43 ## supp 1 205.35 917.46 173.64 15.572 0.0002312 *** ## dose 2 2426.43 3138.54 245.43 92.000 < 2.2e-16 *** ## supp:dose 2 108.32 820.43 164.93 4.107 0.0218603 * ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
return_aov will return an ANOVA list from which we can then call objects
# make an ID variable ToothGrowth2$id <- seq(1:60) require(ez) ez <- ezANOVA(data = ToothGrowth2, dv = .(len), wid = .(id), between = .(supp, dose), return_aov = T, detailed = TRUE, type = 3)
ez
## $ANOVA ## Effect DFn DFd SSn SSd F p p<.05 ## 1 (Intercept) 1 54 21236.491 712.106 1610.392970 6.939976e-42 * ## 2 supp 1 54 205.350 712.106 15.571979 2.311828e-04 * ## 3 dose 2 54 2426.434 712.106 91.999965 4.046291e-18 * ## 4 supp:dose 2 54 108.319 712.106 4.106991 2.186027e-02 * ## ges ## 1 0.9675557 ## 2 0.2238254 ## 3 0.7731092 ## 4 0.1320279 ## ## $`Levene's Test for Homogeneity of Variance` ## DFn DFd SSn SSd F p p<.05 ## 1 5 54 38.926 246.053 1.708578 0.1483606 ## ## $aov ## Call: ## aov(formula = formula(aov_formula), data = data) ## ## Terms: ## supp dose supp:dose Residuals ## Sum of Squares 205.350 2426.434 108.319 712.106 ## Deg. of Freedom 1 2 2 54 ## ## Residual standard error: 3.631411 ## Estimated effects may be unbalanced
If you want exports you’ll need the ‘car’ or ‘ez’ objects route.
If you want to make it prettier, you would change the labels in the dataframe (and consider dropping certain columns (e.g., the star notation)).
require(stargazer) stargazer(ANOVAtwobythree, summary = F, type="html", out="ANOVA_2_3.html") stargazer(ez$ANOVA, summary = F, type="html", out="ez_2_3.html")
A two-way ANOVA was conducted to examine the effect of supplement and dose on growth of odontoblasts. There was a significant interaction effect between supplement and dose, F(2,54)= 4.11, p= .022, \(\eta^2_g\)= .13.
You would then further explore the contrasts or plot the results.
TukeyHSD(ez$aov, which="dose")
## Tukey multiple comparisons of means ## 95% family-wise confidence level ## ## Fit: aov(formula = formula(aov_formula), data = data) ## ## $dose ## diff lwr upr p adj ## 1-0.5 9.130 6.362488 11.897512 0.0e+00 ## 2-0.5 15.495 12.727488 18.262512 0.0e+00 ## 2-1 6.365 3.597488 9.132512 2.7e-06
library(multcomp) summary(glht(ez$aov, linfct = mcp(dose = "Tukey")))
## ## Simultaneous Tests for General Linear Hypotheses ## ## Multiple Comparisons of Means: Tukey Contrasts ## ## ## Fit: aov(formula = formula(aov_formula), data = data) ## ## Linear Hypotheses: ## Estimate Std. Error t value Pr(>|t|) ## 1 - 0.5 == 0 9.130 1.148 7.951 <1e-05 *** ## 2 - 0.5 == 0 15.495 1.148 13.493 <1e-05 *** ## 2 - 1 == 0 6.365 1.148 5.543 <1e-05 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## (Adjusted p values reported -- single-step method)
ggpubr makes ggplot2 even easier, and churns out some beautiful graphs.
require(ggpubr) plot_moderation <- ggviolin(ToothGrowth2, x = "dose", y = "len", color = "dose", palette = c("black", "darkblue", "darkred"), add = c("jitter", "boxplot"), shape = "dose") + labs(x = "Condition", y = "Length", color = "Dose", shape = "Dose") plot_moderation + facet_wrap(~supp)
# Jitter could be omitted
require(apaTables) apa.2way.table(supp, dose, len, ToothGrowth2, filename = "toothgrowth ANOVA", table.number = 1, show.conf.interval = T, show.marginal.means = T, landscape = TRUE)
## ## ## Table 1 ## ## Means and standard deviations for len as a function of a 2(supp) X 3(dose) design ## ## M M_95%_CI SD ## dose:0.5 ## supp ## OJ 13.23 [10.04, 16.42] 4.46 ## VC 7.98 [6.02, 9.94] 2.75 ## ## dose:1 ## supp ## OJ 22.70 [19.90, 25.50] 3.91 ## VC 16.77 [14.97, 18.57] 2.52 ## ## dose:2 ## supp ## OJ 26.06 [24.16, 27.96] 2.66 ## VC 26.14 [22.71, 29.57] 4.80 ## ## Note. M and SD represent mean and standard deviation, respectively. ## LL and UL indicate the lower and upper limits of the ## 95% confidence interval for the mean, respectively. ## The confidence interval is a plausible range of population means ## that could have created a sample mean (Cumming, 2014).
Note the singular.ok = T statement.
# Bootstrap CI for ANOVA library(boot) # function to obtain p.values from F-test from the # data p_value_twowayaov <- function(data, indices) { data_boot <- data[indices, ] # allows boot to select sample ANOVAtwobytwo_boot <- Anova(lm(len ~ supp * dose, data = data_boot), contrasts = c("contr.sum", "contr.poly"), type = 3, singular.ok = TRUE) return(ANOVAtwobytwo_boot$`Pr(>F)`[4]) } # bootstrapping with 1000 replications set.seed(1234) results_twowayaov <- boot(data = ToothGrowth2, statistic = p_value_twowayaov, R = 1000)
# view results results_twowayaov
## ## ORDINARY NONPARAMETRIC BOOTSTRAP ## ## ## Call: ## boot(data = ToothGrowth2, statistic = p_value_twowayaov, R = 1000) ## ## ## Bootstrap Statistics : ## original bias std. error ## t1* 0.02186027 0.04918218 0.1434026
89% CI - check here why.
plot(results_twowayaov)
# get 89% confidence interval boot.ci(results_twowayaov, conf = 0.89, type="bca")
## BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS ## Based on 1000 bootstrap replicates ## ## CALL : ## boot.ci(boot.out = results_twowayaov, conf = 0.89, type = "bca") ## ## Intervals : ## Level BCa ## 89% ( 0.0002, 0.6498 ) ## Calculations and Intervals on Original Scale
Three are ways to “automate” this search for interaction, and there are ‘tree’ based models which will uncover interactions.
require(tree) tree <- tree(len ~ ., ToothGrowth) plot(tree) text(tree)
Read about ??party. Amazing package to find patterns in your data. (It does so in a very clever way. )
require(party) ctree <- ctree(len ~ ., ToothGrowth)
Party= Recursive Partitioning of Variance. (underpinning it is permutation testing…)
plot(ctree)
Back to the Salaries dataset.
require(car) salaries<-carData::Salaries require(dplyr) salaries<- salaries %>% mutate(monthly_salary=salary/9) salaries<- salaries %>% mutate(yrs.since.phd_cent= yrs.since.phd -mean(yrs.since.phd)) names(salaries)[5] <- "gender" # renamed
Regression model.
interaction<-lm(monthly_salary~gender*yrs.since.phd_cent,data=salaries) summary(interaction)
## ## Call: ## lm(formula = monthly_salary ~ gender * yrs.since.phd_cent, data = salaries) ## ## Residuals: ## Min 1Q Median 3Q Max ## -9224 -2160 -332 1673 11406 ## ## Coefficients: ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 12503.16 295.75 42.276 < 2e-16 *** ## gender1 -220.27 295.75 -0.745 0.457 ## yrs.since.phd_cent 142.32 26.00 5.473 7.88e-08 *** ## gender1:yrs.since.phd_cent 40.44 26.00 1.555 0.121 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 3047 on 393 degrees of freedom ## Multiple R-squared: 0.1867, Adjusted R-squared: 0.1805 ## F-statistic: 30.07 on 3 and 393 DF, p-value: < 2.2e-16
phia package (can do quick basic plots)
require(phia) means_int <- interactionMeans(interaction) plot(means_int)
Not useful here as only 2 levels, but for model with 3 this is how you would get the post-hoc tests.
testInteractions(interaction)
## F Test: ## P-value adjustment method: holm ## Value Df Sum of Sq F Pr(>F) ## Female-Male -440.53 1 5148409 0.5547 0.4569 ## Residuals 393 3647788770
You can modify this. Alternatively look to ggplot2 (Basically a scatterplot with grouping).
library(effects) plot(allEffects(interaction), multiline=TRUE, ci.style="bands")
Conduct a 2 x 2 ANOVA with gender (or sex if you don’t want to rename…) and yrs.service (center it first).
Make a plot.
We can again do something clever, permutations! More here and the ‘vegan’ manual.
library(vegan) ToothGrowth2<-ToothGrowth attach(ToothGrowth2) # need to attach data # margin corresponds to type III error. adonis2(len~ supp*dose, data = ToothGrowth2, by='margin', method='euclidean')
## Permutation test for adonis under reduced model ## Marginal effects of terms ## Permutation: free ## Number of permutations: 999 ## ## adonis2(formula = len ~ supp * dose, data = ToothGrowth2, method = "euclidean", by = "margin") ## Df SumOfSqs R2 F Pr(>F) ## supp:dose 1 88.9 0.02576 5.3335 0.022 * ## Residual 56 933.6 0.27045 ## Total 59 3452.2 1.00000 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
The following example data (epi.bfi) comes from the psych package (Revelle, 2012), 231 undergraduate students.
bdi = Beck Depression Inventory
epiNeur = Neuroticism from Eysenck Personality Inventory
stateanx = state anxiety
Does the relation between neuroticism (X) and depression (Y) depend on state-anxiety (Z)? (Basic example - consider centering when you run it yourself - easier to interpret reduces multicollinearity issues).
require(psych) interaction2<-lm(bdi ~ stateanx*epiNeur, data=epi.bfi) summary(interaction2)
## ## Call: ## lm(formula = bdi ~ stateanx * epiNeur, data = epi.bfi) ## ## Residuals: ## Min 1Q Median 3Q Max ## -12.0493 -2.2513 -0.4707 2.1135 11.9949 ## ## Coefficients: ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 0.06367 2.18559 0.029 0.9768 ## stateanx 0.03750 0.06062 0.619 0.5368 ## epiNeur -0.14765 0.18869 -0.782 0.4347 ## stateanx:epiNeur 0.01528 0.00466 3.279 0.0012 ** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 4.12 on 227 degrees of freedom ## Multiple R-squared: 0.4978, Adjusted R-squared: 0.4912 ## F-statistic: 75.02 on 3 and 227 DF, p-value: < 2.2e-16
Usually we are interested in shifts of one SD.
library(rockchalk) plotCurves(interaction2, plotx = "epiNeur", modx = "stateanx", modxVals = "std.dev.", interval = "confidence")
require(jtools) sim_slopes(interaction2, pred = epiNeur, modx = stateanx, johnson_neyman = FALSE)
## SIMPLE SLOPES ANALYSIS ## ## Slope of epiNeur when stateanx = 51.33 (+ 1 SD): ## Est. S.E. t val. p ## 0.64 0.09 7.20 0.00 ## ## Slope of epiNeur when stateanx = 39.85 (Mean): ## Est. S.E. t val. p ## 0.46 0.06 7.20 0.00 ## ## Slope of epiNeur when stateanx = 28.36 (- 1 SD): ## Est. S.E. t val. p ## 0.29 0.08 3.66 0.00
The benefit of the J-N interval: it tells you exactly where the predictor’s slope becomes significant/insignificant. No arbitrary cut-offs.
johnson_neyman(interaction2, pred = epiNeur, modx = stateanx)
## JOHNSON-NEYMAN INTERVAL ## ## When stateanx is OUTSIDE the interval [-35.09, 22.25], the slope of ## epiNeur is p < .05. ## ## Note: The range of observed values of stateanx is [21.00, 79.00]
require(sandwich) # Robust estimation. require(lmtest) sim_slopes(interaction2, pred = epiNeur, modx = stateanx, robust = T)
## JOHNSON-NEYMAN INTERVAL ## ## When stateanx is OUTSIDE the interval [-154.05, 22.72], the slope of ## epiNeur is p < .05. ## ## Note: The range of observed values of stateanx is [21.00, 79.00] ## ## SIMPLE SLOPES ANALYSIS ## ## Slope of epiNeur when stateanx = 51.33 (+ 1 SD): ## Est. S.E. t val. p ## 0.64 0.13 4.91 0.00 ## ## Slope of epiNeur when stateanx = 39.85 (Mean): ## Est. S.E. t val. p ## 0.46 0.07 6.35 0.00 ## ## Slope of epiNeur when stateanx = 28.36 (- 1 SD): ## Est. S.E. t val. p ## 0.29 0.08 3.79 0.00
90% CI - tweak everything ??interact_plot. Again, ggplot2 can also do this. Also check ??interplot.
interact_plot(interaction2, pred = "epiNeur", modx = "stateanx", interval = TRUE, int.width = 0.9)
Have you ever conducted a RM-ANOVA design with between-within? What were the within- and between-subject factors?
Did it have an interaction?
If you haven’t conducted one, what would be a scenario where you would test one?
Example: Hypothetical language acquisition study from here. We are interested in children who are raised bilingually, where one of the languages they speak is ‘home only’ and the other language is also used in their school. MLU is mean length of utterance.
library(ez) bilingual <- read.delim("http://coltekin.net/cagri/R/data/bilingual.txt") rm_anov <- ezANOVA(data = bilingual, dv = mlu, wid = .(subj), within = .(language, age), between = gender, type = 3)
## Warning: Converting "subj" to factor for ANOVA.
Different types of error (I,II,III).
Specify errors always to be on safe side.
It is a list, you can call parts with the $ sign.
rm_anov$ANOVA
## Effect DFn DFd F p p<.05 ges ## 2 gender 1 18 2.923713e-04 9.865458e-01 1.023857e-05 ## 3 language 1 18 6.776889e+00 1.797626e-02 * 3.376541e-02 ## 5 age 2 36 1.743440e+01 5.072873e-06 * 1.470202e-01 ## 4 gender:language 1 18 1.977594e-01 6.618365e-01 1.018717e-03 ## 6 gender:age 2 36 6.474809e-01 5.293489e-01 6.360440e-03 ## 7 language:age 2 36 3.070242e+00 5.872916e-02 1.658619e-02 ## 8 gender:language:age 2 36 1.424144e+00 2.539516e-01 7.762601e-03
rm_anov$`Mauchly's Test for Sphericity`
## Effect W p p<.05 ## 5 age 0.9937147 0.9478173 ## 6 gender:age 0.9937147 0.9478173 ## 7 language:age 0.9905139 0.9221786 ## 8 gender:language:age 0.9905139 0.9221786
If significant, then you would opt for sphericity corrected measures.
Greenhouse-Geiser correction / Huyn-Feldt corrections for sphericity.
rm_anov$`Sphericity Corrections`
## Effect GGe p[GG] p[GG]<.05 HFe ## 5 age 0.9937540 5.378113e-06 * 1.116711 ## 6 gender:age 0.9937540 5.284531e-01 1.116711 ## 7 language:age 0.9906031 5.929377e-02 1.112534 ## 8 gender:language:age 0.9906031 2.540382e-01 1.112534 ## p[HF] p[HF]<.05 ## 5 5.072873e-06 * ## 6 5.293489e-01 ## 7 5.872916e-02 ## 8 2.539516e-01
Download the data thechase_R.xlsx . This file contains coding for 50 episodes from ITV’s The Chase (coded by yours Truly). Read the data (use the ‘readxl’ package for example). The codes are largely self-explanatory.
Test the assumptions for a 2 (gender_contestant) x 5 (Opponent) ANOVA on ‘High’ (this is the highest offer made by the chaser).
Conduct the 2 x 5 ANOVA. Calculate the post-hoc contrasts. Report as you would do in a paper.
Make a (beautiful) violin plot.
Conduct a 2 (gender_contestant) x 2 (age) ANOVA on ‘chosen’ (the amount chosen by the contestant.).
Make a plot showing the interaction (see the examples from the slides or check ggplot2).
BONUS: Load the salaries dataset, test the interaction effect for yrs.service x yrs.since.Ph.D. on monthly salary.
Calculate Johnson-Neyman interval range.
BONUS: use vegan to conduct a permutation analysis. for the 2 x 5 ANOVA you conducted above.
BONUS: use the party package to model ‘high’. (Reduce the dataset via ‘dplyr’, keep a sensible number of variables).
Also check the reading list! (many more than listed here)