Day 2 · Lab

Continuous distributions, linear models, and generalized linear models

From a Normal regression to a logistic GLM to a negative-binomial count model — with explicit emphasis on how to read coefficients on the log and logit scales as biology.

What's in this lab and what to hand in

This page is your in-class practice problem set — worked examples with visible answers. Your graded 20-point assignment is a separate problem set; write up your answers in the R Markdown template and submit on Canvas.

Morning · 20 pts
Continuous distributions problem set
Practice Walk through the morning problems on this page — answers visible, in-class
Assessed Graded problem set — answers NOT shown; graded — submit on Canvas
Hand-inmorning_lab_template.Rmd — knit to HTML and upload to Canvas
Afternoon · 20 pts
Linear models problem set
Practice Walk through the afternoon problems on this page — answers visible, in-class
Assessed Graded problem set — answers NOT shown; graded — submit on Canvas
Hand-inafternoon_lab_template.Rmd — knit to HTML and upload to Canvas

Need help with R Markdown? See the R Markdown tutorial. Other docs for this day: plain-language summary · practice answer key.

Data files & R scripts

Everything referenced in this lab's exercises lives in this folder. Click any link below to download.

Datasets

sockeye_adult.csvDaily sockeye salmon counts and flow-corrected eDNA (Qcorr_qPCR = qPCR eDNA concentration × stream flow) (Levi et al. 2019).
titanic_long.csvOne row per Titanic passenger (binary survived, class, sex, age).
titanic_prop.csvTitanic survival aggregated to (Yes, No) counts per class for binomial-form GLM.

R scripts

InteractionTerms.RInteractionTerms.R · interaction algebra & geometry walkthrough
Plain-language summary Day 2 — the ideas in normal English A short, no-math explanation of what a GLM is actually doing: why we need a link function at all, and what a log or logit coefficient means in biological terms. Read it before the lab to preview the ideas, or afterwards to consolidate what you did. Open the summary →
Helpful companions for today. Keep the log & logit intuition guide open while you work — it has 16 worked examples of reading GLM coefficients as biology. Also useful: the interactive link-function & logit visualizer.

Where we are

Yesterday we built probability and met the discrete distributions. This morning we'll meet the continuous distributions you need for regression: Normal, Lognormal, Gamma, Beta, Exponential. This afternoon we'll plug those distributions into a linear-model framework, generalize via link functions, and fit our first GLMs: logistic regression (Bernoulli/Binomial), Poisson regression with log link, and Negative-Binomial regression for overdispersed counts.

The unifying idea today: a GLM is a regression in which the linear predictor $\eta_i = \beta_0 + \beta_1 x_i + \dots$ is connected to a distribution's mean through a link function $g$, so that $g(\mu_i) = \eta_i$. The Normal regression you already know is just a GLM with the identity link. Everything else is a small variation on that template.

Plain-language overview

Regression is just "draw a line (or curve) through the cloud of data, then interpret what the slope means biologically." The catch is that the response variable might be a count, a proportion, or a strictly positive measurement — none of which behave like a Normal. The GLM framework lets you keep the line-drawing logic while choosing a distribution that fits the response.

Two new ideas you'll learn today:

  1. A link function stretches the response to a scale where a straight line makes sense. The log link stretches positive numbers; the logit link stretches probabilities.
  2. Once you fit the model, you have to back-transform coefficients to talk about them biologically. A slope of $+0.5$ on the log scale means a $\times 1.65$ multiplicative effect on the raw scale.

Read the full plain-language summary for Day 2 → — the same ideas worked through slowly, with examples and no math.

Linear model anatomy

A linear model has three pieces:

PieceWhat it isSymbol
Linear predictorWhat you add up from coefficients and predictors$\eta_i = \beta_0 + \beta_1 x_{i1} + \beta_2 x_{i2} + \ldots$
Link functionHow $\eta$ relates to the mean of the response$g(\mu_i) = \eta_i$
DistributionHow observations vary around the mean$y_i \sim D(\mu_i, \theta)$

The Normal regression you know from intro statistics is the special case $g(\mu) = \mu$ (identity link) and $D = \text{Normal}$:

$y_i \sim \mathcal{N}(\mu_i, \sigma^2), \quad \mu_i = \beta_0 + \beta_1 x_i$

Once we relax the link and the distribution, we get the GLM "menu":

ResponseDistributionLinkR family
Continuous, unboundedNormal (Gaussian)Identitygaussian("identity")
Binary (0/1) or proportion of $n$Bernoulli / BinomialLogitbinomial("logit")
Count, equidispersedPoissonLogpoisson("log")
Count, overdispersedNegative BinomialLogMASS::glm.nb
Continuous, strictly positiveGamma or LognormalLog (Gamma) or log on response (Lognormal)Gamma("log") or lm(log(y) ~ ...)

Continuous distributions (morning preview)

Normal — the workhorse continuous distribution

Story: sums of many small independent contributions (central limit theorem). Symmetric, no boundary, support $(-\infty, \infty)$.

Parameters: mean $\mu$, variance $\sigma^2$.

Use it when: the response can take any real value, with roughly symmetric scatter.

Caveat: almost no ecological variable is truly unbounded; check residuals after fitting.

Lognormal — when log(Y) is Normal

Story: products of many small independent contributions. Always positive, right-skewed, with multiplicative variation.

Parameters: $\mu, \sigma$ on the log scale; on the raw scale, mean is $e^{\mu + \sigma^2/2}$.

Use it when: the response is positive and right-skewed; the variability scales with the mean.

Ecology: body sizes, abundances at sites, eDNA concentrations.

Gamma — flexible positive continuous

Story: waiting time for $k$ independent Poisson events (sum of exponentials).

Parameters: shape $\alpha$, rate $\beta$. Mean $= \alpha/\beta$; variance $= \alpha/\beta^2$.

Use it when: response is positive and continuous; can be right-skewed but doesn't have to be.

Common GLM choice: glm(y ~ x, family=Gamma(link="log")) for positive responses where variance grows with mean.

Beta — for proportions between 0 and 1

Story: a distribution over probabilities. Two shape parameters $\alpha, \beta$.

Mean $= \alpha/(\alpha+\beta)$. Beta(1,1) is uniform on [0,1].

Use it when: response is a proportion that can take any value strictly between 0 and 1 (not a count out of a fixed total).

Ecology: percent cover, individual-level detection probabilities (the heterogeneity component of beta-binomial).

Exponential — waiting time for one event

Story: time until the first event in a Poisson process at rate $\lambda$. Memoryless.

Mean $= 1/\lambda$.

Ecology: time-between-detections in a camera-trap; line-transect distances to first sighting.

Practice — answers visible

Morning practice problems

The exercises below are walk-through practice. Your graded morning assignment (Continuous distributions problem set) uses a separate, novel problem set — open the graded problem set below and write your answers in morning_lab_template.Rmd.

Open the graded morning problem set →    morning_lab_template.Rmd ↓

Morning exercises

Exercise M1 · Exponential

Orangutan line transects

The detection function for orangutans on a line transect is roughly exponential, with rate $\lambda = 0.02$ per meter of perpendicular distance. Derive the survival function (probability of not yet detecting at distance $x$) and use it to compute the probability of missing an orangutan at 50 m.

Reveal

Derive the survival function — two ways. "Not yet detected at distance $x$" means the detection distance $X$ exceeds $x$, so we want the survival function $S(x) = P(X \ge x)$. There are two routes to it, and it is worth seeing both.

Way 1 — from first principles (integrate the density). The Exponential$(\lambda)$ density is $f(t) = \lambda e^{-\lambda t}$ for $t \ge 0$. Accumulate all the probability beyond $x$ by integrating from $x$ outward:

$S(x) = P(X \ge x) = \int_x^\infty \lambda e^{-\lambda t}\,dt = \big[-e^{-\lambda t}\big]_x^\infty = e^{-\lambda x}.$

Way 2 — from the CDF you already know (no integral). Once you recognize that a "distance to first detection under a constant per-meter detection hazard" is an Exponential random variable, you can skip the calculus. The Exponential CDF is a standard result, $F(x) = P(X \le x) = 1 - e^{-\lambda x}$, and the survival function is simply its complement:

$S(x) = P(X \ge x) = 1 - F(x) = 1 - \big(1 - e^{-\lambda x}\big) = e^{-\lambda x}.$

Same answer, and Way 2 takes one line — the payoff for naming the distribution instead of re-integrating. (Way 1 is really just the on-the-spot derivation of that CDF; the two are the same calculation, done from different starting points.)

The general rule to carry away: for any exponential process the survival function is a pure exponential decay $e^{-\lambda x}$, so the fraction still undetected drops by the constant factor $e^{-\lambda}$ per meter — and, exactly as in the log-link redd example, there is a "detection half-distance" $\ln(2)/\lambda$ at which half the animals remain undetected. Here $\ln(2)/0.02 = 34.7$ m.

Now plug in $\lambda = 0.02$ per meter and $x = 50$ m, so $\lambda x = 1$:

$S(50) = e^{-\lambda x} = e^{-1.0} = 0.368.$

A 50-m-distant orangutan has about a 37% chance of being missed — just past the half-distance, so a bit more than half are still detectable.

Exercise M2 · Normal linear transformations

Prove that linear transformations of Normals are Normal

Let $Z \sim \mathcal{N}(0,1)$ and $Y = \mu + \sigma Z$. Using $E[aZ + b] = a E[Z] + b$ and $\mathrm{Var}(aZ+b) = a^2 \mathrm{Var}(Z)$, show $E[Y] = \mu$ and $\mathrm{Var}(Y) = \sigma^2$.

Reveal

Mean — one step at a time.

$E[Y] = E[\mu + \sigma Z]$   (substitute $Y = \mu + \sigma Z$)

$\quad = \mu + \sigma\,E[Z]$   (by linearity of expectation, $E[aX+b] = a\,E[X] + b$, here with $a=\sigma,\ b=\mu$)

$\quad = \mu + \sigma\cdot 0$   (because $E[Z]=0$ for $Z\sim\mathcal N(0,1)$)

$\quad = \mu.$

Variance — one step at a time.

$\mathrm{Var}(Y) = \mathrm{Var}(\mu + \sigma Z)$   (substitute $Y = \mu + \sigma Z$)

$\quad = \mathrm{Var}(\sigma Z)$   (adding a constant shifts location but not spread: $\mathrm{Var}(X+b) = \mathrm{Var}(X)$)

$\quad = \sigma^2\,\mathrm{Var}(Z)$   (by the scaling rule $\mathrm{Var}(aX+b) = a^2\,\mathrm{Var}(X)$, here $a=\sigma$)

$\quad = \sigma^2\cdot 1$   (because $\mathrm{Var}(Z)=1$ for $Z\sim\mathcal N(0,1)$)

$\quad = \sigma^2.$

Shape. The distribution stays Normal because a linear transformation of a Gaussian is Gaussian — the moment-generating function of $\mu+\sigma Z$, namely $e^{\mu t + \sigma^2 t^2/2}$, is exactly the MGF of a $\mathcal N(\mu,\sigma^2)$. So $Y\sim\mathcal N(\mu,\sigma^2)$.

Exercise M3 · Beta-Binomial

Marten cameras with heterogeneous detection

200 trail cameras are deployed. Plain assumption: per-camera detection probability $p = 0.03$ each night. But cameras vary: $p_i \sim \text{Beta}(0.03 \cdot \phi, 0.97 \cdot \phi)$ with $\phi = 1$ (so beta is very spread out).

  1. What is the mean of this Beta?
  2. Each of 20 cameras runs for 10 nights, and a camera's detectability $p_i$ is drawn once and reused for all 10 of its nights. Simulate 5000 deployments under a plain Binomial(200, 0.03) and under this Beta-Binomial. Compare P(all-zero) and the mean/SD.
  3. Why does the Beta-Binomial put much more mass on "all-zero"? What would happen if each camera-night drew its own independent $p$?
Reveal
set.seed(1); reps <- 5000
K <- 20; J <- 10 # 20 cameras x 10 nights = 200 camera-nights

bin <- rbinom(reps, K*J, 0.03)
betbin <- replicate(reps, {
 p <- rbeta(K, 0.03, 0.97) # ONE p per camera...
 sum(rbinom(K, J, p)) # ...reused across its 10 nights
})
mean(bin==0); mean(betbin==0) # 0.003 vs 0.166
mean(bin); mean(betbin) # 6.0 vs 6.0 — same mean
sd(bin); sd(betbin) # 2.5 vs 5.6 — about 5x the variance

(c) With $\phi$ this small the Beta piles $p_i$ up near 0 or 1, so most cameras are near-blind while a few are hot. A camera that draws a tiny $p_i$ contributes zero on all ten of its nights, so all-zero deployments become common (0.166 vs 0.003) — even though the mean is unchanged at ~6. That is overdispersion: same mean, fatter tails.

The clustering is the whole point. If each camera-night drew its own independent $p$, the heterogeneity would integrate straight out: marginally each night is Bernoulli($E[p] = 0.03$) and the nights are independent, so the total would be exactly Binomial(200, 0.03) — same mean, same SD, no overdispersion at all. Try it: swap the two lines above for sum(rbinom(200, 1, rbeta(200, 0.03, 0.97))) and you get SD 2.4, indistinguishable from the plain binomial. Overdispersion needs a $p$ that is shared across several observations — which is exactly why the Titanic and sockeye data overdisperse: repeated observations share a group.

Practice — answers visible

Afternoon practice problems

The exercises below are walk-through practice. Your graded afternoon assignment (Linear models problem set) uses a separate, novel problem set — open the graded problem set below and write your answers in afternoon_lab_template.Rmd.

Open the graded afternoon problem set →    afternoon_lab_template.Rmd ↓

Afternoon exercises

Exercise A1 · Normal vs Poisson vs NB on eDNA counts

Sockeye eDNA — find the right distribution

Load sockeye_adult.csv from this Day's data/ folder. It is in long format (one row per Sockeyetype per day), so first subset to adult sockeye. The response is the daily Count of adult sockeye; the predictor is Qcorr_qPCR, the flow-corrected eDNA signal — the raw qPCR eDNA concentration multiplied by stream discharge (flow). Multiplying by flow converts a concentration into a quantity proportional to the total amount of eDNA transported past the sampler, which tracks fish abundance better than concentration alone. Build a Year factor from Date and a log-transformed eDNA predictor:

library(MASS)
sock <- read.csv("data/sockeye_adult.csv", stringsAsFactors = FALSE)
table(sock$Sockeyetype)                            # confirm the adult level name
d <- subset(sock, Sockeyetype == "Sockeye_Adults")
d$Year <- factor(sub(".*/", "", d$Date))           # "15" or "16"
d <- d[!is.na(d$Count) & !is.na(d$Qcorr_qPCR), ]   # complete cases (n = 55)
min_pos <- min(d$Qcorr_qPCR[d$Qcorr_qPCR > 0])      # small constant so log() is finite
d$logED <- log(d$Qcorr_qPCR + min_pos)

Fit, in sequence:

  1. lm(log(Count + 1) ~ logED + Year, data = d) — the old-school log-transform approach.
  2. glm(Count ~ logED + Year, family = poisson, data = d).
  3. glm(Count ~ logED + Year, family = quasipoisson, data = d).
  4. MASS::glm.nb(Count ~ logED + Year, data = d).

For each fit:

  • Examine residual deviance / df. Is overdispersion present?
  • Look at SEs for the eDNA coefficient. Which models give realistically wide vs. unrealistically narrow CIs?
  • Back-transform the log-link slope to a multiplicative effect: "a doubling of the flow-corrected eDNA signal is associated with a ×__ change in expected daily count."
Reveal worked solution
f_lm <- lm(log(Count + 1) ~ logED + Year, data = d)
f_p  <- glm(Count ~ logED + Year, family = poisson,      data = d)
f_qp <- glm(Count ~ logED + Year, family = quasipoisson, data = d)
f_nb <- MASS::glm.nb(Count ~ logED + Year, data = d)

# overdispersion diagnostic for the Poisson
f_p$deviance / f_p$df.residual              # 2296.6 / 52 = 44.2
summary(f_qp)$dispersion                     # 51.0
f_nb$deviance / f_nb$df.residual             # 48.8 / 52 = 0.94  (theta = 0.73)
2^coef(f_nb)["logED"]                         # 1.86
FitlogED slopeSE(logED)dispersion / (resid. dev ÷ df)
1. lm(log(Count+1))0.5700.047
2. Poisson0.7370.013resid.dev/df = 2296.6/52 = 44.2
3. Quasipoisson0.7370.095dispersion = 51.0
4. Negative binomial0.8970.081resid.dev/df = 48.8/52 = 0.94, $\theta = 0.73$

Overdispersion. The Poisson residual deviance is 44× its df — massive overdispersion, so its SE on logED (0.013) is far too small. The quasipoisson rescales that SE by $\sqrt{51}\approx 7.1$ to 0.095; the negative binomial (a proper likelihood, $\theta = 0.73$) gives 0.081 and a residual-deviance ratio near 1 (0.94) — a well-calibrated fit. Trust fits 3 and 4, not the naive Poisson.

Back-transform — where the ×1.86 comes from. Don't memorize the recipe; derive it, because the same three steps turn any log-link slope into a sentence about biology. Start from what the model literally says. With a log link and a log-transformed predictor,

$\log \mathbb{E}[\text{Count}] = \beta_0 + \beta_1\,\log(\text{eDNA}) + (\text{Year}),$

and exponentiating both sides puts it on the count scale:

$\mathbb{E}[\text{Count}] = e^{\beta_0}\,\cdot\,(\text{eDNA})^{\beta_1}\,\cdot\,e^{(\text{Year})}.$

Because we logged the predictor, eDNA enters as a power law with exponent $\beta_1$ — and that is what makes the effect multiplicative. Now ask the actual question: what happens to the expected count when the eDNA signal doubles, $\text{eDNA} \to 2\,\text{eDNA}$, with Year held fixed? Take the ratio of the new expected count to the old, so everything that didn't change cancels:

$\dfrac{\mathbb{E}[\text{Count}\mid 2\,\text{eDNA}]}{\mathbb{E}[\text{Count}\mid \text{eDNA}]} = \dfrac{e^{\beta_0}\,(2\,\text{eDNA})^{\beta_1}\,e^{(\text{Year})}}{e^{\beta_0}\,(\text{eDNA})^{\beta_1}\,e^{(\text{Year})}} = \dfrac{(2\,\text{eDNA})^{\beta_1}}{(\text{eDNA})^{\beta_1}} = 2^{\beta_1}.$

The intercept, the Year term, and even the baseline eDNA level all cancel — only the factor you multiplied the predictor by survives, raised to $\beta_1$. That is the general result, and it is worth keeping: multiply a log-transformed predictor by any factor $c$, and the expected response multiplies by $c^{\beta_1}$ (doubling is just $c = 2$; a 50% rise is $c = 1.5$, giving $1.5^{0.897}=1.44$, a 44% increase). It is the same fact seen from the link scale — doubling adds $\log 2$ to $\log(\text{eDNA})$, hence adds $\beta_1\log 2$ to the linear predictor, and $e^{\beta_1\log 2}=2^{\beta_1}$ back on the response scale. Additive on the link, multiplicative on the response.

Only now do the numbers enter. Plug in the NB estimate $\hat\beta_1 = 0.897$:

$2^{0.897} = e^{0.897\,\times\,\ln 2} = e^{0.897\,\times\,0.693} = e^{0.622} = 1.86.$

So a doubling of the flow-corrected eDNA signal is associated with roughly an 86% increase (×1.86) in the expected daily adult sockeye count — matching the graded Problem 5. "The slope was 0.90 (SE 0.08)" is not a finding; "doubling the eDNA signal is associated with 86% more sockeye" is the sentence that goes in the paper.

Where the biology lives. The NB log-link slope here is $\hat\beta = 0.897$, so $e^{0.897 \cdot \ln 2} = 2^{0.897} = 1.86$. A doubling of the flow-corrected eDNA signal is associated with an ~86% increase in expected daily sockeye. That sentence is what you put in your paper — not "the slope was 0.90 (SE 0.08)."
Exercise A2 · Titanic logistic regression — long vs aggregated

Titanic survival

Using titanic_long.csv and titanic_prop.csv:

  1. Fit glm(survived ~ class, family=binomial, data=titanic_long).
  2. Fit glm(cbind(Yes, No) ~ Class, family=binomial, data=titanic_prop).
  3. Confirm that the two fits agree — identical fitted probabilities, and identical coefficients once the two data frames use the same reference class. Why must they?
  4. Convert each class's log-odds to a probability. Compare to raw proportions.
Reveal solution
long <- read.csv("data/titanic_long.csv")
prop <- read.csv("data/titanic_prop.csv")
f1 <- glm(survived ~ class, family=binomial, data=long)
f2 <- glm(cbind(Yes, No) ~ Class, family=binomial, data=prop)

# Fitted survival probability per class -- identical across the two forms
plogis(coef(f1)[1] + c(0, coef(f1)[-1]))    # crew, first, second, third
tapply(long$survived, long$class, mean)      # exactly the same numbers
# titanic_prop has one row per Class x Sex x Age, so aggregate to Class first:
tapply(prop$Yes, prop$Class, sum) / tapply(prop$Yes + prop$No, prop$Class, sum)

Fitted probabilities per class come out crew 0.240, first 0.625, second 0.414, third 0.252 — identical to the raw proportions (a single categorical predictor gives a saturated model, so each group's MLE is just its empirical proportion).

(c) Why the two forms agree. Write $y_i\in\{0,1\}$ for each passenger and group them by class. For a group with $n$ passengers and $y=\sum y_i$ survivors, the two likelihoods are

$\underbrace{\prod_{i=1}^{n} p^{y_i}(1-p)^{1-y_i}}_{\text{Bernoulli, one row per person}} \;=\; p^{y}(1-p)^{n-y}$   vs.   $\underbrace{\binom{n}{y}\,p^{y}(1-p)^{n-y}}_{\text{Binomial, aggregated counts}}$

They differ only by the factor $\binom{n}{y}$ — the number of ways to arrange $y$ survivors among $n$ people. Taking logs, the two log-likelihoods differ by the additive constant $\log\binom{n}{y}$, which does not contain $p$. Because the MLE and its standard errors depend only on how the log-likelihood changes with $p$ (its derivatives in $p$), a term free of $p$ drops out: the estimated coefficients, fitted probabilities, and SEs are identical. (The coefficient tables can still look different if the two data frames sort their factor levels differently and thus pick different reference classes — align the references and the numbers match exactly.)

What the constant does shift is the absolute log-likelihood, and therefore the AIC. Aggregating the Titanic passengers into the class×sex×age cells of titanic_prop.csv gives $\sum_{\text{groups}}\log\binom{n}{y} = 1025.44$, so the Binomial log-likelihood sits exactly that far above the Bernoulli one ($-268.84$ vs $-1294.28$) and the AICs differ by $2 \times 1025.44 = 2050.87$.

The residual deviance is a different story, and it is worth getting right. Deviance is $2(\ell_{\text{saturated}} - \ell_{\text{model}})$, and $\log\binom{n}{y}$ sits in both terms — so it cancels, and the deviance does not shift by that constant. The two deviances still differ (Bernoulli 2588.56 vs Binomial 491.06, a gap of 2097.49 — which is neither 1025.44 nor 2050.87), but for another reason entirely: the saturated reference model changes. The Bernoulli form's saturated model gets one parameter per passenger and so fits every row perfectly; the Binomial form's gets only one per cell. The gap is exactly the Bernoulli deviance of that cell-saturated model — the individual-level noise that aggregation simply stops measuring.

deviance(f1)                                                             # 2588.555  Bernoulli
deviance(f2)                                                             #  491.061  Binomial
deviance(glm(survived ~ class*sex*age, family = binomial, data = long))  # 2097.495  <- the gap

So never compare the deviance or AIC of a Bernoulli (one-row-per-individual) fit against an aggregated Binomial fit. The AIC gap is bookkeeping; the deviance gap is a change of yardstick. Neither is evidence about model fit. Within one data form, both are still perfectly good for comparing nested models.

Exercise A3 · Titanic with interactions

Sex × Class effects on survival

Fit glm(survived ~ class * sex + age, family=binomial, data=titanic_long). Use effects::allEffects() to plot fitted probabilities, and use emmeans::emmeans(fit, pairwise ~ class | sex) to compare classes within each sex.

(Note: in this dataset age is a two-level factor, adult vs child — not a number in years.)

  1. Which pairwise contrast is largest (in odds ratio)?
  2. Why is the simple TukeyHSD on an aov object the wrong way to do this comparison?
  3. Translate the female 1st-vs-3rd-class log-odds difference into a probability difference for an adult passenger.
Reveal worked solution
library(effects); library(emmeans)
long <- read.csv("data/titanic_long.csv", stringsAsFactors = TRUE)
fit  <- glm(survived ~ class * sex + age, family = binomial, data = long)
plot(allEffects(fit))                                  # fitted probabilities
emmeans(fit, pairwise ~ class | sex, type = "response")  # class contrasts within each sex

(a) Largest pairwise odds ratio. The emmeans pairwise output (odds-ratio scale) gives, among females, first-vs-third class as the largest contrast:

# sex = female:
#  contrast        odds.ratio     SE  z.ratio  p.value
#  first / third        48.7    25.7    7.35   <.0001
# sex = male:
#  first / third         2.78    0.55   5.14    <.0001

First-class women had about 49× the odds of survival of third-class women — the sharpest class gradient in the model. Among men the same first-vs-third contrast is only ~2.8×: the "women and children first" protocol compressed the class effect for men.

(b) Why not TukeyHSD on an aov? TukeyHSD operates on an aov object, which assumes Normal errors, constant variance, and an identity link. A survival GLM has Bernoulli errors (variance $p(1-p)$, not constant) and a logit link, so Tukey on an aov would use the wrong likelihood, ignore the link, and ignore the interaction. emmeans on the fitted GLM forms the contrasts on the correct log-odds scale (optionally back-transformed to the response scale) with proper delta-method SEs and a Tukey multiplicity adjustment.

(c) Female 1st-vs-3rd, adult passenger.

predict(fit,
        newdata = data.frame(class = c("first","third"), sex = "female", age = "adult"),
        type = "response")
#     1st        3rd
#  0.972      0.419

An adult first-class woman is modeled at $p = 0.972$ versus $p = 0.419$ for an adult third-class woman — a probability difference of about 0.55 (55 percentage points). On the link scale that is a log-odds gap of $3.56 - (-0.33) = 3.89$, i.e. an odds ratio of $e^{3.89} \approx 49$, matching part (a). The odds ratio and the probability difference describe the same effect on two scales; report both, because a huge odds ratio near the probability ceiling still corresponds to a bounded (though here still large) probability gap.

Careful: emmeans and predict answer different questions here. Ask for the fitted probabilities with emmeans(fit, ~ class | sex, type = "response") instead and the same two cells come back as 0.983 and 0.550 — a difference of 0.433, not 0.55. (The graded Problem 7 takes that route, so expect to see both numbers today.) Neither is wrong.

emmeans marginalises over age: it averages the adult and child linear predictors on the logit scale and back-transforms once (for third-class females, $\tfrac{1}{2}(-0.33 + 0.73) = 0.20$, and $\text{plogis}(0.20) = 0.550$). predict(..., age = "adult") pins age at adult and reports a real, specific kind of passenger ($\text{plogis}(-0.33) = 0.419$). Since agechild is a big positive coefficient ($+1.05$), age-averaging pushes both probabilities up — and shrinks the gap, because first class is already pressed against the ceiling while third class still has room to move.

This is the same lesson as the log link: averaging on the link scale and then back-transforming is not the same as back-transforming and then averaging. Say which one you did. The odds ratio (~49×) is identical either way, because it is a link-scale contrast in which the age term cancels.

Interaction terms: algebra meets geometry

An interaction in a linear model means "the effect of one variable depends on the value of another." The companion script InteractionTerms.R demonstrates this with simulated data, 2D heatmaps, and 3D plotly surfaces. Four lessons to leave with:

  1. If the true surface is a plane, there is no interaction. A model with $y \sim x_1 \cdot x_2$ should return an interaction coefficient near zero.
  2. If the surface is twisted (saddle/warped), there is an interaction. The sign of the interaction tells you whether the effect of $x_1$ is amplified or dampened as $x_2$ grows.
  3. Interactions on a probability scale can look very different from interactions on a log-odds scale (compare the binary GLM heatmap with the linear predictor's heatmap).
  4. The phrase "is there an interaction?" depends on the response scale. A model that is additive on the log scale is multiplicative on the raw scale — that's not a bug, it's the link function at work.
A useful sanity check: if you have to log-transform your response, then any "additive" model you fit on the log scale is actually a multiplicative model on the raw scale. So "no interaction" in your fit might mask a real biological interaction, and vice versa. Always be explicit about the scale.

R cheat sheet — GLMs

ModelR callLinkNotes
Normal regressionlm(y ~ x)identityresiduals checked with plot(fit)
Logisticglm(y ~ x, family=binomial)logitexp(coef) = odds ratios
Poissonglm(y ~ x, family=poisson)logexp(coef) = multiplicative rate
Quasipoissonfamily=quasipoissonlogno AIC; SEs inflated for overdispersion
Negative binomialMASS::glm.nb(y ~ x)logproper likelihood; AIC OK
Gammaglm(y ~ x, family=Gamma(link="log"))logpositive continuous responses
If your residual deviance is much bigger than your residual df in a Poisson fit, you have overdispersion. Don't trust the SEs. Switch to NB.
→ Log/logit intuition guide → Full answer key