Lecture slides

ANOVA and Regression

ANOVA and Regression

How much of the variation can other variables explain?

Chapter 8

Polla Fattah

By the end of today you can

  • explain how ANOVA splits variation, and what F compares;
  • compare three or more groups, and find which differ;
  • analyse two factors and their interaction;
  • fit and interpret simple and multiple linear regression;
  • separate a real effect from a confounder;
  • model curves and interactions;
  • read diagnostic plots;
  • interpret logistic regression as odds ratios and probabilities;
  • report ANOVA and regression in a thesis.

One idea behind both methods

Students differ in stress, grades, and wellbeing.

Which part of those differences is connected with faculty, sleep, or support, and which part remains unexplained?

Method Models
ANOVA the averages of three or more groups
linear regression a numeric outcome from predictors
logistic regression a yes-or-no outcome from predictors

The data for this chapter

scores <- questionnaire |>
  mutate(stress_4 = 6 - stress_4,
         stress   = rowMeans(pick(stress_1:stress_6), na.rm = TRUE),
         support  = rowMeans(pick(support_1:support_6), na.rm = TRUE)) |>
  select(student_id, stress, support)

study <- students |>
  left_join(scores, join_by(student_id)) |>
  left_join(semesters |> filter(semester == 1), join_by(student_id))

One row per student: background, first semester, and questionnaire scores.

Why not ten t-tests?

Five faculties give 10 pairs.

Each test has a 5% false alarm risk, and across 10 tests the chance of at least one is about 40%.

ANOVA asks one question first: do the faculty averages differ at all?

Variation between and within groups

Group averages (copper) vs the overall average: between. Points vs their own average: within.

The F statistic

\[ F = \frac{\text{variation between groups}}{\text{variation within groups}} \]

summary(aov(stress ~ group, data = group_scores))

F near 1: averages differ no more than chance produces. Large F: they differ more.

The ratio matters, not only the gaps

tight <- group_scores |> mutate(stress = group_avg + 0.5 * offsets)
wide  <- group_scores |> mutate(stress = group_avg + 4 * offsets)
Version Group averages F
tight identical 39.2
wide identical 0.6

Half a point is convincing when students within a group hardly differ, and unremarkable when they differ by two.

Stress by faculty

Much overlap. Health Sciences sits a little higher, Humanities a little lower.

One-way ANOVA

stress_anova <- aov(stress ~ faculty, data = study)
summary(stress_anova)

p = 0.003: the faculties differ in stress.

The next question is how much.

Eta squared: the size of the effect

ss <- summary(stress_anova)[[1]][["Sum Sq"]]
ss[1] / sum(ss)

\(\eta^2\) ≈ 0.03: only about 3% of the differences in stress lie between faculties.

\(\eta^2\) 0.01 0.06 0.14
label small medium large

Faculty matters, but says very little about one student’s stress.

Post-hoc tests: which groups differ?

TukeyHSD(stress_anova)

Tukey’s test compares every pair while keeping the overall false alarm rate at 5%.

Only one pair differs: Health Sciences is 0.34 points more stressed than Humanities.

Assumptions of ANOVA

Assumption Check
independent observations the design
roughly normal in each group graphs, or large groups
similar spread Levene’s test: car::leveneTest()
kruskal.test(stress ~ faculty, data = study)

Kruskal-Wallis compares ranks when assumptions fail. Here it agrees.

Two-way ANOVA

Term Asks
main effect of programme Master’s vs PhD, on average
main effect of study mode full-time vs part-time, on average
interaction does the study-mode effect depend on programme?

An interaction plot

Parallel lines: no interaction. Part-time is lower in both programmes.

Testing main effects and interaction

summary(aov(wellbeing ~ programme * study_mode, data = study))

programme * study_mode asks for both main effects and their interaction.

Study mode matters; programme and the interaction do not.

A significant interaction is interpreted before the main effects.

Regression models the average outcome

ANOVA compares groups. Regression models how an outcome changes with predictors, numeric or categorical.

ANOVA is a special case of regression.

Average GPA at each level of sleep

sleep_bands <- study |>
  filter(!is.na(sleep_hours)) |>
  mutate(sleep_band = round(sleep_hours * 2) / 2) |>
  summarise(mean_gpa = mean(gpa), students = n(), .by = sleep_band)

The average GPA of students who sleep 5 hours, 5.5 hours, 6 hours, and so on.

The line summarises conditional averages

The spread of grey points around the line is what the model does not explain.

Simple linear regression

\[ \text{GPA} = b_0 + b_1 \times \text{sleep} + \text{error} \]

Term Meaning
\(b_0\), intercept predicted GPA at zero sleep; places the line
\(b_1\), slope change in predicted GPA per extra hour
residual vertical distance from point to line

Least squares makes the squared residuals as small as possible.

Fitting a line to five students

five <- tibble(sleep = c(5, 6, 6.5, 7, 8),
               gpa   = c(2.8, 3.0, 3.2, 3.1, 3.4))
lm(gpa ~ sleep, data = five)

Each extra hour goes with a GPA about 0.19 higher.

Nobody sleeps zero hours, so the intercept rarely means anything on its own.

Three results in summary()

gpa_simple <- lm(gpa ~ sleep_hours, data = study)
summary(gpa_simple)
Result Value Meaning
slope 0.08 GPA per extra hour of sleep
R-squared 0.06 share of GPA variation explained
residual SE 0.32 typical distance from the line

Sleep matters, but it is far from the whole story.

Multiple regression

gpa_model <- lm(gpa ~ sleep_hours + study_hours + stress + support,
                data = study)
Predictor Change in GPA per unit
sleep (hour) 0.11
study (hour a week) 0.007
stress (point) -0.08
support (point) 0.11

Each coefficient holds the other predictors constant.

How much the model explains

R-squared ≈ 25%: modest, and normal for grades, which depend on much no survey measures.

  • adjusted R-squared corrects for the number of predictors;
  • lm() drops students with any missing value: 13 here.

Always report the number of cases each model is based on.

Categorical predictors

lm(gpa ~ sleep_hours + study_hours + stress + support + gender,
   data = study)

R compares each category with a reference category, alphabetically first: Female.

genderMale is tiny and far from significant: no evidence that gender affects GPA. That is a finding too.

relevel(factor(faculty), ref = "Humanities") chooses another reference.

Two explanations for caffeine and GPA

flowchart LR
  subgraph A["Caffeine as a cause"]
    C1[Caffeine] --> G1[GPA]
  end
  subgraph B["Sleep as a confounder"]
    S2[Sleep] --> C2[Caffeine]
    S2 --> G2[GPA]
    C2 -. "apparent link" .- G2
  end

Regression can tell them apart by comparing students who sleep the same amount.

Adjusting for the confounder

lm(gpa ~ caffeine_mg, data = study)
lm(gpa ~ caffeine_mg + sleep_hours, data = study)
Model Caffeine coefficient p
caffeine alone -0.00043 <0.001
with sleep -0.00008 0.51

Among students who sleep the same, caffeine makes no difference to grades.

Regression adjusts only for what you include

A confounder is removed only if it was measured and put in the model.

Outside a randomised experiment, regression shows associations adjusted for the variables in the model, not proof of cause.

Curved relationships

gpa_curve <- lm(gpa ~ study_hours + I(study_hours^2), data = study)

I() calculates study_hours^2 before fitting.

The squared term is negative and significant: the line bends downwards.

Diminishing returns

The first hours make a real difference. The curve peaks at about 32 hours a week.

Interactions in regression

study <- study |> mutate(support_c = support - mean(support))
lm(gpa ~ support_c * programme + sleep_hours + study_hours, data = study)

Centring: zero now means average support.

Programme GPA per point of support
Master’s 0.10
PhD 0.24

Support matters about 2.3 times as much for PhD students.

What violated assumptions look like

A shapeless band is good. A U shape means a missed curve. A funnel makes p-values unreliable.

Checking the GPA model

par(mfrow = c(1, 2))
plot(gpa_model, which = 1:2)

Residuals against fitted: a shapeless band, no curve, no funnel.

Q-Q plot of residuals: close to the line, so roughly normal. The model looks good.

Tidy results with broom

library(broom)
tidy(gpa_model, conf.int = TRUE)   # coefficients with intervals
glance(gpa_model)                  # overall fit

Model results as data frames, ready for a thesis table.

Writing it up

A multiple regression of first-semester GPA on sleep, study hours, stress, and supervisor support explained 25% of the variance, F(4, 582) = 49.14, p < .001, N = 587. Holding the other predictors constant, each additional hour of sleep was associated with a GPA 0.11 points higher (95% CI [0.08, 0.13]).

The null hypothesis of a zero sleep coefficient was rejected.

Yes-or-no outcomes need logistic regression

Linear regression could predict probabilities below 0 or above 1.

Logistic regression models the probability of “yes” in a way that stays between 0 and 1.

Odds and odds ratios

jobs <- table(students$employment, students$considering_dropout)
odds_full <- jobs["Full-time job", "Yes"] / jobs["Full-time job", "No"]
odds_none <- jobs["None", "Yes"] / jobs["None", "No"]
Group Odds of considering dropout
full-time job 0.35
no job 0.16
odds ratio 2.2

1 means no difference; above 1 higher odds; below 1 lower.

A model for dropout

study <- study |> mutate(dropout = as.integer(considering_dropout == "Yes"))

dropout_model <- glm(
  dropout ~ stress + support + financial_worry + employment + study_mode,
  data = study, family = binomial)

exp(cbind(odds_ratio = coef(dropout_model),
          confint.default(dropout_model)))

The outcome is 0 or 1. exp() turns log-odds into odds ratios.

Reading the odds ratios

Predictor (per point) Odds ratio 95% CI
stress 3.60 2.25 to 5.77
financial worry 1.57 1.24 to 2.00
support 0.33 0.22 to 0.49

Stress raises the odds; support cuts them by about 67%. The stress hypothesis is supported.

What happened to employment?

Alone, a full-time job about doubled the odds of considering dropout.

In the model, both employment intervals include 1.

A job likely matters because of the stress and money pressure it brings. Multiple regression makes that visible.

Predicted probabilities are easier to read

examples <- tibble(stress = c(2.5, 4.0), support = c(4.0, 2.0),
                   financial_worry = 3, employment = "None",
                   study_mode = "Full-time")
predict(dropout_model, newdata = examples, type = "response")
Student Chance of considering dropout
low stress, good support 1%
high stress, little support 42%

Explaining is not predicting

This model explains who considers dropping out.

Whether it can predict that for new students, and how to test that fairly, is a different question.

It is the subject of Chapters 11 and 12.

In your field: forestry and agriculture

tree_model <- lm(Volume ~ Girth + Height, data = trees)
summary(tree_model)$r.squared

Timber volume of 31 black cherry trees from measurements taken without felling them.

Girth and height explain 95% of volume, against 25% for GPA: physics is more predictable than people.

Practical lab: the Chapter 8 playground

Work through the playground exercises in your browser, with hints and solutions.

Every exercise also runs in RStudio, from the downloadable chapter project.

Practical exercises 1–3: ANOVA and regression

  1. Wellbeing by faculty: ANOVA, \(\eta^2\), and Tukey if significant.
  2. Wellbeing on sleep: interpret the slope and R-squared.
  3. Add stress and support: what happens to the sleep slope and R-squared?

Practical exercises 4–6: categories, odds, confounders

  1. Add study mode: name the reference and interpret its coefficient.
  2. Dropout on burnout alone: report and explain the odds ratio.
  3. Draw a confounder from your own field, and say how regression would separate it.

Try this yourself

Choose one outcome in the student wellbeing data.

  • plot it against one predictor;
  • fit a simple regression, then add two plausible confounders;
  • watch how the first coefficient changes;
  • check the residual plot;
  • write one sentence reporting the adjusted effect with its interval.

Troubleshooting guide (Part 1)

Symptom Likely cause
ten t-tests, one “significant” multiple testing; use ANOVA and post-hoc tests
significant ANOVA, unclear which group no post-hoc test
a coefficient changes sign when adding a predictor a confounder, or correlated predictors
fewer cases than expected lm() dropped rows with missing values

Troubleshooting guide (Part 2)

Symptom Likely cause
a U shape in the residuals a missed curve
a funnel in the residuals unequal spread; p-values unreliable
predicted probabilities above 1 linear regression on a yes-or-no outcome
coefficients hard to explain to readers log-odds not turned into odds ratios or probabilities

Completion checklist

Misconceptions to leave behind (Part 1)

Misconception Better mental model
a significant coefficient shows a cause an association adjusted for the model’s variables
a low R-squared makes a model useless a real effect can explain little variation

Misconceptions to leave behind (Part 2)

Misconception Better mental model
each coefficient describes its predictor alone each holds the others constant
a significant ANOVA shows all groups differ at least one differs; post-hoc tests say which

The chapter in one sentence

ANOVA and regression split variation into what the predictors explain and what remains, and each coefficient holds the others constant, but only the ones you measured.

Next: Chapter 9

The next chapter looks at many variables at once:

  • constructs and latent variables;
  • principal component analysis;
  • factor analysis of the questionnaire;
  • reporting a questionnaire’s structure;
  • cluster analysis for student profiles.

Questions

Which confounder could explain the main association in your own research?

Did you measure it?