πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Analysis of Covariance (ANCOVA)

Advanced Statistical MethodsAnalysis of Variance🟒 Free Lesson

Advertisement

Analysis of Covariance (ANCOVA)

Advanced Statistical Methods

Adjusting for Confounders While Testing Group Differences

ANCOVA combines ANOVA with regression to compare group means while statistically controlling for continuous covariates. It increases precision and removes bias from confounding variables.

  • Clinical trials β€” Compare treatment effects while adjusting for baseline severity scores
  • Education research β€” Assess school performance while controlling for socioeconomic status
  • Agriculture β€” Compare crop yields while adjusting for soil quality differences across fields

ANCOVA levels the playing field so you can see true group differences clearly.


Analysis of Covariance (ANCOVA) extends the general linear model to include both categorical independent variables (factors) and continuous independent variables (covariates). ANCOVA combines the explanatory power of ANOVA for group comparisons with the precision-enhancing capability of regression through covariate adjustment. By statistically controlling for the linear influence of one or more covariates, ANCOVA reduces error variance, increases statistical power, and enables more precise comparisons of treatment means adjusted for confounding variables.

Mathematical Framework

Homogeneity of Regression Slopes

A critical assumption of ANCOVA is that the regression slopes relating the covariate to the dependent variable are equal across all groups.

Adjusted Means

ANCOVA produces adjusted (least-squares) means that account for the covariate, providing fairer comparisons when groups have different covariate distributions.

Effect Size: Partial Eta-Squared

Assumptions and Diagnostics

Python Implementation

import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.api as sm
from statsmodels.formula.api import ols
import matplotlib.pyplot as plt

# Generate ANCOVA data
np.random.seed(42)

n_per_group = 25
n_groups = 3

# Group-specific true effects
group_effects = {'A': 5.0, 'B': 8.0, 'C': 3.0}
beta_true = 0.75  # True common slope

# Generate covariate (pretest) - different distributions per group
covariate_means = {'A': 75, 'B': 65, 'C': 70}
data_list = []

for group, mean_x in covariate_means.items():
    X = np.random.normal(mean_x, 10, n_per_group)
    Y = 50 + group_effects[group] + beta_true * X + np.random.normal(0, 5, n_per_group)
    
    df = pd.DataFrame({
        'Pretest': X,
        'Posttest': Y,
        'Group': group
    })
    data_list.append(df)

data = pd.concat(data_list, ignore_index=True)

print("Data Summary:")
print(data.groupby('Group').agg(['mean', 'std', 'count']))

# Test homogeneity of slopes (interaction model)
model_interaction = ols('Posttest ~ C(Group) * Pretest', data=data).fit()
print("\nInteraction Model (Test Homogeneity of Slopes):")
print(f"Interaction F-statistic: {model_interaction.fvalue:.3f}")
print(f"Interaction p-value: {model_interaction.f_pvalue:.4f}")

# ANCOVA model (assuming parallel slopes)
model_ancova = ols('Posttest ~ C(Group) + Pretest', data=data).fit()
print("\nANCOVA Model Summary:")
print(model_ancova.summary())

# Extract ANCOVA table
from statsmodels.stats.anova import anova_lm
ancova_table = anova_lm(model_ancova, typ=2)
print("\nANCOVA Table (Type II):")
print(ancova_table)

# Compute partial eta-squared
ss_group = ancova_table.loc['C(Group)', 'sum_sq']
ss_covariate = ancova_table.loc['Pretest', 'sum_sq']
ss_error = ancova_table.loc['Residual', 'sum_sq']

eta2_partial_group = ss_group / (ss_group + ss_error)
eta2_partial_covariate = ss_covariate / (ss_covariate + ss_error)

print(f"\nPartial eta-squared (Group): {eta2_partial_group:.4f}")
print(f"Partial eta-squared (Pretest): {eta2_partial_covariate:.4f}")

# Compute adjusted means
group_means = data.groupby('Group')['Posttest'].mean()
group_cov_means = data.groupby('Group')['Pretest'].mean()
overall_cov_mean = data['Pretest'].mean()
beta_hat = model_ancova.params['Pretest']

adjusted_means = {}
for group in ['A', 'B', 'C']:
    adj_mean = group_means[group] - beta_hat * (group_cov_means[group] - overall_cov_mean)
    adjusted_means[group] = adj_mean

print("\nUnadjusted vs Adjusted Means:")
print(f"{'Group':<8} {'Unadjusted':<12} {'Adjusted':<12} {'Pretest Mean':<14}")
for group in ['A', 'B', 'C']:
    print(f"{group:<8} {group_means[group]:<12.3f} {adjusted_means[group]:<12.3f} "
          f"{group_cov_means[group]:<14.1f}")

# Standard error of adjusted means
ms_error = ancova_table.loc['Residual', 'mean_sq']
n_g = data.groupby('Group').size()

se_adjusted = {}
for group in ['A', 'B', 'C']:
    se = np.sqrt(ms_error * (1/n_g[group] + 
                            (group_cov_means[group] - overall_cov_mean)**2 / 
                            ancova_table.loc['Pretest', 'sum_sq']))
    se_adjusted[group] = se

print("\nAdjusted Means with 95% CI:")
for group in ['A', 'B', 'C']:
    ci_low = adjusted_means[group] - 1.96 * se_adjusted[group]
    ci_high = adjusted_means[group] + 1.96 * se_adjusted[group]
    print(f"  Group {group}: {adjusted_means[group]:.3f} [{ci_low:.3f}, {ci_high:.3f}]")

# Assumption diagnostics
# 1. Linearity - scatter plots
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
colors = {'A': 'blue', 'B': 'red', 'C': 'green'}

for i, group in enumerate(['A', 'B', 'C']):
    mask = data['Group'] == group
    axes[i].scatter(data[mask]['Pretest'], data[mask]['Posttest'], 
                   c=colors[group], alpha=0.6, s=50)
    
    # Add regression line
    x_range = np.linspace(data[mask]['Pretest'].min(), 
                         data[mask]['Pretest'].max(), 100)
    y_pred = model_ancova.params['Intercept'] + model_ancova.params[f'C(Group)[T.{group}]'] + \
             beta_hat * x_range
    axes[i].plot(x_range, y_pred, 'k-', linewidth=2)
    
    axes[i].set_xlabel('Pretest (Covariate)')
    axes[i].set_ylabel('Posttest')
    axes[i].set_title(f'Group {group}')
    axes[i].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('ancova_linearity.png', dpi=150)
plt.show()

# 2. Homogeneity of slopes visualization
fig, ax = plt.subplots(figsize=(8, 6))

for group in ['A', 'B', 'C']:
    mask = data['Group'] == group
    ax.scatter(data[mask]['Pretest'], data[mask]['Posttest'], 
              c=colors[group], alpha=0.6, label=group, s=50)
    
    # Separate regression lines
    z = np.polyfit(data[mask]['Pretest'], data[mask]['Posttest'], 1)
    x_line = np.linspace(data[mask]['Pretest'].min(), 
                        data[mask]['Pretest'].max(), 100)
    ax.plot(x_line, np.polyval(z, x_line), color=colors[group], 
           linestyle='--', linewidth=2)

# Common regression line
x_common = np.linspace(data['Pretest'].min(), data['Pretest'].max(), 100)
y_common = model_ancova.params['Intercept'] + beta_hat * x_common
ax.plot(x_common, y_common, 'k-', linewidth=2, label='Common slope (ANCOVA)')

ax.set_xlabel('Pretest (Covariate)')
ax.set_ylabel('Posttest')
ax.set_title('Test of Homogeneity of Regression Slopes')
ax.legend()
ax.grid(True, alpha=0.3)
plt.savefig('ancova_slopes.png', dpi=150)
plt.show()

# 3. Residual diagnostics
residuals = model_ancova.resid
fitted = model_ancova.fittedvalues

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# Residuals vs Fitted
axes[0].scatter(fitted, residuals, alpha=0.6)
axes[0].axhline(0, color='red', linestyle='--')
axes[0].set_xlabel('Fitted Values')
axes[0].set_ylabel('Residuals')
axes[0].set_title('Residuals vs Fitted')
axes[0].grid(True, alpha=0.3)

# Q-Q plot
stats.probplot(residuals, dist="norm", plot=axes[1])
axes[1].set_title('Normal Q-Q Plot')
axes[1].grid(True, alpha=0.3)

# Residuals by group
for group in ['A', 'B', 'C']:
    mask = data['Group'] == group
    group_residuals = residuals[mask]
    axes[2].boxplot(group_residuals, positions=[list(['A', 'B', 'C']).index(group)],
                   widths=0.5)

axes[2].set_xticklabels(['A', 'B', 'C'])
axes[2].set_xlabel('Group')
axes[2].set_ylabel('Residuals')
axes[2].set_title('Residuals by Group')
axes[2].axhline(0, color='red', linestyle='--')
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('ancova_diagnostics.png', dpi=150)
plt.show()

# Levene's test for homoscedasticity
levene_stat, levene_p = stats.levene(*[data[data['Group'] == g]['Posttest'] 
                                       for g in ['A', 'B', 'C']])
print(f"\nLevene's test for homoscedasticity: F = {levene_stat:.3f}, p = {levene_p:.4f}")

# Shapiro-Wilk test for normality of residuals
shapiro_stat, shapiro_p = stats.shapiro(residuals)
print(f"Shapiro-Wilk test for normality: W = {shapiro_stat:.3f}, p = {shapiro_p:.4f}")

# Comparison: ANOVA vs ANCOVA
from scipy.stats import f_oneway

# Separate ANOVAs (one-way)
anova_groups = [data[data['Group'] == g]['Posttest'] for g in ['A', 'B', 'C']]
f_anova, p_anova = f_oneway(*anova_groups)

print("\nComparison: One-way ANOVA vs ANCOVA")
print(f"One-way ANOVA: F({n_groups-1}, {len(data)-n_groups}) = {f_anova:.3f}, p = {p_anova:.4f}")
print(f"ANCOVA: F({n_groups-1}, {len(data)-n_groups-1}) = {ancova_table.loc['C(Group)', 'F']:.3f}, "
      f"p = {ancova_table.loc['C(Group)', 'PR(>F)']:.4f}")
print(f"Error reduction: {(1 - ss_error/((len(data)-n_groups)*np.var(data['Posttest'], ddof=1)))*100:.1f}%")

Need Expert Statistics Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement