This document demonstrates a complete Multivariate Analysis of
Variance (MANOVA) using the classic iris
dataset. MANOVA is
used when we have multiple continuous dependent variables and one or
more categorical independent variables. It tests whether there are
differences between groups on a linear combination of dependent
variables.
Do the three iris species (setosa, versicolor, virginica) differ significantly in their overall morphological profile across multiple floral characteristics?
The following R packages are required for this analysis:
Manova()
functionThe iris
dataset contains measurements of 150 iris
flowers from 3 species. Each flower has four morphological
measurements:
# Basic structure
str(iris)
'data.frame': 150 obs. of 5 variables:
$ Sepal.Length: num 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
$ Sepal.Width : num 3.5 3 3.2 3.1 3.6 3.9 3.4 3.4 2.9 3.1 ...
$ Petal.Length: num 1.4 1.4 1.3 1.5 1.4 1.7 1.4 1.5 1.4 1.5 ...
$ Petal.Width : num 0.2 0.2 0.2 0.2 0.2 0.4 0.3 0.2 0.2 0.1 ...
$ Species : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
# Calculate additional summary statistics for inline use
species_counts <- table(iris$Species)
# Summary statistics by species
summary_stats <- iris %>%
group_by(Species) %>%
summarise(
n = n(),
mean_sepal_length = round(mean(Sepal.Length), 2),
mean_sepal_width = round(mean(Sepal.Width), 2),
mean_petal_length = round(mean(Petal.Length), 2),
mean_petal_width = round(mean(Petal.Width), 2),
.groups = 'drop'
)
summary_stats %>%
kable(caption = "Descriptive Statistics by Species")
Species | n | mean_sepal_length | mean_sepal_width | mean_petal_length | mean_petal_width |
---|---|---|---|---|---|
setosa | 50 | 5.01 | 3.43 | 1.46 | 0.25 |
versicolor | 50 | 5.94 | 2.77 | 4.26 | 1.33 |
virginica | 50 | 6.59 | 2.97 | 5.55 | 2.03 |
# Create a pairs plot to visualize relationships
pairs(iris[1:4],
col = iris$Species,
pch = 19,
main = "Iris Measurements by Species",
labels = c("Sepal\nLength", "Sepal\nWidth", "Petal\nLength", "Petal\nWidth"))
# Add legend
legend("top",
legend = levels(iris$Species),
col = 1:3,
pch = 19,
ncol = 3,
inset = c(0, -0.05),
xpd = TRUE)
We fit a MANOVA model using the lm()
function, treating
the four morphological variables as a multivariate response and species
as the independent variable.
# Create the dependent variable matrix
Y <- as.matrix(iris[, 1:4])
colnames(Y) <- c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width")
# Fit the MANOVA model using lm()
# The cbind() function combines multiple dependent variables
manova_model <- lm(cbind(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width) ~ Species,
data = iris)
# Display model summary
summary(manova_model)
Response Sepal.Length :
Call:
lm(formula = Sepal.Length ~ Species, data = iris)
Residuals:
Min 1Q Median 3Q Max
-1.6880 -0.3285 -0.0060 0.3120 1.3120
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 5.0060 0.0728 68.762 < 2e-16 ***
Speciesversicolor 0.9300 0.1030 9.033 8.77e-16 ***
Speciesvirginica 1.5820 0.1030 15.366 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.5148 on 147 degrees of freedom
Multiple R-squared: 0.6187, Adjusted R-squared: 0.6135
F-statistic: 119.3 on 2 and 147 DF, p-value: < 2.2e-16
Response Sepal.Width :
Call:
lm(formula = Sepal.Width ~ Species, data = iris)
Residuals:
Min 1Q Median 3Q Max
-1.128 -0.228 0.026 0.226 0.972
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 3.42800 0.04804 71.359 < 2e-16 ***
Speciesversicolor -0.65800 0.06794 -9.685 < 2e-16 ***
Speciesvirginica -0.45400 0.06794 -6.683 4.54e-10 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.3397 on 147 degrees of freedom
Multiple R-squared: 0.4008, Adjusted R-squared: 0.3926
F-statistic: 49.16 on 2 and 147 DF, p-value: < 2.2e-16
Response Petal.Length :
Call:
lm(formula = Petal.Length ~ Species, data = iris)
Residuals:
Min 1Q Median 3Q Max
-1.260 -0.258 0.038 0.240 1.348
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 1.46200 0.06086 24.02 <2e-16 ***
Speciesversicolor 2.79800 0.08607 32.51 <2e-16 ***
Speciesvirginica 4.09000 0.08607 47.52 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.4303 on 147 degrees of freedom
Multiple R-squared: 0.9414, Adjusted R-squared: 0.9406
F-statistic: 1180 on 2 and 147 DF, p-value: < 2.2e-16
Response Petal.Width :
Call:
lm(formula = Petal.Width ~ Species, data = iris)
Residuals:
Min 1Q Median 3Q Max
-0.626 -0.126 -0.026 0.154 0.474
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 0.24600 0.02894 8.50 1.96e-14 ***
Speciesversicolor 1.08000 0.04093 26.39 < 2e-16 ***
Speciesvirginica 1.78000 0.04093 43.49 < 2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 0.2047 on 147 degrees of freedom
Multiple R-squared: 0.9289, Adjusted R-squared: 0.9279
F-statistic: 960 on 2 and 147 DF, p-value: < 2.2e-16
We use the car::Manova()
function to obtain multivariate
test statistics. By default, it shows only Pillai’s trace, but we can
access all four test statistics by specifying the test parameter or
examining the summary.
# Perform MANOVA using car package - default shows Pillai's trace
manova_results <- Manova(manova_model, type = "III")
# Pillai's trace (default)
pillai_result <- Manova(manova_model, type = "III", test = "Pillai")
af_cat(pillai_result)
Type III MANOVA Tests: Pillai test statistic Df test stat approx F num Df den Df Pr(>F) (Intercept) 1 0.98304 2086.77 4 144 < 2.2e-16 *** Species 2 1.19190 53.47 8 290 < 2.2e-16 *** --- Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Wilks' Lambda
wilks_result <- Manova(manova_model, type = "III", test = "Wilks")
af_cat(wilks_result)
Type III MANOVA Tests: Wilks test statistic Df test stat approx F num Df den Df Pr(>F) (Intercept) 1 0.016959 2086.77 4 144 < 2.2e-16 *** Species 2 0.023439 199.15 8 288 < 2.2e-16 *** --- Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Hotelling-Lawley trace
hotelling_result <- Manova(manova_model, type = "III", test = "Hotelling-Lawley")
af_cat(hotelling_result)
Type III MANOVA Tests: Hotelling-Lawley test statistic Df test stat approx F num Df den Df Pr(>F) (Intercept) 1 57.966 2086.77 4 144 < 2.2e-16 *** Species 2 32.477 580.53 8 286 < 2.2e-16 *** --- Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# Roy's largest root
roy_result <- Manova(manova_model, type = "III", test = "Roy")
af_cat(roy_result)
Type III MANOVA Tests: Roy test statistic Df test stat approx F num Df den Df Pr(>F) (Intercept) 1 57.966 2086.8 4 144 < 2.2e-16 *** Species 2 32.192 1167.0 4 145 < 2.2e-16 *** --- Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# COMPREHENSIVE RESULTS EXTRACTION FOR INLINE USE THROUGHOUT DOCUMENT
output <- capture.output(summary(manova_results))
# Find and extract the test statistic lines
species_start <- grep("Multivariate Tests: Species", output)
test_stat_lines <- output[(species_start + 2):(species_start + 5)]
# Extract values using regex
pillai_value <- as.numeric(sub("Pillai\\s+\\d+\\s+([0-9.]+)\\s+.*", "\\1", test_stat_lines[1]))
wilks_value <- as.numeric(sub("Wilks\\s+\\d+\\s+([0-9.]+)\\s+.*", "\\1", test_stat_lines[2]))
hotelling_value <- as.numeric(sub("Hotelling-Lawley\\s+\\d+\\s+([0-9.]+)\\s+.*", "\\1", test_stat_lines[3]))
roy_value <- as.numeric(sub("Roy\\s+\\d+\\s+([0-9.]+)\\s+.*", "\\1", test_stat_lines[4]))
p_value <- as.numeric(sub(".*<\\s+([0-9.e-]+).*", "\\1", test_stat_lines[1]))
# Univariate ANOVA results
univariate_summary <- summary.aov(manova_model)
# Extract F-statistics and degrees of freedom
f_stats <- sapply(1:4, function(i) {
univariate_summary[[i]][["F value"]][1]
})
df_between <- sapply(1:4, function(i) {
univariate_summary[[i]][["Df"]][1]
})
df_within <- sapply(1:4, function(i) {
univariate_summary[[i]][["Df"]][2]
})
names(f_stats) <- names(df_between) <- names(df_within) <- colnames(Y)
# Effect sizes (eta-squared)
eta_squared <- sapply(1:4, function(i) {
ss_effect <- univariate_summary[[i]][["Sum Sq"]][1]
ss_total <- sum(univariate_summary[[i]][["Sum Sq"]])
ss_effect / ss_total
})
names(eta_squared) <- colnames(Y)
# Store key effect size variables
max_eta_var <- names(which.max(eta_squared))
max_eta_value <- max(eta_squared)
min_eta_var <- names(which.min(eta_squared))
min_eta_value <- min(eta_squared)
# Sample size variables
min_group_size <- min(table(iris$Species))
recommended_min <- n_species * n_vars_check + 10
sample_adequate <- min_group_size > n_vars_check && n_total > recommended_min
# Assumption test results
# Multivariate normality
mshapiro_test <- mshapiro.test(t(Y))
mv_normal <- mshapiro_test$p.value > 0.05
# Box's M test
box_m_result <- boxM(Y, iris$Species)
box_m_stat <- box_m_result$statistic
box_m_p <- box_m_result$p.value
covariance_homogeneous <- box_m_p > 0.05
# Linearity assessment
cor_matrix <- cor(Y)
correlations <- cor_matrix[upper.tri(cor_matrix)]
min_correlation <- min(abs(correlations))
max_correlation <- max(abs(correlations))
linearity_adequate <- min_correlation > 0.1 && max_correlation < 0.9
# Multicollinearity
det_cor <- det(cor_matrix)
multicollinearity_ok <- det_cor > 0.00001
# Outliers
center <- colMeans(Y)
cov_matrix <- cov(Y)
mahal_dist <- mahalanobis(Y, center, cov_matrix)
critical_value <- qchisq(0.99, df = n_vars_check)
outliers <- sum(mahal_dist > critical_value)
outlier_proportion <- outliers / nrow(Y)
outliers_acceptable <- outlier_proportion < 0.05
# Species names for profile plot
species_names <- levels(iris$Species)
The MANOVA results show four different test statistics, each with specific properties:
Pillai’s Trace (also called Pillai-Bartlett Trace) is a multivariate test statistic that measures the proportion of variance in the combination of dependent variables that is explained by the independent variable(s). It’s calculated as the sum of the squared canonical correlations.
Interpretation:
Guidelines:
Key Characteristics:
Wilks’ Lambda represents the proportion of variance in the combination of dependent variables that is NOT explained by the independent variable(s). It’s calculated as the ratio of within-group variance to total variance.
Interpretation:
Guidelines:
Key Characteristics:
Hotelling-Lawley Trace represents the ratio of between-group variance to within-group variance across all discriminant functions. It’s calculated as the sum of the eigenvalues from the hypothesis matrix relative to the error matrix.
Interpretation:
Range: 0 to ∞ (theoretically unbounded) Value near 0: Little to no group differences Larger values: Stronger group differences
Guidelines:
Key Characteristics:
When to Use:
Roy’s Largest Root represents the proportion of variance explained by the first and strongest discriminant function (canonical variate). It focuses only on the largest eigenvalue, capturing the maximum possible separation between groups along one dimension.
Interpretation:
Guidelines:
Key Characteristics:
When to Use:
Important Caveat:
Roy’s Largest Root can be overly optimistic if group differences are actually spread across multiple dimensions. It may detect significance even when other more conservative tests do not, but this doesn’t necessarily mean the effect is stronger - just that it’s concentrated.
Roy’s test is like using a spotlight that only illuminates the brightest part of the pattern, potentially missing subtler but still important differences in other dimensions.
All test statistics are highly significant (p < <0.001), providing strong convergent evidence that iris species differ significantly in their overall morphological profile across the four measurements.
Effect Size Interpretation:
Since the MANOVA is significant, we examine univariate ANOVAs for each dependent variable to understand which specific variables contribute to the group differences.
# Display univariate ANOVA results (values already extracted above)
print(univariate_summary)
Response Sepal.Length :
Df Sum Sq Mean Sq F value Pr(>F)
Species 2 63.212 31.606 119.26 < 2.2e-16 ***
Residuals 147 38.956 0.265
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Response Sepal.Width :
Df Sum Sq Mean Sq F value Pr(>F)
Species 2 11.345 5.6725 49.16 < 2.2e-16 ***
Residuals 147 16.962 0.1154
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Response Petal.Length :
Df Sum Sq Mean Sq F value Pr(>F)
Species 2 437.10 218.551 1180.2 < 2.2e-16 ***
Residuals 147 27.22 0.185
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Response Petal.Width :
Df Sum Sq Mean Sq F value Pr(>F)
Species 2 80.413 40.207 960.01 < 2.2e-16 ***
Residuals 147 6.157 0.042
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
All four morphological variables show statistically significant species differences:
Petal measurements show the largest F-statistics, indicating that petal characteristics are the primary drivers of species differentiation, with petal length showing the strongest discrimination (F = 1180.2).
MANOVA has several important assumptions that must be verified for valid interpretation.
# Display sample size information (values already computed above)
cat("Total sample size:", n_total, "\n")
Total sample size: 150
cat("Recommended minimum (groups × variables + 10):", recommended_min, "\n")
Recommended minimum (groups × variables + 10): 22
Result: Sample size is adequate. Each group (n=50) exceeds the number of variables (4), and total sample size (150) exceeds the recommended minimum (22).
# Display multivariate normality test (values already computed above)
cat("Multivariate Shapiro-Wilk Test:\n")
Multivariate Shapiro-Wilk Test:
Result: Multivariate normality is violated (W = 0.9793, p = 2.342118e-02). However, MANOVA is generally robust to moderate violations of normality, especially with balanced designs and adequate sample sizes like we have here.
# Display Box's M test (values already computed above)
print(box_m_result)
Box's M-test for Homogeneity of Covariance Matrices
data: Y
Chi-Sq (approx.) = 140.94, df = 20, p-value < 2.2e-16
Result: The assumption of equal covariance matrices is violated (χ² = 140.94, p = 3.352034e-20). This suggests that the three species have different patterns of relationships among the morphological variables. However, this violation can be addressed by using Pillai’s trace, which is robust to covariance matrix heterogeneity.
# Display correlation matrix (values already computed above)
print(round(cor_matrix, 3))
Sepal.Length Sepal.Width Petal.Length Petal.Width
Sepal.Length 1.000 -0.118 0.872 0.818
Sepal.Width -0.118 1.000 -0.428 -0.366
Petal.Length 0.872 -0.428 1.000 0.963
Petal.Width 0.818 -0.366 0.963 1.000
cat("\nCorrelation range:", round(min_correlation, 3), "to", round(max_correlation, 3), "\n")
Correlation range: 0.118 to 0.963
cat("Adequate linearity (0.1 < |r| < 0.9):", linearity_adequate, "\n")
Adequate linearity (0.1 < |r| < 0.9): FALSE
Result: Linear relationships appear problematic. Correlations range from 0.118 to 0.963, suggesting possible multicollinearity issues.
# Display multicollinearity assessment (values already computed above)
cat("Correlation matrix determinant:", format(det_cor, scientific = TRUE), "\n")
Correlation matrix determinant: 8.109611e-03
cat("No extreme multicollinearity (det > 0.00001):", multicollinearity_ok, "\n")
No extreme multicollinearity (det > 0.00001): TRUE
Result: No extreme multicollinearity detected. The correlation matrix determinant (8.109611e-03) is well above the critical threshold, indicating that variables provide relatively independent information.
# Display outlier analysis (values already computed above)
cat("Critical value (99th percentile):", round(critical_value, 2), "\n")
Critical value (99th percentile): 13.28
cat("Proportion of outliers:", round(outlier_proportion * 100, 1), "%\n")
Proportion of outliers: 0 %
# Identify outlier cases if any
if(outliers > 0) {
outlier_cases <- which(mahal_dist > critical_value)
cat("Outlier cases:", outlier_cases, "\n")
}
Result: Very few outliers detected (0% of cases), which is within acceptable limits. These cases represent legitimate extreme observations rather than problematic data points.
# Display effect sizes (values already computed above)
cat("Eta-squared (proportion of variance explained by species):\n")
Eta-squared (proportion of variance explained by species):
for(i in 1:4) {
cat(names(eta_squared)[i], ":", round(eta_squared[i], 3),
"(", round(eta_squared[i] * 100, 1), "%)\n")
}
Sepal.Length : 0.619 ( 61.9 %)
Sepal.Width : 0.401 ( 40.1 %)
Petal.Length : 0.941 ( 94.1 %)
Petal.Width : 0.929 ( 92.9 %)
cat("Small effect: η² ≈ 0.01, Medium effect: η² ≈ 0.06, Large effect: η² ≈ 0.14\n")
Small effect: η² ≈ 0.01, Medium effect: η² ≈ 0.06, Large effect: η² ≈ 0.14
Effect Sizes: All variables show large effect sizes,
with species explaining: - Petal.Length: 94.1% of
variance (extremely large effect) - Petal.Width: 92.9%
of variance (extremely large effect)
- Sepal.Length: 61.9% of variance (large effect) -
Sepal.Width: 40.1% of variance (large effect)
These results indicate not only statistical significance but also strong practical significance, with Petal.Length showing the strongest effect (η² = 0.941).
# Create means for profile plot
species_means <- iris %>%
group_by(Species) %>%
summarise(
Sepal.Length = mean(Sepal.Length),
Sepal.Width = mean(Sepal.Width),
Petal.Length = mean(Petal.Length),
Petal.Width = mean(Petal.Width),
.groups = 'drop'
) %>%
tidyr::pivot_longer(cols = -Species, names_to = "Variable", values_to = "Mean")
# Store species names for inline reference
species_names <- levels(iris$Species)
# Create profile plot
ggplot(species_means, aes(x = Variable, y = Mean, color = Species, group = Species)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
theme_minimal() +
labs(
title = "Profile Plot: Mean Morphological Characteristics by Species",
subtitle = "Iris Dataset MANOVA Results",
x = "Morphological Variables",
y = "Mean Measurement (cm)",
color = "Species"
) +
theme(
plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
plot.subtitle = element_text(hjust = 0.5, size = 12),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "bottom"
) +
scale_color_viridis_d(option = "plasma", end = 0.8)
Profile Plot Interpretation: The plot reveals distinct morphological profiles for each species:
The non-parallel lines confirm significant interaction effects and support the MANOVA findings of species differences.
# Display pairwise comparisons
cat("Pairwise t-tests with Bonferroni correction:\n\n")
Pairwise t-tests with Bonferroni correction:
variables <- colnames(Y)
for(var in variables) {
cat("Variable:", var, "\n")
pairwise_result <- pairwise.t.test(iris[[var]], iris$Species,
p.adjust.method = "bonferroni")
print(pairwise_result)
cat("\n")
}
Variable: Sepal.Length
Pairwise comparisons using t tests with pooled SD
data: iris[[var]] and iris$Species
setosa versicolor
versicolor 2.6e-15 -
virginica < 2e-16 8.3e-09
P value adjustment method: bonferroni
Variable: Sepal.Width
Pairwise comparisons using t tests with pooled SD
data: iris[[var]] and iris$Species
setosa versicolor
versicolor < 2e-16 -
virginica 1.4e-09 0.0094
P value adjustment method: bonferroni
Variable: Petal.Length
Pairwise comparisons using t tests with pooled SD
data: iris[[var]] and iris$Species
setosa versicolor
versicolor <2e-16 -
virginica <2e-16 <2e-16
P value adjustment method: bonferroni
Variable: Petal.Width
Pairwise comparisons using t tests with pooled SD
data: iris[[var]] and iris$Species
setosa versicolor
versicolor <2e-16 -
virginica <2e-16 <2e-16
P value adjustment method: bonferroni
# extract and store pairwise p-values in the extract_all_results chunk
pairwise_results <- list()
for(var in colnames(Y)) {
pairwise_results[[var]] <- pairwise.t.test(iris[[var]], iris$Species,
p.adjust.method = "bonferroni")$p.value
}
# Then make dynamic in interpretation
all_significant <- all(sapply(pairwise_results, function(x) all(x < 0.05, na.rm = TRUE)))
Post-hoc Results: Pairwise t-tests with Bonferroni correction show that all comparisons are highly significant (p < 0.001), indicating that each species differs significantly from every other species on the morphological variables.
Primary Finding: MANOVA revealed highly significant differences between iris species in their overall morphological profile (Pillai’s trace = 1.192, p = 2.22e-16).
Univariate Effects: All four morphological variables contribute significantly to species differentiation, with petal length showing the strongest discrimination power (F = 1180.2).
Effect Magnitude: Species differences are not only statistically significant but also practically meaningful, with species explaining 40-94% of variance in morphological traits (large to very large effects).
Assumption Evaluation: While multivariate normality was violated and covariance homogeneity was violated, the analysis remains valid due to MANOVA’s robustness properties, balanced design (n = 50 per group), and adequate sample sizes. The use of Pillai’s trace provides additional protection against assumption violations.
Petal characteristics emerge as the primary drivers of species classification (explaining 94.1% of variance in Petal.Length)
The morphological profiles suggest: - Evolutionary divergence in floral architecture among species - Adaptive specialization potentially related to different pollination strategies - Clear taxonomic boundaries supporting the species-level classification
This analysis demonstrates proper MANOVA methodology: - Comprehensive assumption testing with appropriate interpretation - Use of robust test statistics (Pillai’s trace) when assumptions are violated - Integration of multivariate and univariate perspectives - Assessment of both statistical and practical significance - Clear documentation of analytical decisions and limitations
The significant results, combined with large effect sizes (η² range: 0.401 - 0.941) and biological plausibility, provide compelling evidence for genuine morphological differentiation among iris species rather than statistical artifacts.