# Create a full model with all available predictorsfull_model <-lm(body_mass_g ~ flipper_length_mm + bill_length_mm + bill_depth_mm + species + sex + island, data = penguins)# Try different directionsstep_forward <-step(lm(body_mass_g ~1, data = penguins),scope =formula(full_model),direction ="forward", trace =0)step_backward <-step(full_model, direction ="backward", trace =0)step_both <-step(full_model, direction ="both", trace =0)# Compare the final modelssummary(step_forward)$coefficientssummary(step_backward)$coefficientssummary(step_both)$coefficients
Questions to discuss:
Do all methods give the same final model?
If different, which variables differ?
Which method would you trust most?
Akaike Information Criterion (AIC)
AIC balances model fit and complexity:
\[\text{AIC} = -2\log(L) + 2p\]
where \(L\) is the likelihood and \(p\) is the number of parameters.
For linear regression: \[\text{AIC} = n\log(\text{SSR}/n) + 2p\]
Lower AIC is better! AIC penalizes adding parameters.
Bayesian Information Criterion (BIC)
BIC is similar to AIC but penalizes complexity more:
\[\text{BIC} = -2\log(L) + p\log(n)\]
For linear regression: \[\text{BIC} = n\log(\text{SSR}/n) + p\log(n)\]
Lower BIC is better! BIC tends to select simpler models than AIC.
Comparing Models with AIC/BIC
# Fit several modelsmodel1 <-lm(body_mass_g ~ flipper_length_mm, data = penguins)model2 <-lm(body_mass_g ~ flipper_length_mm + bill_length_mm, data = penguins)model3 <-lm(body_mass_g ~ flipper_length_mm + bill_length_mm + species, data = penguins)# Compare AICAIC(model1, model2, model3)
Guideline: Difference in AIC > 2 suggests models are meaningfully different.
🎯 Activity 3: Build Your Best Model
Individual/Pair Work:
# Your task: Find the best model for predicting penguin body mass# Available predictors: flipper_length_mm, bill_length_mm, # bill_depth_mm, species, sex, island# Step 1: Fit at least 4 different models (your choice!)model_a <-lm(body_mass_g ~ ..., data = penguins)model_b <-lm(body_mass_g ~ ..., data = penguins)model_c <-lm(body_mass_g ~ ..., data = penguins)model_d <-lm(body_mass_g ~ ..., data = penguins)# Step 2: Compare using AIC and BICAIC(model_a, model_b, model_c, model_d)BIC(model_a, model_b, model_c, model_d)# Step 3: Check diagnostics of your best modelplot(your_best_model)# Step 4: Report your findingssummary(your_best_model)
Questions:
Which model has the lowest AIC? Lowest BIC?
Do AIC and BIC agree?
How much do the values differ?
Does your best model pass diagnostic checks?
Cross-Validation: The Gold Standard
Idea: Estimate prediction error on unseen data.
K-Fold Cross-Validation:
Split data into \(K\) folds
For each fold: train on \(K-1\) folds, test on remaining fold
where \(e_i\) is the residual and \(h_{ii}\) is the leverage.
This is also called the PRESS statistic (Predicted Residual Sum of Squares).
Cross-Validation in R
# We'll use the caret package for easy CV implementation# install.packages("caret") # if neededlibrary(caret)# Set up 10-fold cross-validationtrain_control <-trainControl(method ="cv", number =10)# Fit model with 10-fold CVmodel_cv10 <-train(body_mass_g ~ flipper_length_mm + bill_length_mm + species,data = penguins,method ="lm",trControl = train_control,na.action = na.omit)# View resultsprint(model_cv10)
Linear Regression
344 samples
3 predictor
No pre-processing
Resampling: Cross-Validated (10 fold)
Summary of sample sizes: 308, 309, 309, 307, 308, 308, ...
Resampling results:
RMSE Rsquared MAE
340.0788 0.8275932 272.459
Tuning parameter 'intercept' was held constant at a value of TRUE
🎯 Activity 4: Cross-Validation
Group Exercise - Compare Different CV Methods:
Your task is to compare three models using different cross-validation approaches and determine which model predicts best!
library(caret)library(palmerpenguins)# Remove missing values firstpenguins_clean <-na.omit(penguins[, c("body_mass_g", "flipper_length_mm", "bill_length_mm", "bill_depth_mm", "species", "sex")])# Define three competing modelsmodel_formulas <-list(simple = body_mass_g ~ flipper_length_mm,moderate = body_mass_g ~ flipper_length_mm + bill_length_mm + species,complex = body_mass_g ~ flipper_length_mm + bill_length_mm + bill_depth_mm + species + sex)# Function to perform CV with different K valuescompare_cv_methods <-function(formula, data) {# 5-Fold CV ctrl_5fold <-trainControl(method ="cv", number =5) cv5 <-train(formula, data = data, method ="lm", trControl = ctrl_5fold)# 10-Fold CV ctrl_10fold <-trainControl(method ="cv", number =10) cv10 <-train(formula, data = data, method ="lm", trControl = ctrl_10fold)# Leave-One-Out CV (LOOCV) ctrl_loocv <-trainControl(method ="LOOCV") loocv <-train(formula, data = data, method ="lm", trControl = ctrl_loocv)# Return RMSE valuesreturn(data.frame(CV5_RMSE = cv5$results$RMSE,CV10_RMSE = cv10$results$RMSE,LOOCV_RMSE = loocv$results$RMSE,CV5_Rsquared = cv5$results$Rsquared,CV10_Rsquared = cv10$results$Rsquared,LOOCV_Rsquared = loocv$results$Rsquared ))}# Compare all three modelsresults_simple <-compare_cv_methods(model_formulas$simple, penguins_clean)results_moderate <-compare_cv_methods(model_formulas$moderate, penguins_clean)results_complex <-compare_cv_methods(model_formulas$complex, penguins_clean)# Create comparison tablecomparison <-data.frame(Model =c("Simple", "Moderate", "Complex"),rbind(results_simple, results_moderate, results_complex))print(comparison)# Visualize the results## create a plot to visualize your results
Activity 4: Discussion Questions
After running the code, discuss:
Which model has the lowest prediction error (RMSE)?
Does this match what AIC/BIC suggested?
How do the three CV methods (K=5, K=10, LOOCV) compare?
Are the RMSE values similar or different?
Which method took longest to run? Why?
What’s the trade-off between bias and variance?
LOOCV has lower bias but higher variance
K-fold has slightly higher bias but lower variance
Practical considerations:
When would you use K=5 vs K=10 vs LOOCV?
How does dataset size affect your choice?
Alternative: Manual K-Fold CV
For deeper understanding, implement CV manually:
# Manual 5-fold cross-validationset.seed(123)n <-nrow(penguins_clean)K <-5# Create fold assignmentsfold_ids <-sample(rep(1:K, length.out = n))# Store predictionscv_errors <-numeric(K)for(k in1:K) {# Split data test_idx <-which(fold_ids == k) train_data <- penguins_clean[-test_idx, ] test_data <- penguins_clean[test_idx, ]# Fit model on training data model <-lm(body_mass_g ~ flipper_length_mm + bill_length_mm + species, data = train_data)# Predict on test data predictions <-predict(model, newdata = test_data)# Calculate MSE for this fold cv_errors[k] <-mean((test_data$body_mass_g - predictions)^2)}# Average across all foldscv_mse <-mean(cv_errors)cv_rmse <-sqrt(cv_mse)cat("5-Fold CV RMSE:", round(cv_rmse, 2), "\n")cat("Individual fold RMSEs:", round(sqrt(cv_errors), 2), "\n")
Activity 4: Extension Challenge (Optional)
For advanced students:
# Compare CV results with train/test splitset.seed(456)train_idx <-sample(1:nrow(penguins_clean), size =0.8*nrow(penguins_clean))train_data <- penguins_clean[train_idx, ]test_data <- penguins_clean[-train_idx, ]# Fit on training datamodel_train <-lm(body_mass_g ~ flipper_length_mm + bill_length_mm + species,data = train_data)# Predict on test datatest_predictions <-predict(model_train, newdata = test_data)test_rmse <-sqrt(mean((test_data$body_mass_g - test_predictions)^2))cat("Single Train/Test Split RMSE:", round(test_rmse, 2), "\n")cat("10-Fold CV RMSE:", round(results_moderate$CV10_RMSE, 2), "\n")# Questions to consider:# 1. Which estimate is more reliable? Why?# 2. What happens if you change the random seed?# 3. How does the 80/20 split affect results?
Discuss:
Why is CV more reliable than a single train/test split?
When might a single split be preferred (e.g., time series)?
How would you report CV results in a paper?
Best Practices for Model Selection
Start with theory: Use domain knowledge
Check assumptions: Before comparing models
Use multiple criteria: Don’t rely on just one metric
Validate: Always test on new data
Keep it simple: Prefer simpler models when performance is similar
Document: Record your selection process
Common Pitfalls to Avoid
❌ Don’t:
Test on the same data used for selection (P)
Use automated selection blindly (P, D)
Ignore assumption violations (P, D)
Focus only on R-squared (P, D)
Include perfectly correlated predictors (P, D)
✓ Do:
Split data or use cross-validation (P)
Understand why predictors are selected (P, D)
Check diagnostics for final model (P, D)
Consider multiple criteria (P, D)
Check for multicollinearity (P, D)
Part 1 Summary
Key Takeaways:
Model selection balances fit and complexity
Multiple methods exist: stepwise, AIC/BIC, CV
AIC/BIC provide quick model comparison
Cross-validation estimates prediction error
Always check assumptions and diagnostics
Next class: ANOVA for comparing multiple groups!
Part 2: Analysis of Variance (ANOVA)
Class Overview - Part 2
Today’s Focus:
One-way ANOVA
ANOVA assumptions and diagnostics
Multiple comparisons
Two-way ANOVA
Interaction effects
Learning Goals: Compare means across multiple groups, check assumptions, and understand factorial designs
One-Way ANOVA
Analysis of Variance allows us to compare means across multiple groups (3 or more).
Example: Critical flicker frequency for 19 subjects with different eye colors.
Response: Flicker (continuous)
Factor: Eye Colour (Brown, Blue, Green)
Question: Do mean flicker frequencies differ by eye color?
Flicker Data
# Measures of 'critical flicker frequency' for 19 subjectsflicker <-read.table(file="http://www.statsci.org/data/general/flicker.txt",header=TRUE, stringsAsFactors=TRUE)head(flicker, 10)
Exploratory Data Analysis
# Check the factor structureis.factor(flicker$Colour)levels(flicker$Colour)# Summary statistics by groupmeansd <-function(x) c(mean=mean(x), sd=sd(x))by(flicker$Flicker, flicker$Colour, FUN=meansd)
library(palmerpenguins)# Research question: Does body mass differ by species?# 1. Create summary statistics by speciesby(penguins$body_mass_g, penguins$species, summary)# 2. Calculate mean and SD for each speciesby(penguins$body_mass_g, penguins$species, function(x) c(mean=mean(x, na.rm=T), sd=sd(x, na.rm=T)))# 3. Create a boxplotboxplot(body_mass_g ~ species, data=penguins,main="Body Mass by Penguin Species",ylab="Body Mass (g)", xlab="Species",col=c("darkorange", "purple", "cyan4"))# 4. Create a violin plot (bonus!)library(ggplot2)ggplot(penguins, aes(x=species, y=body_mass_g, fill=species)) +geom_violin() +theme_minimal()
Discuss: Do you think there are statistically significant differences? Why?
ANOVA Model: One-Way Design
Basic Model:
\[Y_{ij} = \mu_j + \varepsilon_{ij}\]
where:
\(Y_{ij}\) is the response for observation \(i\) in group \(j\)
\(\mu_j\) is the mean for group \(j\)
\(\varepsilon_{ij} \sim N(0, \sigma^2)\) are independent errors
Alternative parameterization:
\[Y_{ij} = \mu + \alpha_j + \varepsilon_{ij}\]
where \(\mu\) is the overall mean and \(\alpha_j\) is the effect of group \(j\)
Between groups: Variation explained by group differences
Within groups: Unexplained variation (error)
Sums of Squares
Total Sum of Squares:\[SS_{total} = \sum_{j=1}^J\sum_{i=1}^{n_j} (Y_{ij} - \bar{y})^2\]
Regression Sum of Squares (Between Groups):\[SS_{reg} = \sum_{j=1}^J n_j(\bar{y}_j - \bar{y})^2\], \[MS_{reg} = \frac{SS_{reg}}{J-1}\]
Sums of Squares
Residual Sum of Squares (Within Groups):\[SS_{res} = \sum_{j=1}^J\sum_{i=1}^{n_j} (Y_{ij} - \bar{y}_j)^2\], \[MS_{res} = \frac{SS_{res}}{N-J}\]
F-Statistic and Decision Rule
Under \(H_0\):\[F = \frac{MS_{reg}}{MS_{res}} \sim F_{J-1, N-J}\]
Under \(H_1\):\[E[MS_{reg}] > \sigma^2 \quad \Rightarrow \quad F > 1\]
Decision rule: Reject \(H_0\) if \(F > F_{J-1, N-J, \alpha}\)
ANOVA Table
Source
df
SS
MS
F
Between Groups
\(J-1\)
\(SS_{reg}\)
\(MS_{reg}\)
\(\frac{MS_{reg}}{MS_{res}}\)
Within Groups
\(N-J\)
\(SS_{res}\)
\(MS_{res}\)
Total
\(N-1\)
\(SS_{total}\)
Running ANOVA in R
# Fit ANOVA modelmodel_flicker <-aov(Flicker ~ Colour, data=flicker)# View ANOVA tableanova(model_flicker)# Or use summarysummary(model_flicker)
ANOVA Assumptions
Before trusting ANOVA results, we must verify three key assumptions:
Independence: Observations are independent within and across groups
Addressed by study design (random sampling/assignment)
Normality: Residuals are normally distributed
Check with Q-Q plots and Shapiro-Wilk test
ANOVA is robust to mild violations with balanced designs
Homogeneity of Variance: Equal variances across groups
Check with Levene’s test or residual plots
Rule of thumb: largest SD < 2 × smallest SD
Checking ANOVA Assumptions with Plots
# Create diagnostic plotspar(mfrow=c(2,2))plot(model_flicker)# These four plots show:# 1. Residuals vs Fitted - checks linearity and homoscedasticity# 2. Normal Q-Q - checks normality of residuals# 3. Scale-Location - checks homogeneity of variance# 4. Residuals vs Leverage - identifies influential points
Interpreting ANOVA Diagnostic Plots
1. Residuals vs Fitted:
Should show no pattern (random scatter around 0)
Points should be evenly spread across fitted values
Funnel shape → heteroscedasticity
2. Normal Q-Q Plot:
Points should fall on the diagonal line
Departures at ends indicate heavy/light tails
S-shape indicates skewness
Interpreting ANOVA Diagnostic Plots
3. Scale-Location:
Horizontal line with random scatter is ideal
Upward/downward trend → unequal variances
4. Residuals vs Leverage:
Identifies influential observations
Watch for points beyond Cook’s distance lines
Testing Normality: Visual and Statistical
# Visual check: Histogram of residualshist(residuals(model_flicker), main="Histogram of Residuals",xlab="Residuals", col="lightblue", breaks=10)# Visual check: Q-Q plot with confidence bandsqqnorm(residuals(model_flicker), main="Normal Q-Q Plot")qqline(residuals(model_flicker), col="red", lwd=2)# Statistical test: Shapiro-Wilk testshapiro.test(residuals(model_flicker))# H0: residuals are normally distributed# If p > 0.05, we do not reject H0 (good!)
Testing Homogeneity of Variance
# Levene's Test (robust to non-normality)library(car)leveneTest(Flicker ~ Colour, data=flicker)# H0: variances are equal across groups# If p > 0.05, we do not reject H0 (good!)# Bartlett's Test (sensitive to non-normality)bartlett.test(Flicker ~ Colour, data=flicker)# Visual check: Boxplots with similar spreadboxplot(Flicker ~ Colour, data=flicker,main="Check variance homogeneity",col=c("blue", "brown", "green"))# Rule of thumb checkby(flicker$Flicker, flicker$Colour, sd)# Largest SD should be < 2 × smallest SD
# Use the penguins data to test if flipper length differs by species# 1. Fit the ANOVA modelmodel_flipper <-aov(flipper_length_mm ~ species, data=penguins)# 2. View the resultssummary(model_flipper)anova(model_flipper)# 3. Check ALL assumptions with diagnostic plotspar(mfrow=c(2,2))plot(model_flipper)# 4. Test normalityshapiro.test(residuals(model_flipper))# 5. Test homogeneity of variancelibrary(car)leveneTest(flipper_length_mm ~ species, data=penguins)# 6. Visual check of variancesby(penguins$flipper_length_mm, penguins$species, sd, na.rm=TRUE)# Calculate ratio: max SD / min SD (should be < 2)# 7. Calculate effect size (eta-squared)SS_between <-anova(model_flipper)$"Sum Sq"[1]SS_total <-sum(anova(model_flipper)$"Sum Sq")eta_squared <- SS_between / SS_totalprint(paste("Eta-squared:", round(eta_squared, 3)))# 8. If assumptions violated, try Kruskal-Walliskruskal.test(flipper_length_mm ~ species, data=penguins)
🎯 Activity 2: Conduct Your Own ANOVA
Individual/Pair Work:
Questions:
What is the F-statistic and p-value?
Do you reject the null hypothesis?
Are the assumptions satisfied? Check each one:
Normality (Q-Q plot, Shapiro test)
Homogeneity of variance (Levene’s test, SD ratio)
What does this mean scientifically?
How large is the effect (eta-squared)?
Would you trust these results or consider transformations?
Multiple Comparisons Problem
After rejecting \(H_0\): Which group means are different?
Naive approach: Conduct separate t-tests for each pair
Problem: Family-wise error rate inflation!
For \(m\) tests at level \(\alpha = 0.05\):
3 tests:\(P(\text{Type I error}) = 1-(0.95)^3 = 0.143\)
10 tests:\(P(\text{Type I error}) = 1-(0.95)^{10} \approx 0.40\)
Solution: Use multiple comparison procedures!
Multiple Comparison Methods
Common approaches:
Fisher’s LSD (Least Significant Difference)
Liberal; doesn’t control family-wise error well
Tukey’s HSD (Honest Significant Difference)
Controls family-wise error rate
Best for all pairwise comparisons
Bonferroni correction
Very conservative
Use when few planned comparisons
Tukey’s HSD in R
M <-aov(Flicker ~ Colour, data=flicker)TukeyHSD(M)# Visualize confidence intervalsplot(TukeyHSD(M))
Time Poison Treatment
1 0.31 I A
2 0.45 I A
3 0.46 I A
4 0.43 I A
5 0.36 II A
6 0.29 II A
7 0.40 II A
8 0.23 II A
9 0.22 III A
10 0.21 III A
11 0.18 III A
12 0.23 III A
13 0.82 I B
14 1.10 I B
15 0.88 I B
16 0.72 I B
17 0.92 II B
18 0.61 II B
19 0.49 II B
20 1.24 II B
Interaction Plots
Visualize potential interactions:
interaction.plot(poison$Poison, poison$Treatment, response=poison$Time, lwd=2,main="Interaction: Poison x Treatment",xlab="Poison", ylab="Mean Survival Time",trace.label="Treatment")
interaction.plot(poison$Treatment, poison$Poison, response=poison$Time, lwd=2,main="Interaction: Treatment x Poison",xlab="Treatment", ylab="Mean Survival Time",trace.label="Poison")
Non-parallel lines suggest interaction.
Two-Way ANOVA with Interaction
L <-aov(Time ~ Poison * Treatment, data=poison)anova(L)
Analysis of Variance Table
Response: Time
Df Sum Sq Mean Sq F value Pr(>F)
Poison 2 1.03708 0.51854 23.3314 3.176e-07 ***
Treatment 3 0.92012 0.30671 13.8000 3.792e-06 ***
Poison:Treatment 6 0.25027 0.04171 1.8768 0.1118
Residuals 36 0.80010 0.02222
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Interpretation:
Interaction is not statistically significant (p = 0.1118)