[1] "epoch" "mb" "bh" "bl" "nh"
Multivariate Analysis of Variance
Tip
Practical Application: Egyptian Skulls
Hands-on Practice
What kinds of experiments can we run with manova?
t-test
ANOVA
MANOVA
Multivariate Analysis of Variance (MANOVA)
Key Advantage: Controls for Type I error inflation when testing multiple variables
Important
Real-world Applications
Before Running MANOVA: Critical Assumptions to Check!
** Fundamental Requirements**
1 Independence of Observations
2 Multivariate Normality
3 Homogeneity of Covariance
** Additional Considerations**
4 No Extreme Outliers
5 Adequate Sample Size
6 Linear Relationships
Remember: Violating assumptions can lead to invalid conclusions! Always check before interpreting results.
What it means: Each observation should be independent of all other observations
Good Examples:
Problematic Examples:
How to check:
What it means: Each group’s data should follow a multivariate normal distribution across all dependent variables
Checking Methods:
When Violated:
Robustness Note: MANOVA is reasonably robust to normality violations with large samples (n > 30 per group), especially with equal group sizes!
What it means: The covariance matrices should be equal across all groups (Box’s assumption)
Mathematical representation: \[\Sigma_1 = \Sigma_2 = \cdots = \Sigma_k\]
What Equal Covariances Mean:
Testing Methods:
What it means: The covariance matrices of the dependent variables are equal across all groups
Box’s M Test Sensitivity:
Robustness Guidelines:
What it means: Extreme values that could distort the analysis should be identified and addressed
Detection Methods:
Types: - Univariate, Multivariate, Influential
Handling Options:
Rule: Mahalanobis distance > χ²(p, 0.001) = potential outlier
What it means: Sufficient observations per group relative to number of dependent variables
Guidelines:
Our Skulls Dataset:
Small Sample Issues:
Unequal Groups: Keep ratio < 4:1
Linear Relationships:
Ideal Correlations: r = 0.3 to 0.7
Avoid: - r < 0.3: Use separate ANOVAs - r > 0.9: Multicollinearity issues
Most Robust: Pillai’s Trace Equal groups + n ≥ 30 = Better robustness
Use MANOVA when:
Don’t use MANOVA when:
Null Hypothesis (H₀): The population mean vectors are equal across all groups
\[H_0: \boldsymbol{\mu}_1 = \boldsymbol{\mu}_2 = \cdots = \boldsymbol{\mu}_k\]
where \(\boldsymbol{\mu}_i = [\mu_{i1}, \mu_{i2}, \ldots, \mu_{ip}]\) is the mean vector for group i
Alternative Hypothesis (H₁): At least one group’s mean vector differs from the others
\[H_1: \text{Not all } \boldsymbol{\mu}_i \text{ are equal}\]
So, Like, What Does That Mean?
Research Question: Do skull measurements differ across historical epochs?
Dataset Details:
Variables:
mb: Maximum breadthbh: Basibregmatic heightbl: Basialveolar lengthnh: Nasal heightMANOVA Setup:
data("skulls") loads the measurements; names() verifies available columns.epoch identifies the five time periods; mb, bh, bl, and nh are the four responses. epoch mb bh bl nh
c4000BC:30 Min. :119 Min. :120.0 Min. : 81.00 Min. :44.00
c3300BC:30 1st Qu.:131 1st Qu.:129.0 1st Qu.: 93.00 1st Qu.:49.00
c1850BC:30 Median :134 Median :133.0 Median : 96.00 Median :51.00
c200BC :30 Mean :134 Mean :132.5 Mean : 96.46 Mean :50.93
cAD150 :30 3rd Qu.:137 3rd Qu.:136.0 3rd Qu.:100.00 3rd Qu.:53.00
Max. :148 Max. :145.0 Max. :114.00 Max. :60.00
aggregate() function is used to compute the mean of each measurement for each epoch.means contains the average values for each measurement across the five epochs. epoch mb bh bl nh
1 c4000BC 131.3667 133.6000 99.16667 50.53333
2 c3300BC 132.3667 132.7000 99.06667 50.23333
3 c1850BC 134.4667 133.8000 96.03333 50.56667
4 c200BC 135.5000 132.3000 94.53333 51.96667
5 cAD150 136.1667 130.3333 93.50000 51.36667
We note that many of the means seem similar across epochs, but some measurements show more variation than others. This motivates a MANOVA to formally test for differences.
means_long <- means |>
pivot_longer(
cols = c(mb, bh, bl, nh),
names_to = "measurement",
values_to = "mean"
)
ggplot(means_long, aes(x = epoch, y = mean, group = 1, color = epoch)) +
geom_line() +
geom_point(size = 3) +
facet_wrap(~measurement) +
labs(
x = "Epoch",
y = "Mean measurement",
title = "Mean skull measurements across epochs" ) +
theme_minimal()Mean Patterns Across Epochs
group_by(epoch) and summarise() calculate each measurement’s mean within each epoch.pivot_longer() reshapes the summary for ggplot(), which displays measurements side by side.# Calculate means by epoch
means_by_epoch <- skulls %>%
group_by(epoch) %>%
summarise(across(where(is.numeric), mean, na.rm = TRUE))
# Reshape for plotting
means_long <- means_by_epoch %>%
pivot_longer(cols = -epoch, names_to = "measurement", values_to = "mean_value")
# Create a vibrant, colorful plot
ggplot(means_long, aes(x = epoch, y = mean_value, fill = measurement)) +
geom_bar(stat = "identity", position = "dodge", alpha = 0.8) +
scale_fill_manual(values = c("mb" = "#FF6B6B", "bh" = "#4ECDC4",
"bl" = "#45B7D1", "nh" = "#96CEB4"),
labels = c("mb" = "Max Breadth ", "bh" = "Height ",
"bl" = "Length ", "nh" = "Nasal Height ")) +
labs(title = " Mean Skull Measurements by Epoch",
subtitle = "Colorful visualization of Egyptian skull evolution over time ",
x = " Historical Epoch",
y = " Mean Value (mm)",
fill = " Measurements") +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 10),
plot.title = element_text(size = 16, hjust = 0.5, color = "#2C3E50"),
plot.subtitle = element_text(size = 12, hjust = 0.5, color = "#7F8C8D"),
legend.position = "bottom",
panel.grid.major = element_line(color = "#BDC3C7"),
panel.background = element_rect(fill = "#FFFFFF")
)Mean Patterns Across Epochs plot
What do you observe?
Important
What the plot suggests
Key Insight: Visual exploration suggests that skull measurements changed over time , but we need statistical testing to confirm significance!
MANOVA uses multiple test statistics (unlike ANOVA’s single F-statistic):
Wilks’ Lambda (Λ)
Pillai’s Trace
Hotelling-Lawley Trace
Roy’s Greatest Root
Important
Reading the formula
cbind() combines the measurements into one multivariate response.epoch is the categorical predictor; convert it to a factor to make that role explicit.manova() fits one joint model, accounting for relationships among the measurements. Df Wilks approx F num Df den Df Pr(>F)
epoch 4 0.66359 3.9009 16 434.45 7.01e-07 ***
Residuals 145
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Important
Df Pillai approx F num Df den Df Pr(>F)
epoch 4 0.35331 3.512 16 580 4.675e-06 ***
Residuals 145
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Df Hotelling-Lawley approx F num Df den Df Pr(>F)
epoch 4 0.48182 4.231 16 562 8.278e-08 ***
Residuals 145
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Df Roy approx F num Df den Df Pr(>F)
epoch 4 0.4251 15.41 4 145 1.588e-10 ***
Residuals 145
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Note
The skull measurements differ across epochs: For this dataset, the overall test is significant, so reject the null that all epoch mean vectors are equal.
Important
Important
What the omnibus test does not tell us
Next steps: Follow-up analyses needed!
Important
summary.aov() runs a separate follow-up ANOVA for each response measurement. Response mb :
Df Sum Sq Mean Sq F value Pr(>F)
epoch 4 502.83 125.707 5.9546 0.0001826 ***
Residuals 145 3061.07 21.111
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Response bh :
Df Sum Sq Mean Sq F value Pr(>F)
epoch 4 229.9 57.477 2.4474 0.04897 *
Residuals 145 3405.3 23.485
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Response bl :
Df Sum Sq Mean Sq F value Pr(>F)
epoch 4 803.3 200.823 8.3057 4.636e-06 ***
Residuals 145 3506.0 24.179
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Response nh :
Df Sum Sq Mean Sq F value Pr(>F)
epoch 4 61.2 15.300 1.507 0.2032
Residuals 145 1472.1 10.153
Important
What we learned
All four skull measurements show significant epoch differences!
The results show an association between epoch and skull morphology; this historical comparison alone cannot identify the cause of the differences.
Correlation Heatmap, also known as a Correlation Matrix
# Create correlation matrix for skull measurements
skull_cor <- skulls %>%
select(mb, bh, bl, nh) %>%
cor()
# Convert to long format for ggplot
library(reshape2)
skull_cor_long <- melt(skull_cor)
# Create a stunning, colorful heatmap
ggplot(skull_cor_long, aes(Var1, Var2, fill = value)) +
geom_tile(color = "white", size = 1) +
geom_text(aes(label = paste0(round(value, 2),
ifelse(value > 0.6, " ",
ifelse(value > 0.4, " ", " ")))),
color = "white", size = 5, fontface = "bold") +
scale_fill_gradient2(low = "#3498DB", high = "#E74C3C", mid = "#9B59B6",
midpoint = 0.5, limit = c(0,1),
name = " Correlation\nStrength") +
labs(title = "Correlation Matrix of Skull Measurements ",
subtitle = "Exploring relationships between variables",
x = "Variables", y = "Variables") +
theme_minimal() +
theme(
plot.title = element_text(size = 16, hjust = 0.5, color = "#2C3E50"),
plot.subtitle = element_text(size = 12, hjust = 0.5, color = "#7F8C8D"),
axis.text = element_text(size = 12, color = "#2C3E50"),
legend.title = element_text(size = 10),
panel.grid = element_blank(),
axis.ticks = element_blank()
) +
coord_fixed()Correlation Heatmap, also known as a Correlation Matrix
Important
Correlation insights
mb and bl (r = 0.71)bh and nh (r = 0.43)Important
Why this matters for MANOVA
Df Wilks approx F num Df den Df Pr(>F)
as.factor(epoch) 1 0.69703 5.9766 4 55 0.0004564 ***
Residuals 58
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Create vibrant boxplots for each measurement by epoch
skulls_long <- skulls %>%
pivot_longer(cols = c(mb, bh, bl, nh),
names_to = "measurement",
values_to = "value") %>%
mutate(measurement = factor(measurement,
labels = c("bh" = "Height ", "bl" = "Length ",
"mb" = "Max Breadth ", "nh" = "Nasal Height ")))
ggplot(skulls_long, aes(x = epoch, y = value, fill = epoch)) +
geom_boxplot(alpha = 0.8, outlier.color = "red", outlier.size = 2) +
geom_jitter(width = 0.3, alpha = 0.4, size = 0.8, color = "darkblue") +
facet_wrap(~ measurement, scales = "free_y", ncol = 2) +
scale_fill_manual(values = c("c4000BC" = "#FF6B6B", "c3300BC" = "#4ECDC4",
"c1850BC" = "#45B7D1", "c200BC" = "#96CEB4",
"cAD150" = "#FECA57")) +
labs(title = " Distribution of Skull Measurements by Epoch ",
subtitle = "Colorful exploration of measurement variations across time ",
x = " Historical Epoch",
y = " Measurement Value (mm)",
caption = " Boxplots show quartiles, dots show individual measurements") +
theme_minimal() +
theme(
axis.text.x = element_text(angle = 45, hjust = 1, size = 9),
plot.title = element_text(size = 16, hjust = 0.5, color = "#2C3E50"),
plot.subtitle = element_text(size = 12, hjust = 0.5, color = "#7F8C8D"),
plot.caption = element_text(size = 10, color = "#95A5A6"),
legend.position = "none",
strip.text = element_text(size = 12, face = "bold", color = "#34495E"),
panel.grid.minor = element_blank(),
panel.background = element_rect(fill = "#FAFAFA")
)Important
Assumptions to assess
1. Independence of Observations
2. Multivariate Normality
3. Homogeneity of Covariance Matrices
4. No Extreme Outliers
5. Adequate Sample Size
Choosing Test Statistics:
Wilks’ Lambda
Pillai’s Trace
Hotelling-Lawley
Roy’s Greatest Root
Effect sizes (eta-squared) for each variable:
for(i in 1:4) {
aov_summary <- skulls_aov[[i]]
ss_effect <- aov_summary[1, "Sum Sq"] # Sum of squares for epoch
ss_total <- sum(aov_summary[, "Sum Sq"]) # Total sum of squares
eta_sq <- ss_effect / ss_total
var_name <- c("mb", "bh", "bl", "nh")[i]
cat(paste(var_name, ": ", round(eta_sq, 3), "\n"))
}mb : 0.141
bh : 0.063
bl : 0.186
nh : 0.04
Eta-squared (η²) Guidelines:
Our Results Show:
What This Means:
Remember:
Important
Psychology & Education
Marketing & Business
Biology & Medicine
Your Turn: Think of a research question in your field that would benefit from MANOVA!
Important
Running multiple ANOVAs instead of MANOVA
Using MANOVA with uncorrelated variables
Ignoring assumptions
Stopping at significant MANOVA
Important
Try using the following datasets to complete basic manova analyses. Check assumptions, create plots, run the tests, and interpret the results.
mtcars dataset: Compare mpg, hp, and wt across different numbers of cylinders (cyl).iris dataset: Compare Sepal.Length, Sepal.Width, Petal.Length, and Petal.Width across species.ToothGrowth dataset: Compare len, supp, and dose across different supplement types and doses.PlantGrowth dataset: Compare weight, group, and treatment across different treatment groups.penguins dataset (from the palmerpenguins package): Compare bill length, bill depth, and flipper length across species and islands.gapminder dataset (from the gapminder package): Compare life expectancy, GDP per capita, and population across continents and years.Use these reproducible examples to compare the skulls analysis with simulated data that has a known group effect.
# Example 1: the four skull measurements across five epochs
data("skulls", package = "HSAUR2")
skulls$epoch <- factor(skulls$epoch)
skull_fit <- manova(cbind(mb, bh, bl, nh) ~ epoch, data = skulls)
summary(skull_fit, test = "Pillai")
summary.aov(skull_fit)
# Example 2: reproducible data with three groups and three outcomes
set.seed(42)
n <- 30
practice <- data.frame(
group = factor(rep(c("A", "B", "C"), each = n)),
measure1 = c(rnorm(n, 0, 1), rnorm(n, 0.8, 1), rnorm(n, 1.4, 1)),
measure2 = c(rnorm(n, 0, 1), rnorm(n, 0.6, 1), rnorm(n, 1.2, 1)),
measure3 = c(rnorm(n, 0, 1), rnorm(n, 0.2, 1), rnorm(n, 0.4, 1))
)
practice_fit <- manova(cbind(measure1, measure2, measure3) ~ group,
data = practice)
summary(practice_fit, test = "Pillai")
summary.aov(practice_fit)n, the group means, or the number of responses. What changes in the MANOVA output?Solution check: With set.seed(42), the simulated example has a significant Pillai test (p approximately 4.13e-08). The follow-up ANOVAs are significant for measure1 and measure2, but not measure3 (p approximately 0.111). In the skulls example, a significant omnibus test means at least one epoch mean vector differs; it does not establish that every pair of epochs differs.
manova(cbind(mb, bh, bl, nh) ~ epoch, data = skulls),
what does cbind() specify?
summary.aov(skull_fit) after a significant MANOVA?
Choose the measurements, test statistic, and epoch scope to generate a runnable R example.
What Can You Do With MANOVA?
Think about your own research:
Next Steps:
R Packages:
HSAUR2: Contains skulls dataset and other examplescar: Advanced MANOVA functions and assumption testingMVN: Multivariate normality testingbiotools: Box’s M test for homogeneityOnline Resources:
?manova