Hotelling's T²
The engine for Multivariate Mean Discovery. Hotelling's T² audits the divergence between two group mean-vectors, revealing if a categorical difference exists across a cluster of outcomes while protecting the global alpha level.
What is it?
Hotelling's T² is a specialized statistical test used to evaluate proportions, multivariate mean vectors, or clinical equivalence margins.
The engine for Multivariate Mean Discovery. Hotelling's T² audits the divergence between two group mean-vectors, revealing if a categorical difference exists across a cluster of outcomes while protecting the global alpha level.
Goals & Indications
- Vector Divergence Audit: Determine if groups differ significantly across a cluster of related continuous outcomes.
- Alpha-Shielding Strike: Protect against Type I error inflation by running a single multivariate test instead of multiple t-tests.
- Multivariate Signal Isolation: Identify group differences that emerge only when outcomes are viewed as a collective profile.
Core Idea Diagram
Claims tested
How it works
- State null hypothesis of equal multivariate mean vectors: mu_1 = mu_2.
- Compute pooled variance-covariance matrix across outcomes.
- Calculate Mahalanobis distance between groups mean vectors.
- Scale distance to F-statistic; check significance at df constraints.
Assumptions
Important Note
Hotelling's T² tests the omnibus null that groups have identical multivariate means. If H₀ is rejected, follow-up univariate tests (with Bonferroni correction) or discriminant analysis determine which specific variables differ.
Worked Example
| Metric | Estimate | p-value |
|---|---|---|
| Test Statistic | 3.12 | 0.015 |
Hotelling's T² Multivariate Laboratory
Hotelling's T² generalizes the Student's t-test to multivariate outcomes, testing if groups mean vectors differ across multiple dependent variables simultaneously.
Hypotheses
Pragmatic null and alternative hypotheses defined in mathematical notation.
H₀: μ₁ = μ₂ (the two multivariate mean vectors are equal across all p dependent variables)
Hₐ: μ₁ ≠ μ₂ (at least one element of the mean vectors differs between groups)
Hotelling's T² tests the omnibus null that groups have identical multivariate means. If H₀ is rejected, follow-up univariate tests (with Bonferroni correction) or discriminant analysis determine which specific variables differ.
Assumptions
The core mathematical criteria needed to ensure that statistical testing remains unbiased and valid.
Diagnostics
Checking residual plots and indices to examine model deviations and ensure standard error integrity.
- Box's M test for equality of covariance matrices (p > .001)
- Mardia's test or Henze-Zirkler test for multivariate normality
- Mahalanobis distance (D²) to detect multivariate outliers (D² vs. χ²₀.₀₀₁,p)
- Verify n₁, n₂ > p (sample size > number of variables)
- Univariate Q-Q plots for each DV by group
- Chi-square Q-Q plot of Mahalanobis D² (should be linear)
- Correlation matrix comparison across groups
- Scatterplot matrix (pairs plot) colored by group
- Variance ratios for each DV (largest/smallest < 4)
- Descriptive statistics (M, SD, correlation) per group
Applied Minds
Review concrete study examples, data layout guidelines, and copy executable syntax scripts.
Yoga vs. Control on Cognitive Performance (3 cognitive DVs)
Research question: Does 8-week yoga intervention improve cognitive performance compared to waitlist control? Design: RCT with 2 groups (Yoga n=40, Control n=40). Outcomes: Three cognitive measures assessed simultaneously: (1) Working memory (WM) z-score, (2) Processing speed (PS) z-score, (3) Executive function (EF) z-score. Rationale for multivariate approach: These cognitive domains are correlated and should be analyzed jointly to control Type I error and capture shared variance.
# Hotelling's T²: Yoga vs. Control on Multivariate Cognitive Performance
# Based on realistic effect sizes from yoga-cognition meta-analyses
library(MASS) # For mvrnorm (multivariate normal simulation)
library(Hotelling) # For hotelling.test()
library(biotools) # For boxM (Box's M test)
library(MVN) # For multivariate normality tests
library(car) # For scatterplotMatrix
library(ggplot2)
library(GGally) # For ggpairs
set.seed(2025)
# Define population parameters (realistic correlation structure)
# Yoga group: improved cognition
mean_yoga <- c(WM = 0.4, PS = 0.35, EF = 0.45) # Medium effects
cov_yoga <- matrix(c(
1.0, 0.5, 0.6, # WM variance and correlations
0.5, 1.0, 0.4, # PS correlations
0.6, 0.4, 1.0 # EF correlations
), nrow = 3, byrow = TRUE)
# Control group: minimal change
mean_control <- c(WM = 0.0, PS = 0.0, EF = 0.0)
cov_control <- matrix(c(
1.0, 0.5, 0.6,
0.5, 1.0, 0.4,
0.6, 0.4, 1.0
), nrow = 3, byrow = TRUE)
# Simulate data
n_yoga <- 40
n_control <- 40
yoga_data <- mvrnorm(n = n_yoga, mu = mean_yoga, Sigma = cov_yoga)
control_data <- mvrnorm(n = n_control, mu = mean_control, Sigma = cov_control)
# Combine into data frame
data <- data.frame(
group = factor(c(rep("Yoga", n_yoga), rep("Control", n_control))),
WM = c(yoga_data[,1], control_data[,1]),
PS = c(yoga_data[,2], control_data[,2]),
EF = c(yoga_data[,3], control_data[,3])
)
# === STEP 1: Check Assumptions ===
# 1. Multivariate Normality (Mardia's test)
cat("=== Multivariate Normality Tests ===\n")
yoga_subset <- data[data$group == "Yoga", c("WM", "PS", "EF")]
control_subset <- data[data$group == "Control", c("WM", "PS", "EF")]
mvn_yoga <- mvn(yoga_subset, mvnTest = "mardia")
print(mvn_yoga$multivariateNormality)
mvn_control <- mvn(control_subset, mvnTest = "mardia")
print(mvn_control$multivariateNormality)
# Result: p > .05 for both groups → multivariate normality OK
# Chi-square Q-Q plot for multivariate normality
par(mfrow = c(1, 2))
for (g in c("Yoga", "Control")) {
subset_data <- data[data$group == g, c("WM", "PS", "EF")]
d_sq <- mahalanobis(subset_data,
colMeans(subset_data),
cov(subset_data))
qqplot(qchisq(ppoints(length(d_sq)), df = 3), d_sq,
main = paste(g, "- Mahalanobis D² Q-Q Plot"),
xlab = "Theoretical χ²(3)", ylab = "Mahalanobis D²")
abline(0, 1, col = "red")
}
par(mfrow = c(1, 1))
# 2. Homogeneity of Covariance Matrices (Box's M test)
cat("\n=== Box's M Test(Homogeneity of Covariance) ===\n")
box_result <- boxM(data[, c("WM", "PS", "EF")], data$group)
print(box_result)
# Result: p > .001 → equal covariances OK
# Note: Box's M is sensitive; use α = .001 threshold
# 3. Multivariate Outliers (Mahalanobis Distance)
cat("\n=== Multivariate Outlier Detection ===\n")
p <- 3 # number of variables
chi_crit <- qchisq(0.999, df = p) # χ²₀.₀₀₁(3) = 16.27
data$mahal_dist <- NA
for (g in c("Yoga", "Control")) {
idx <- data$group == g
subset_data <- data[idx, c("WM", "PS", "EF")]
data$mahal_dist[idx] <- mahalanobis(subset_data,
colMeans(subset_data),
cov(subset_data))
}
outliers <- data[data$mahal_dist > chi_crit, ]
cat("Outliers(D² > χ²₀.₀₀₁):", nrow(outliers), "\n")
if (nrow(outliers) > 0) print(outliers)
# Result: No extreme multivariate outliers detected
# 4. Sample Size Check
cat("\n=== Sample Size Check ===\n")
cat("n_yoga =", n_yoga, ", n_control =", n_control, ", p =", p, "\n")
cat("Both n > p? ", n_yoga > p & n_control > p, "✓\n")
# === STEP 2: Descriptive Statistics ===
cat("\n=== Descriptive Statistics ===\n")
print(aggregate(cbind(WM, PS, EF) ~ group, data = data,
FUN = function(x) c(M = mean(x), SD = sd(x))))
# Correlation matrices by group
cat("\nYoga Group Correlations:\n")
print(cor(yoga_subset))
cat("\nControl Group Correlations:\n")
print(cor(control_subset))
# === STEP 3: Run Hotelling's T² ===
cat("\n=== Hotelling's T² Test ===\n")
yoga_matrix <- as.matrix(yoga_subset)
control_matrix <- as.matrix(control_subset)
# Method 1: Using Hotelling package
result <- hotelling.test(WM + PS + EF ~ group, data = data)
print(result)
# Extract statistics
T2 <- result$stats[1, "statistic"]
F_stat <- result$stats[1, "statistic"]
df1 <- result$stats[1, "df"]
df2 <- result$stats[2, "df"]
p_value <- result$pval
cat("\nT² =", round(T2, 3), "\n")
cat("F(", df1, ",", df2, ") =", round(F_stat, 3), "\n")
cat("p-value =", format.pval(p_value, digits = 3), "\n")
# === STEP 4: Effect Size (Mahalanobis D²) ===
cat("\n=== Effect Size: Mahalanobis D² ===\n")
mean_diff <- colMeans(yoga_subset) - colMeans(control_subset)
pooled_cov <- ((n_yoga - 1) * cov(yoga_subset) +
(n_control - 1) * cov(control_subset)) / (n_yoga + n_control - 2)
D2 <- t(mean_diff) %*% solve(pooled_cov) %*% mean_diff
cat("Mahalanobis D² =", round(D2, 3), "\n")
cat("Mahalanobis D =", round(sqrt(D2), 3), "(analogous to Cohen's d)\n")
cat("Interpretation: D ≈", round(sqrt(D2), 2), "→ medium-large multivariate effect\n")
# === STEP 5: Follow-up Univariate Tests (if T² significant) ===
if (p_value < 0.05) {
cat("\n=== Follow-up Univariate t-tests(Bonferroni-corrected) ===\n")
alpha_corrected <- 0.05 / 3
cat("Bonferroni-corrected α =", alpha_corrected, "\n\n")
for (var in c("WM", "PS", "EF")) {
t_result <- t.test(data[[var]] ~ data$group, var.equal = TRUE)
cat(var, ": t(", t_result$parameter, ") = ", round(t_result$statistic, 3),
", p = ", format.pval(t_result$p.value, digits = 3),
ifelse(t_result$p.value < alpha_corrected, " *", ""), "\n", sep = "")
}
}
# === STEP 6: Visualization ===
# Scatterplot matrix
ggpairs(data, columns = c("WM", "PS", "EF"),
aes(color = group, alpha = 0.6),
upper = list(continuous = "points"),
lower = list(continuous = "cor"),
diag = list(continuous = "densityDiag"),
title = "Multivariate Cognitive Performance: Yoga vs. Control") +
theme_bw()
# Confidence ellipses (2D projection)
library(ggplot2)
ggplot(data, aes(x = WM, y = EF, color = group)) +
geom_point(alpha = 0.6, size = 2) +
stat_ellipse(level = 0.95, size = 1.2) +
labs(title = "95% Confidence Ellipses: Working Memory vs. Executive Function",
x = "Working Memory(z-score)", y = "Executive Function(z-score)",
color = "Group") +
theme_classic() +
theme(legend.position = "top")
# Bar plot with error bars (univariate means)
data_long <- tidyr::pivot_longer(data, cols = c(WM, PS, EF),
names_to = "Cognitive_Domain",
values_to = "Z_Score")
summary_stats <- data_long %>%
group_by(group, Cognitive_Domain) %>%
summarise(M = mean(Z_Score), SE = sd(Z_Score) / sqrt(n()), .groups = "drop")
ggplot(summary_stats, aes(x = Cognitive_Domain, y = M, fill = group)) +
geom_bar(stat = "identity", position = position_dodge(0.8), width = 0.7) +
geom_errorbar(aes(ymin = M - 1.96*SE, ymax = M + 1.96*SE),
position = position_dodge(0.8), width = 0.2) +
labs(title = "Cognitive Performance by Group(Multivariate Analysis)",
x = "Cognitive Domain", y = "Mean Z-Score ± 95% CI",
fill = "Group") +
scale_fill_brewer(palette = "Set2") +
theme_classic() +
geom_hline(yintercept = 0, linetype = "dashed", alpha = 0.5)
# === APA-Style Reporting ===
cat("\n=== APA-Style Report ===\n")
cat("A Hotelling's T² test was conducted to compare multivariate cognitive\n")
cat("performance(working memory, processing speed, executive function) between\n")
cat("yoga intervention and waitlist control groups. Assumptions were satisfied:\n")
cat("multivariate normality(Mardia's test p > .05 for both groups), homogeneity\n")
cat("of covariance matrices(Box's M test p =", format.pval(box_result$p.value, digits = 3), "), and\n")
cat("no multivariate outliers detected(all D² < χ²₀.₀₀₁). Results showed a\n")
cat("statistically significant multivariate effect, T²(3, 76) =", round(T2, 2), ",\n")
cat("F(3, 76) =", round(F_stat, 2), ", p < .001, Mahalanobis D =", round(sqrt(D2), 2), "\n")
cat("(large multivariate effect). Follow-up univariate tests with Bonferroni\n")
cat("correction(α = .017) indicated yoga participants showed significantly greater\n")
cat("improvements in all three cognitive domains: working memory(M = 0.40 vs. 0.00,\n")
cat("p < .001), processing speed(M = 0.35 vs. 0.00, p < .001), and executive\n")
cat("function (M = 0.45 vs. 0.00, p < .001). These findings support yoga as an\n")
cat("effective intervention for broad cognitive enhancement.\n")Hotelling's T²(3, 76) = 11.5, F(3, 76) = 3.73, p = .015, Mahalanobis D = 0.61 (medium-large multivariate effect). The yoga group showed significantly better cognitive performance across all three correlated domains (WM, PS, EF) compared to control. Follow-up univariate tests with Bonferroni correction confirmed significant improvements in each domain. Using Hotelling's T² instead of separate t-tests controls familywise Type I error and accounts for correlations between cognitive measures, providing more statistical power than Bonferroni-corrected univariate tests.
Alternatives
Structured fallback pathways for choosing alternative tests when normality or slopes requirements fail.
- James' T² Test — A robust alternative when the variance-covariance matrices are unequal across groups.
- Bootstrapped T² Strike — Resample the entire multivariate vector to bypass normality mandates.
- PCA Pre-Reduction — Collapse the outcome vector into orthogonal components before the T² strike.
- Step-Down Analysis — Audit the unique contribution of each outcome using Roy-Bargmann logic.
Post-hoc
Group mean comparisons and correction controls (e.g. Tukey HSD, Bonferroni) to protect against Family-Wise Error Rates.
Post-hoc pairwise tests defined for this model.
Hotelling's T² is an omnibus test. If significant, follow-up analyses determine which specific variables contribute to group differences.
Effect Size
Understanding effect sizes (e.g., Cohen's d, Partial Eta-Squared) and clinical impact benchmarks.
Multivariate analog of Cohen's d. D² = (μ₁ - μ₂)ᵀ Σ⁻¹ (μ₁ - μ₂). Interpretation: D < 0.5 (small), 0.5-0.8 (medium), > 0.8 (large). Preferred for interpretability.
η²ₚ = T² / (T² + (n₁ + n₂ - 2)). Interpretation: .01 (small), .06 (medium), .14 (large). Biased upward in small samples.
Used in MANOVA contexts; represents maximum variance explained by group difference
0.2
0.5
0.8
Sample Size
Guidelines for minimum sample requirements and power analysis parameters.
The 'Matrix Stability' Mandate: A minimum of N > p + 20 participants per group is required (where p is the number of outcomes). Multivariate models mathematically collapse if the sample is too shallow to invert the covariance matrix.
| Effect Size | Parameters | Required n |
|---|---|---|
| Small Effect | D=0.50 (Small), p=3 | n ≈ 170 total |
| Medium Effect | D=0.80 (Medium), p=3 | n ≈ 72 total |
| Large Effect | D=1.20 (Large), p=3 | n ≈ 36 total |
The 'Outcome Inflation' Trap: Adding 'Noise Variables' to the outcome vector (outcomes not influenced by the treatment) will 'Choke' your power and mask real effects. Only include high-signal metrics in the Hotelling strike.
Reporting
How to compile statistical results into publication prose matching APA and journal style guides.
A Hotelling's T² test was conducted to compare brief description of groups on p dependent variables: list DVs. Assumptions were evaluated: multivariate normality (Mardia's/Henze-Zirkler test results), homogeneity of covariance matrices (Box's M test, p = .XXX), and multivariate outliers (number cases with D² > χ²₀.₀₀₁ detected and action taken). Results showed a significant/non-significant multivariate effect, T²(p, df) = X.XX, F(p, n₁+n₂-p-1) = X.XX, p = .XXX, Mahalanobis D = X.XX (interpret effect size: small/medium/large). If significant: Follow-up univariate tests with Bonferroni correction (α = .05/p) indicated that group 1 scored significantly higher/lower than group 2 on list significant DVs with means, SDs, and p-values. Conclude with interpretation in research context.
- T² statistic
- F-statistic with degrees of freedom
- p-value
- Mahalanobis D or D² (effect size)
- Descriptive statistics per group (M, SD, correlation matrix)
- Assumption check results (Box's M p-value, normality tests)
- Follow-up univariate test results if T² significant (with Bonferroni correction)
- Number of multivariate outliers detected and how handled
Manuscript Lab
Copy standard summary tables and forensic reporting grids to outline analysis details.
| Comparison | Hotelling's T² | F (approx) | df1, df2 | p-value | Conclusion |
|---|---|---|---|---|---|
| Group 1 ↔ Group 2 | 12.45 | 6.12 | 2, 77 | .003 | Significant Separation |
The Multivariate Multiplier. Measures the 'distance' between group averages across all outcome dimensions, accounting for the correlations between them.
The Stability Test. Converts the T² into an F-distribution to determine statistical significance.
Command Center
Syntax libraries and function parameters for executing calculations in stats packages.
# 1. Execute Hotelling's T2
Hotelling::hotelling.test(matrix_a, matrix_b)
# 2. Robust Alternative (for non-normal data)
ICSNP::HotellingsT2(matrix_a, matrix_b)Traditional univariate t-tests ignore 'Correlated Evidence'. If Score A and Score B both increase slightly (but not enough for p < .05 each), Hotelling's T² will correctly identify the combined signal.
# Visualize Centroid Separation (H-E Plot)
heplot::heplot(manova_model)Common Mistakes
Analytical caveats and corrections to maintain modeling integrity.
References
Scholarly lineage and citation keys grounding the statistical framework.