STAT 204 โ Introduction to Statistical Data Analysis
22 Oct 2026
Programming is a systematic way to think about a problem โ turning a fuzzy question into precise definitions and a chain of answers. Learning R is how we build that mental model.
Thereโs no point using AI if youโre trying to build that mental model yourself.
Todayโs Focus:
Learning Goals: Match what you report, and which assumptions you check, to the goal of the analysis
| ๐ Describe | ๐ฌ Infer | ๐ฏ Predict | |
|---|---|---|---|
| Question | What patterns are in this data? | What can we say about the population? | How well will it do on new data? |
| Main output | Coefficients, group means, fitted lines, \(R^2\) | Confidence intervals, \(t\) and \(F\) tests | predict(), prediction intervals, CV error |
| Assumptions that matter most | Linearity | Linearity, independence, constant variance, normality (small \(n\)) | Linearity, independence; constant variance and normality for prediction intervals |
| Choosing the model | The question picks the variables | Chosen before looking at results | Let the data choose (AIC, CV) |
Slides today are tagged ๐, ๐ฌ or ๐ฏ by goal.
lm(), Different SentencesSame model, different claims, different checks.
Tests: \(H_0\): All slope coefficients are zero (\(\beta_1 = \beta_2 = \cdots = \beta_p = 0\))
Test statistic: \[F = \frac{(\text{SST} - \text{SSR})/p}{\text{SSR}/(n-p-1)} = \frac{\text{MSR}}{\text{MSE}} \sim F_{p, n-p-1}\]
This appears at the bottom of summary() output.
model1 <- lm(body_mass_g ~ flipper_length_mm, data = penguins)
model2 <- lm(body_mass_g ~ flipper_length_mm + bill_length_mm + bill_depth_mm,
data = penguins)
summary(model2)
confint(model2)
# Questions:
# 1. What percentage of the variance in body mass does model2 explain?
# Compare summary(model1)$r.squared, summary(model2)$r.squared
# and summary(model2)$adj.r.squared. Did adding predictors help?
# 2. Which coefficients are statistically significant? Do any
# confidence intervals contain zero?
# 3. What are the F-statistic and its p-value? What do they tell you?
# 4. Can individual predictors be non-significant while the overall
# model is significant?library(ggplot2)
# Add predictions to data
penguins_fit <- penguins %>%
drop_na(any_of(c("body_mass_g","bill_length_mm",
"bill_depth_mm")))%>%
mutate(fitted = fitted(model2),
residuals = residuals(model2))
# Plot actual vs fitted
ggplot(penguins_fit, aes(x = fitted, y = body_mass_g)) +
geom_point(alpha = 0.5) +
geom_abline(slope = 1, intercept = 0, color = "blue") +
labs(title = "Actual vs Fitted Values", x = "Fitted Body Mass (g)",y = "Actual Body Mass (g)")R automatically creates dummy variables for factors:
The reference category (Adelie) is absorbed into the intercept.
Does the effect of one predictor depend on another?
# Include an interaction between flipper length and species
model4 <- lm(body_mass_g ~ flipper_length_mm + species +
flipper_length_mm:species,
data = penguins)
# Shorthand
model4 <- lm(body_mass_g ~ flipper_length_mm * species,
data = penguins)
summary(model4)
# See it: one fitted line per species. Different slopes = interaction
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", se = FALSE)| Assumption | ๐ Describe | ๐ฌ Infer | ๐ฏ Predict |
|---|---|---|---|
| Linearity | Critical: a straight line must be an honest summary | Critical | Critical |
| Independence | Minor | Critical: standard errors are wrong without it | Matters: CV assumes it |
| Constant variance | Minor | Critical: affects SEs, tests, CIs | Matters for prediction intervals |
| Normality of errors | Not needed | Matters with small \(n\) | Matters for prediction intervals |
If assumptions are violated: descriptions can mislead, tests and CIs can be invalid, prediction intervals can be too narrow.
We check them with residuals.
Residual for observation \(i\): \(e_i = Y_i - \hat{Y}_i\)
Standardized residual: \(r_i = \dfrac{e_i}{\hat{\sigma}\sqrt{1 - h_{ii}}}\), where \(h_{ii}\) is the leverage of observation \(i\) (how unusual its \(X\) values are)
Purpose: Check linearity and constant variance
Purpose: Check normality of residuals
Points should fall approximately on the diagonal line.
The scale-location and residuals vs leverage plots, the hat matrix, and the Breusch-Pagan, Shapiro-Wilk and Durbin-Watson tests are not covered live. See the Self-Guided Practice section at the end of these slides.
| What you see | What to try |
|---|---|
| Curve in residuals vs fitted | Transform \(X\), add a polynomial term or an interaction |
| Funnel shape | Transform \(Y\) (log, square root); for inference, robust standard errors |
| Outliers or heavy tails | Check the data first; then robust regression. Hurts inference and prediction intervals most |
| Dependence (time, clusters) | A different model: time series, mixed models |
Or try a different model family (GLM, GAM). Logistic regression is next week.
We often have many potential predictors. Questions:
๐๐ฌ For description and inference, the question chooses the model.
๐ฏ For prediction, we want the model that predicts best on new data.
Underfitting (too simple):
Overfitting (too complex):
Goal: Find the sweet spot!
Work in pairs:
# Generate data with a simple relationship
set.seed(123)
x <- seq(0, 10, length.out = 20)
y <- 2 + 3*x + rnorm(20, sd = 5)
# Fit models of increasing complexity
fit_linear <- lm(y ~ x)
fit_cubic <- lm(y ~ poly(x, 3))
fit_10 <- lm(y ~ poly(x, 10))
# Plot and compare
par(mfrow=c(1,3))
plot(x, y, main="Linear (Simple)")
abline(fit_linear, col="red", lwd=2)
plot(x, y, main="Cubic (Moderate)")
lines(sort(x), fitted(fit_cubic)[order(x)], col="blue", lwd=2)
plot(x, y, main="10th degree (Complex)")
lines(sort(x), fitted(fit_10)[order(x)], col="green", lwd=2)Discuss: Which model has the highest \(R^2\)? Which would you trust for a new \(x\)?
Idea: Estimate prediction error on data the model has not seen.
K-Fold Cross-Validation:
Common: \(K = 5\) or \(K = 10\). Leave-one-out CV is \(K = n\) (self-study).
# We'll use the caret package for easy CV implementation
# install.packages("caret") # if needed
library(caret)
library(palmerpenguins)
# Set up 10-fold cross-validation
train_control <- trainControl(method = "cv", number = 10)
# Fit model with 10-fold CV
model_cv10 <- train(body_mass_g ~ flipper_length_mm + bill_length_mm + species,
data = penguins,
method = "lm",
trControl = train_control,
na.action = na.omit)
# View results
print(model_cv10)Linear Regression
344 samples
3 predictor
No pre-processing
Resampling: Cross-Validated (10 fold)
Summary of sample sizes: 308, 308, 307, 307, 309, 307, ...
Resampling results:
RMSE Rsquared MAE
339.7271 0.8272359 273.622
Tuning parameter 'intercept' was held constant at a value of TRUE
library(caret)
library(palmerpenguins)
# Remove missing values first, so all models use the same rows
penguins_clean <- na.omit(penguins[, c("body_mass_g", "flipper_length_mm",
"bill_length_mm", "bill_depth_mm",
"species", "sex")])
ctrl <- trainControl(method = "cv", number = 10)
# Same seed = same folds for every model
cv_results <- function(formula) {
set.seed(204)
train(formula, data = penguins_clean, method = "lm", trControl = ctrl)$results
}
rbind(
simple = cv_results(body_mass_g ~ flipper_length_mm),
moderate = cv_results(body_mass_g ~ flipper_length_mm + bill_length_mm + species),
complex = cv_results(body_mass_g ~ flipper_length_mm + bill_length_mm +
bill_depth_mm + species + sex)
)Discuss:
Leave-one-out CV and its shortcut for linear regression, comparing \(K = 5\), \(K = 10\) and LOOCV, a single train/test split, and K-fold CV by hand with a for loop are not covered live. See the Self-Guided Practice section at the end of these slides.
Key Takeaways:
Next class (Thu Oct 29): ANOVA for comparing groups, then variable selection
Todayโs Focus:
Learning Goals: Compare means across several groups, and choose a model in a way that fits the goal
Analysis of Variance compares means across multiple groups (3 or more). It is lm() with one categorical predictor:
Critical flicker frequency for 19 subjects with different eye colors. Question: Do mean flicker frequencies differ by eye color?
flicker <- read.table(
file="http://www.statsci.org/data/general/flicker.txt",
header=TRUE, stringsAsFactors=TRUE)
levels(flicker$Colour)
# Summary statistics by group
meansd <- function(x) c(mean=mean(x), sd=sd(x))
by(flicker$Flicker, flicker$Colour, FUN=meansd)
boxplot(Flicker ~ Colour, data=flicker,
ylab="Flicker", col=c('Blue','Brown','Green'))\[Y_{ij} = \mu_j + \varepsilon_{ij} \quad \text{or} \quad Y_{ij} = \mu + \alpha_j + \varepsilon_{ij}, \qquad \varepsilon_{ij} \sim N(0, \sigma^2) \text{ independent}\]
Hypotheses:
\[H_0: \mu_1 = \mu_2 = \cdots = \mu_J \quad (\text{equivalently } \alpha_1 = \cdots = \alpha_J = 0)\] \[H_1: \text{Not all } \mu_j \text{ are equal}\]
Total variation can be decomposed:
\[\underbrace{\sum_{j=1}^J\sum_{i=1}^{n_j} (Y_{ij} - \bar{y})^2}_{SS_{total}} = \underbrace{\sum_{j=1}^J n_j(\bar{y}_j - \bar{y})^2}_{SS_{reg} \text{ (between)}} + \underbrace{\sum_{j=1}^J\sum_{i=1}^{n_j} (Y_{ij} - \bar{y}_j)^2}_{SS_{res} \text{ (within)}}\]
Mean squares: \(MS_{reg} = \dfrac{SS_{reg}}{J-1}\), \(\quad MS_{res} = \dfrac{SS_{res}}{N-J}\)
๐ \(\eta^2 = SS_{reg}/SS_{total}\) is the share of variation explained by the groups (like \(R^2\)).
Under \(H_0\): \(F = \dfrac{MS_{reg}}{MS_{res}} \sim F_{J-1, N-J}\). Under \(H_1\), \(E[MS_{reg}] > \sigma^2\), so \(F\) tends to be larger than 1.
Decision rule: Reject \(H_0\) if \(F > F_{J-1, N-J, \alpha}\)
| 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}\) |
The same residual plots as in Part 1:
# Does flipper length differ by species?
model_flipper <- aov(flipper_length_mm ~ species,
data=penguins)
summary(model_flipper)
# Check assumptions
par(mfrow=c(2,2))
plot(model_flipper)
library(car)
leveneTest(flipper_length_mm ~ species, data=penguins)
by(penguins$flipper_length_mm, penguins$species,
sd, na.rm=TRUE)
# Effect size (eta-squared)
ss <- summary(model_flipper)[[1]]$"Sum Sq"
ss[1] / sum(ss)Questions:
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\):
Solution: Use multiple comparison procedures!
# Continue with the penguin flipper length analysis
# 1. Perform Tukey's HSD
tukey_results <- TukeyHSD(model_flipper)
tukey_results
plot(tukey_results)
# 2. Try different adjustment methods
pairwise.t.test(penguins$flipper_length_mm, penguins$species,
p.adjust = "none")
pairwise.t.test(penguins$flipper_length_mm, penguins$species,
p.adjust = "bonferroni")
pairwise.t.test(penguins$flipper_length_mm, penguins$species,
p.adjust = "holm")Questions:
Two factors with possible interaction, like the interaction terms in regression.
Example: Survival times for animals exposed to:
Model: \[y_{ijk} = \mu + \alpha_i + \beta_j + \gamma_{ij} + \epsilon_{ijk}\]
where:
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
Visualize potential interactions:


Non-parallel lines suggest interaction.
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:
Analysis of Variance Table
Response: Time
Df Sum Sq Mean Sq F value Pr(>F)
Poison 2 1.03708 0.51854 20.734 5.448e-07 ***
Treatment 3 0.92012 0.30671 12.264 6.743e-06 ***
Residuals 42 1.05037 0.02501
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Tables of means
Grand mean
0.4791667
Poison
Poison
I II III
0.6175 0.5444 0.2756
Treatment
Treatment
A B C D
0.3142 0.6767 0.3925 0.5333
Tukey multiple comparisons of means
95% family-wise confidence level
Fit: aov(formula = Time ~ Poison + Treatment, data = poison)
$Poison
diff lwr upr p adj
II-I -0.073125 -0.2089618 0.06271181 0.3987984
III-I -0.341875 -0.4777118 -0.20603819 0.0000008
III-II -0.268750 -0.4045868 -0.13291319 0.0000582
$Treatment
diff lwr upr p adj
B-A 0.36250000 0.18980177 0.5351982 0.0000083
C-A 0.07833333 -0.09436490 0.2510316 0.6219967
D-A 0.21916667 0.04646843 0.3918649 0.0079262
C-B -0.28416667 -0.45686490 -0.1114684 0.0004077
D-B -0.14333333 -0.31603157 0.0293649 0.1344032
D-C 0.14083333 -0.03186490 0.3135316 0.1451135
Regularization (Ridge, Lasso) is another way to choose for prediction: we cover it on Tue Nov 17.
Both balance model fit and complexity, with \(p\) parameters and likelihood \(L\):
\[\text{AIC} = -2\log(L) + 2p \qquad \text{BIC} = -2\log(L) + p\log(n)\]
For linear regression: \[\text{AIC} = n\log(\text{SSR}/n) + 2p \qquad \text{BIC} = n\log(\text{SSR}/n) + p\log(n)\]
model1 <- lm(body_mass_g ~ flipper_length_mm, data = penguins)
model2 <- lm(body_mass_g ~ flipper_length_mm + bill_length_mm + bill_depth_mm,
data = penguins)
model3 <- lm(body_mass_g ~ flipper_length_mm + species, data = penguins)
AIC(model1, model2, model3)
BIC(model1, model2, model3)
# Questions:
# 1. Which model has the lowest AIC? The lowest BIC?
# 2. Are they the same model?
# 3. How much do AIC values differ?# Use complete cases, so every candidate model uses the same rows
penguins_complete <- na.omit(penguins)
full_model <- lm(body_mass_g ~ flipper_length_mm + bill_length_mm +
bill_depth_mm + species,
data = penguins_complete)
step(lm(body_mass_g ~ 1, data = penguins_complete),
scope = formula(full_model), direction = "forward")
step(full_model, direction = "backward")
step(full_model, direction = "both")# Create a full model with all available predictors
full_model <- lm(body_mass_g ~ flipper_length_mm + bill_length_mm +
bill_depth_mm + species + sex + island,
data = penguins_complete)
# Try different directions
step_forward <- step(lm(body_mass_g ~ 1, data = penguins_complete),
scope = formula(full_model),
direction = "forward")
step_backward <- step(full_model, direction = "backward")
# Do they give the same final model?After stepwise selection, summary() of the chosen model is too optimistic:
So:
| ๐ | ๐ฌ | ๐ฏ | |
|---|---|---|---|
| Start from domain knowledge and the question | โ | โ | โ |
| Check diagnostics for the final model | โ | โ | โ |
| Check multicollinearity (VIF), donโt include perfectly correlated predictors | โ | โ | |
| Fix the model before seeing results | โ | ||
| Donโt use automated selection blindly; understand why predictors are selected | โ | โ | โ |
| Validate on new data or with CV; never test on the data used for selection | โ | ||
| Donโt rely only on \(R^2\); use several criteria | โ | โ | โ |
| Prefer the simpler model when performance is similar | โ | โ | |
| Document how you chose the model | โ | โ | โ |
Using the penguins data:
car::vif())Transformations and Box-Cox, Bartlettโs test, compact letter displays, and diagnostics for two-way ANOVA are not covered live. See the Self-Guided Practice section at the end of these slides.
Key Takeaways:
Next class (Tue Nov 3): Project proposal presentations
Then: logistic regression (Thu Nov 5). HW4 is released today
Office Hours: Tuesdays and Thursdays, 3:15 pm - 4:00 pm (before lecture)
The following slides are not covered live. Work through them on your own, or bring questions to the Wednesday self-guided session.
Purpose: Check homoscedasticity
This plots \(\sqrt{|\text{standardized residuals}|}\) vs fitted values.
What to look for: Horizontal line with equal spread โ constant variance โ
The fitted values are \(\hat{\mathbf{Y}} = \mathbf{H}\mathbf{Y}\), with the hat matrix
\[\mathbf{H} = \mathbf{X}(\mathbf{X}^T\mathbf{X})^{-1}\mathbf{X}^T\]
The leverage \(h_{ii}\) is the \(i\)-th diagonal element of \(\mathbf{H}\). Observations with unusual \(X\) values have high leverage.
Purpose: Identify influential observations
Cookโs distance measures influence of each observation.
Rule of thumb: Cookโs distance > 1 indicates influential point.
Tests for heteroscedasticity.
\(H_0\): Homoscedasticity (constant variance)
If p-value is very small: Reject \(H_0\), evidence of heteroscedasticity.
Tests for normality of residuals.
\(H_0\): Residuals are normally distributed
Important: With large \(n\), this test is very sensitive. Visual inspection (Q-Q plot) often more useful.
Tests for autocorrelation in residuals (common in time series).
\(H_0\): No autocorrelation
Test statistic โ 2 suggests no autocorrelation.
Special case: \(K = n\)
CV error: \[\text{CV} = \frac{1}{n}\sum_{i=1}^{n}(Y_i - \hat{Y}_{-i})^2\]
where \(\hat{Y}_{-i}\) is prediction for \(i\) using model trained without observation \(i\).
Computationally expensive but can use shortcut for linear regression!
\[\text{CV} = \frac{1}{n}\sum_{i=1}^{n}\left(\frac{e_i}{1 - h_{ii}}\right)^2\]
where \(e_i\) is the residual and \(h_{ii}\) is the leverage.
This is also called the PRESS statistic (Predicted Residual Sum of Squares).
This uses penguins_clean from Activity 2 of Part 1.
model_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 values
compare_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 values
return(data.frame(
CV5_RMSE = cv5$results$RMSE,
CV10_RMSE = cv10$results$RMSE,
LOOCV_RMSE = loocv$results$RMSE
))
}
results_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)
data.frame(
Model = c("Simple", "Moderate", "Complex"),
rbind(results_simple, results_moderate, results_complex)
)
# 1. Are the RMSE values similar across K = 5, K = 10 and LOOCV?
# 2. Which method took longest to run? Why?
# 3. LOOCV has lower bias but higher variance; K-fold the reverse.
# When would you use each? How does dataset size affect the choice?set.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 data
model_train <- lm(body_mass_g ~ flipper_length_mm + bill_length_mm + species,
data = train_data)
# Predict on test data
test_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")
# 1. Which estimate is more reliable? Why?
# 2. What happens if you change the random seed?
# 3. When might a single split be preferred (e.g., time series)?For deeper understanding, implement CV manually. This uses penguins_clean from Activity 2 of Part 1.
# Manual 5-fold cross-validation
set.seed(123)
n <- nrow(penguins_clean)
K <- 5
# Create fold assignments
fold_ids <- sample(rep(1:K, length.out = n))
# Store predictions
cv_errors <- numeric(K)
for(k in 1: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 folds
cv_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")# Log transformation (for right-skewed data, positive values only)
model_log <- aov(log(Flicker) ~ Colour, data=flicker)
plot(model_log)
# Square root transformation (for count data)
model_sqrt <- aov(sqrt(Flicker) ~ Colour, data=flicker)
# Box-Cox transformation (finds optimal transformation)
library(MASS)
bc <- boxcox(lm(Flicker ~ Colour, data=flicker))
lambda <- bc$x[which.max(bc$y)]
model_bc <- aov(Flicker^lambda ~ Colour, data=flicker)
# After transformation, check diagnostics again!
par(mfrow=c(2,2))
plot(model_bc)Another test of equal variances. Unlike Leveneโs test, it is sensitive to non-normality.
Groups that share a letter are not significantly different.
Same assumptions apply! Always check before trusting results.
Shapiro-Wilk normality test
data: residuals(L1)
W = 0.92213, p-value = 0.003538
Levene's Test for Homogeneity of Variance (center = median)
Df F value Pr(>F)
group 11 4.1582 0.0005539 ***
36
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
![]()
STAT 204 โ Intro to Statistical Data Analysis