Activity: Linear Mixed Models
Random effects for nested data
Worksheet: Linear Mixed Models
How to use this worksheet
Work through each part in order, at your own pace. Type every line of code yourself into a plain R script — do not copy-paste. Blocks marked ▶ Run this are code you should type and execute. Blocks marked ✏️ Your turn ask you to write, modify, or answer something. Boxes marked 🚀 If you finish early are optional bonus material that goes a bit further than what we covered in lecture.
This worksheet picks up the exact same sea urchin grazing / algae cover dataset from the Nested ANOVA activity —
patchnested withintreat— and fits it as a mixed model instead of a traditionalaov()with anError()term.
Part 1 · Setup — The Sea Urchin Data, Again
▶ Run this in your Script:
library(dotwhisker)
library(janitor)
library(car) # For Levene's test and Type III SS
library(lme4) # For mixed-effects models
library(lmerTest) # For p-values in mixed models
library(emmeans) # For estimated marginal means
library(performance) # For model diagnostics
library(DHARMa) # For residual diagnostics
library(tidyverse)
options(scipen = 999)
options(contrasts = c("contr.treatment", "contr.poly"))
u_df <- read_csv("data/andrew.csv") %>% clean_names()
u_df <- u_df %>%
mutate(treat = case_when(
treat == "removal" ~ "none",
treat == "control" ~ "high",
treat == "dens_33" ~ "low",
treat == "dens_66" ~ "medium",
TRUE ~ "other")) %>%
mutate(treat = factor(treat,
levels = c("none", "low", "medium", "high"),
labels = c("none", "low", "medium", "high")),
patch = as_factor(patch))
head(u_df)
levels(u_df$treat)u_df contains: patch (random patches 1–16 where treatments were applied), treat (urchin density treatment — none, low, medium, high), quad (replicate quadrats within each patch:treatment combination), algae (percentage cover of filamentous algae — the response variable).
🖐 Notice
If you still have nested_model from the Nested ANOVA activity in your environment, keep it around — Part 4 asks you to compare its p-value to what you get here.
Part 2 · The Modern Way — Mixed Models
Fixed effects go in as a plain variable name. Random effects can be coded a few different ways, depending on the design:
- Random intercept, fixed slope:
+ (1|random)— e.g.,color ~ fixed + (1|group) - Random intercept, random slope:
+ (fixed|random)— e.g.,color ~ fixed + (fixed|group) - Nested design (a sample can exist only within a larger grouping):
y ~ color + (1|greenbox/graybox), equivalent toy ~ color + (1|greenbox) + (1|greenbox:graybox)— this models random variation in the intercept for each patch, and also for each quadrat within each patch - Fully crossed design (not nested):
y ~ color + (1|green_box) + (1|gray_box)
▶ Run this — fit the mixed model:
mixed_model <- lmer(algae ~ treat + (1|patch), data = u_df,
control = lmerControl(calc.derivs = FALSE))
summary(mixed_model)▶ Run this — Method 1, the F-test with Satterthwaite degrees of freedom (more conservative, better for small samples):
satt_result <- Anova(mixed_model, type = 3,
test.statistic = "F",
ddf = "Satterthwaite")
satt_result▶ Run this — Method 2, the Chi-square test (more liberal, assumes large samples):
anova_car <- Anova(mixed_model,
type = 3,
test.statistic = "Chisq")
anova_car💡 Chi-square = F × numerator df. The two differ because the F-test accounts for the denominator df (reflecting sample size), while Chi-square assumes infinite denominator df. Rule of thumb: under 100 observations or < 20 random-effect levels, use the F-test; over 500 observations and > 50 random-effect levels, Chi-square is fine; in between, the F-test is safer.
✏️ Your turn: Do the F-test and Chi-square p-values agree on significance here? Given the rule of thumb above, which one should you trust more for this dataset (16 patches)? ________________________
🚀 If you finish early: Refit mixed_model using (1|treat:patch) instead of (1|patch). Does the model summary change? (Hint: since patch numbers are already unique to a treatment in this dataset, the two notations should behave the same here — but they wouldn’t if patch IDs were reused across treatments.)
# Write your code here:Part 3 · Model Diagnostics
▶ Run this — DHARMa’s simulation-based diagnostics, purpose-built for mixed models:
set.seed(123)
simulation_output <- simulateResiduals(fittedModel = mixed_model,
plot = FALSE)
plotQQunif(simulation_output)
plotResiduals(simulation_output)
testDispersion(simulation_output)
testOutliers(simulation_output)▶ Run this — a couple of other useful diagnostic views:
plot(mixed_model, type = c("p", "smooth"))
# Cook's-distance-style influence plot (values > 0.5 are worth a look)
car::influencePlot(mixed_model)
# Coefficient plot for the fixed effects
dotwhisker::dwplot(mixed_model, effects = "fixed") +
geom_vline(xintercept = 0, color = "darkblue", linewidth = 1)
# Variance components
VarCorr(mixed_model)✏️ Your turn: From VarCorr(mixed_model), how does the variance attributed to patch compare to the residual variance? What does that tell you about spatial heterogeneity in this system? ________________________
🚀 If you finish early: DHARMa’s testDispersion() checks for over/under-dispersion. Look up (?testDispersion) what a significant result would mean, and write a one-sentence summary.
________________________________________________________
Scientific interpretation
This mixed-model analysis reveals substantial spatial heterogeneity in algae cover, with significant variation among patches within each treatment. Whether the urchin-density treatment effect itself reaches significance depends on which test (F vs. Chi-square) you use — a good reminder that “significant” is sensitive to analytical choices in nested designs. The substantial variance component associated with patches nested within treatments shows why spatial heterogeneity needs to be accounted for when designing and analyzing ecological field experiments.
✏️ Your turn: Write a 2–3 sentence results paragraph for this analysis, using your own F-statistic, df, p-value, and variance component numbers from Parts 2–3.
________________________________________________________
________________________________________________________
Part 4 · Comparing the Two Approaches
The linear mixed-model approach (lmer) gives similar results to the traditional nested ANOVA (aov with an Error() term) from last class. The main advantage of the mixed-model approach is its more elegant handling of random effects, easier extension to unbalanced or more complex designs, and the extensive diagnostic tools available through packages like DHARMa.
✏️ Your turn: Compare the p-value for treat you got from nested_model in the Nested ANOVA activity to the p-value from satt_result here. Are they close? Which approach would you reach for first on a future nested design of your own, and why? ________________________
Review and checkpoint
At this point you can:
📤 What to turn in before next class
Upload both of these to the course management system:
- Your code — the
scripts/folder (or just15_linear_mixed_models.R) - This worksheet, with your written answers
Part 5 · Take-Home Extension — A Linear Mixed Model for Crayfish Growth
Due Wednesday, November 4 — before the next class.
This is your first time building a mixed model entirely from scratch, using the same lmer() syntax from Part 2, on a new dataset with a different kind of random effect (lake, rather than nested patches).
Background
Sargent & Lodge (2014) reared young-of-year rusty crayfish (Orconectes rusticus) from native (Ohio) and invasive (Wisconsin) populations in enclosures across three northern Wisconsin lakes. Individual crayfish within the same lake share that lake’s conditions, so they’re not independent of each other — lake should be treated as a random effect.
▶ Run this:
cray_df <- read_csv("data/sargent_lodge_crayfish.csv")
head(cray_df)Your Task
Question: Does crayfish growth rate differ between native and invasive populations, once you account for the fact that crayfish were grown in different lakes?
✏️ Your turn: State your hypotheses for the fixed effect of range. Decide how you want to specify the random effect of lake (a random intercept is enough here — this is not a nested design like Part 2’s patches-within-treatments). Justify your model, fit it, run diagnostics, interpret the fixed effect and the variance components, and report the result properly.
H0 =
________________________________________________________
Ha =
________________________________________________________
Model I will use and why:
________________________________________________________
# Write your code here:Interpretation:
________________________________________________________
________________________________________________________
Final Figure
Produce one publication-quality figure comparing growth rate between ranges — proper axis labels, no default ggplot grey background — and export it with ggsave().
# Write your code here:📤 What to turn in — due Nov 4
Your write-up should include:
Reference
Sargent, L.W. & Lodge, D.M. (2014). Evolution of invasive traits in nonindigenous species: increased survival and faster growth in invasive populations of rusty crayfish (Orconectes rusticus). Evolutionary Applications, 7, 949–961.
Getting unstuck
When code breaks — and it will, that is normal:
- Read the error message out loud. R usually names the line and the problem.
- Check the usual suspects: did you run
library(lme4),library(lmerTest),library(car),library(emmeans),library(DHARMa), andlibrary(tidyverse)? Spelling? A missing)or%>%at the start of a line? ?function_nameopens the built-in help page.- Bring the exact error (copy-paste it) to class or office hours.
💡 Key idea: Every working scientist googles error messages daily. Getting stuck is not failing — it is the job.