Overview
Introduction to ANCOVA
The ANCOVA workflow in R
When to use ANCOVA
Building the ANCOVA model in R
Assumptions of ANCOVA
Interpreting results & “Adjusted Means”
Handling violations (heterogeneous slopes)
What is ANCOVA?
A continuous response variable (Y)
A categorical predictor (Factor A)
A continuous predictor (Covariate X)
The covariate (e.g., body size, initial temperature) explains some of the “noise” (residual error) in the response variable.
By “removing” this variation, we reduce the residual error, making it easier to detect the true effect of our categorical factor.
This results in a more powerful test of treatment effects.
Imagine your treatment groups happen to differ in their average covariate value (e.g., Group A was tested in a warmer room than Group B).
ANCOVA allows you to separate the treatment effect from the covariate effect.
Cricket Chirping Example
lm(y ~ predictor).
model_reg <- lm(y ~ x, data = mydata)model_aov <- lm(y ~ A, data = mydata)model_ancova <- lm(y ~ A + x, data = mydata)model_int <- lm(y ~ A * x, data = mydata)lm(y ~ A + x + A:x, data = mydata)A:x term tests if the slopes are different.Step 1: Test for Homogeneity of Slopes - Always Start Here
Step 2: Examine the Interaction Term
model_int and find the p-value for interaction term (A:x).y ~ A + x) inappropriate**Step 3: Run the ANCOVA (Additive Model)
The ANOVA table for a single-factor ANCOVA has these components: :::{.panel column=“screen”}
# Create a tibble with ANOVA table information
anova_table <- tibble(
Source = c("Factor A (adjusted)", "Covariate", "Residual", "Total"),
df = c("(p-1)", "1", "n-p-1", "n-1"),
`Sum of Squares` = c("SS_A(adj)", "SS_Covariate", "SS_Residual", "SS_Total"),
`Mean Square` = c("MS_A(adj) = SS_A(adj)/(p-1)", "MS_Covariate = SS_Covariate/1",
"MS_Residual = SS_Residual/(n-p-1)", ""),
`F-ratio` = c("MS_A(adj)/MS_Residual", "MS_Covariate/MS_Residual", "", ""),
`Expected MS` = c("σ² + n∑α²/(p-1)", "σ² + β²∑(X-X̄)²", "σ²", "")
)
# Create
anova_table
## # A tibble: 4 × 6
## Source df `Sum of Squares` `Mean Square` `F-ratio` `Expected MS`
## <chr> <chr> <chr> <chr> <chr> <chr>
## 1 Factor A (adjust… (p-1) SS_A(adj) "MS_A(adj) =… "MS_A(ad… "σ² + n∑α²/(…
## 2 Covariate 1 SS_Covariate "MS_Covariat… "MS_Cova… "σ² + β²∑(X-…
## 3 Residual n-p-1 SS_Residual "MS_Residual… "" "σ²"
## 4 Total n-1 SS_Total "" "" "":::
\[H_0: \beta_1 = \beta_2 = ... = \beta_p\]
Are the regression slopes the same for all groups?
Test with the A:x interaction term in lm(y ~ A * x).
\[H_0: \alpha_1 = \alpha_2 = ... = \alpha_p = 0\]
Are the adjusted group means equal?
Test with F = MS_A(adj)/MS_Residual (the ‘A’ term in lm(y ~ A + x)).
\[H_0: \beta = 0\]
Is there a relationship between the covariate and the response?
Test with F = MS_Covariate/MS_Residual (the ‘x’ term in lm(y ~ A + x)).
Parallel vs. Non-Parallel Slopes
ANCOVA on Longevity of Male Fruitflies
# Step 1: Fit model with interaction term
homo_slopes_model <- lm(LONGEV ~ THORAX * treatment, data = partridge)
# Step 2: Test significance of interaction using Type III SS
Anova(homo_slopes_model, type = "III")
## Anova Table (Type III tests)
##
## Response: LONGEV
## Sum Sq Df F value Pr(>F)
## (Intercept) 755.6 1 6.6320 0.01128 *
## THORAX 3486.3 1 30.5999 2.017e-07 ***
## treatment 36.9 4 0.0810 0.98805
## THORAX:treatment 42.5 4 0.0933 0.98441
## Residuals 13102.1 115
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Extract the p-value for interaction
p_interaction <- Anova(homo_slopes_model, type = "III")["THORAX:treatment", "Pr(>F)"]# Step 3: Fit the ANCOVA model (additive model)
ancova_model <- lm(LONGEV ~ THORAX + treatment, data = partridge)
# View the final ANCOVA table
Anova(ancova_model, type = "III")
## Anova Table (Type III tests)
##
## Response: LONGEV
## Sum Sq Df F value Pr(>F)
## (Intercept) 2234.9 1 20.233 1.605e-05 ***
## THORAX 13168.9 1 119.219 < 2.2e-16 ***
## treatment 9611.5 4 21.753 1.719e-13 ***
## Residuals 13144.7 119
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Use emmeans package to get the adjusted means / perform pairwise comparisons
The “estimated marginal mean” (or adjusted mean)
predicted longevity for each group, assuming an average thorax length
This is the “level playing field” comparison.
# Get adjusted means using emmeans
library(emmeans)
adjusted_means <- emmeans(ancova_model, "treatment")
adjusted_means
## treatment emmean SE df lower.CL upper.CL
## No females 65.4 2.11 119 61.3 69.6
## One virgin female daily 61.5 2.11 119 57.3 65.7
## Eight virgin females daily 64.2 2.10 119 60.0 68.3
## One inseminated female daily 54.5 2.11 119 50.3 58.7
## Eight inseminated females daily 41.6 2.12 119 37.4 45.8
##
## Confidence level used: 0.95Interpretation:
The pairs() output gives p-values for all combinations.
The plot shows these comparisons visually. Red arrows connect any groups that are NOT significantly different.
Here, “One virgin female” and “One inseminated female” are not different from each other, but all other comparisons are significant.
# Pairwise comparisons of adjusted means
pairs(adjusted_means, adjust = "tukey")
## contrast estimate SE
## No females - One virgin female daily 3.93 3.00
## No females - Eight virgin females daily 1.28 2.98
## No females - One inseminated female daily 10.95 3.00
## No females - Eight inseminated females daily 23.88 2.97
## One virgin female daily - Eight virgin females daily -2.65 2.98
## One virgin female daily - One inseminated female daily 7.02 2.97
## One virgin female daily - Eight inseminated females daily 19.95 3.01
## Eight virgin females daily - One inseminated female daily 9.67 2.98
## Eight virgin females daily - Eight inseminated females daily 22.60 2.99
## One inseminated female daily - Eight inseminated females daily 12.93 3.01
## df t.ratio p.value
## 119 1.311 0.6849
## 119 0.428 0.9929
## 119 3.650 0.0035
## 119 8.031 <.0001
## 119 -0.891 0.8996
## 119 2.361 0.1336
## 119 6.636 <.0001
## 119 3.249 0.0129
## 119 7.560 <.0001
## 119 4.298 0.0003
##
## P value adjustment: tukey method for comparing a family of 5 estimatesInterpretation:
The pairs() output gives p-values for all combinations.
The plot shows these comparisons visually. Red arrows connect any groups that are NOT significantly different.
Here, “One virgin female” and “One inseminated female” are not different from each other, but all other comparisons are significant.
Visualization Options for ANCOVA
Compared suture widths between treatments
Three groups: high food, low food, initial sample
Covariate: body volume (cube root transformed)
Significant interaction between treatment and covariate
Heterogeneous slopes across treatments
If the interaction term is significant (p < 0.05):
Report interaction! often more interesting biological result than simple mean differences
Analyze slopes: Use emmeans to get slopes for each group.
Johnson-Neyman procedure: advanced method to identify regions of covariate (X-axis) where groups are (and are not) significantly different.
Separate regressions: Analyze each group separately (less powerful).
#| echo: false
# Fit model with interaction
urchin_model <- lm(suture_width ~ volume * treatment, data = urchin_data)
Anova(urchin_model, type = 3)
## Anova Table (Type III tests)
##
## Response: suture_width
## Sum Sq Df F value Pr(>F)
## (Intercept) 0.0005253 1 5.91 0.01778 *
## volume 0.0151663 1 170.64 < 2.2e-16 ***
## treatment 0.0020070 2 11.29 6.064e-05 ***
## volume:treatment 0.0062129 2 34.95 4.453e-11 ***
## Residuals 0.0058662 66
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1Key Assumptions
lm(rank(y) ~ rank(x) + A)# Example of rank-transformed ANCOVA
partridge$rank_LONGEV <- rank(partridge$LONGEV)
partridge$rank_THORAX <- rank(partridge$THORAX)
# Fit ANCOVA on ranked data
rank_ancova <- lm(rank_LONGEV ~
rank_THORAX + treatment,
data = partridge)
Anova(rank_ancova, type = "III")
## Anova Table (Type III tests)
##
## Response: rank_LONGEV
## Sum Sq Df F value Pr(>F)
## (Intercept) 31251 1 63.616 1.045e-12 ***
## rank_THORAX 54314 1 110.564 < 2.2e-16 ***
## treatment 40447 4 20.584 6.536e-13 ***
## Residuals 58458 119
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Compare p-values for treatment effect
cat("P-value (parametric):",
Anova(ancova_model, type="III")["treatment", "Pr(>F)"], "\n")
## P-value (parametric): 1.719359e-13
cat("P-value (rank-based):",
Anova(rank_ancova, type="III")["treatment", "Pr(>F)"])
## P-value (rank-based): 6.536035e-13Note: The results are very similar, which gives us more confidence in our original parametric test.
Scientific Writing Example
Here’s how you might write up ANCOVA results for publication:
“We analyzed the effects of mating strategy on male fruitfly longevity using analysis of covariance (ANCOVA), with thorax length as a covariate. Before conducting the main analysis, we tested the homogeneity of slopes assumption and found no significant interaction between treatment and thorax length (F₄,₁₁₅ = xxx , indicating that the effect of body size on longevity was consistent across treatments.
The ANCOVA revealed significant effects of both treatment (F₄,₁₁₉ = xxxx and thorax length (F₁,₁₁₉ = xxxx, P < 0.001) on longevity. Thorax length was positively associated with longevity…
After adjusting for body size, males with no female partners lived significantly longer (adjusted mean ± SE: xxxx` days) than males in any other treatment group. Pairwise comparisons using Tukey’s HSD test indicated significant differences between most treatment groups (P < 0.05) except between the ‘One virgin female’ and ‘One inseminated female’ groups (P = x).”
lm(y ~ A * x)).lm(y ~ A + x)).car::Anova(model, type = "III") to get the ANOVA table.emmeans() to get adjusted means and pairwise comparisons.Independence of observations
Normal distribution of residuals
Homogeneity of variances
Linearity of relationships within groups
Homogeneity of regression slopes (The most important one!)