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 work is a separate problem set. The morning session's Continuous distributions problem set is worth 20 points and the afternoon session's Linear models problem set is worth 20 points, for 40 points across Day 2. Both live on the same page (the graded problem set); write up your answers in the R Markdown template for each session and submit them as two Canvas assignments.

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.

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 is 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 is that 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 are 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

Practice Exercise M1 · Exponential

Orangutan line transects

Suppose you conduct line transects looking for orangutans. If orangutans are randomly placed in the forest, then the time (or distance) it takes before you see an orangutan follows an exponential distribution.

Recall that an exponential random variable $X \sim \text{exp}(\lambda)$ has mean $1/\lambda$ and pdf given by $f(x) = \lambda e^{-\lambda x}$ on $x \ge 0$.

  1. What is the probability that you do not see an orangutan by distance $x$. That is, what is $P(X \ge x)$?
Reveal

"Not seen by distance $x$" means the distance you had to travel exceeded $x$, so we want $P(X \ge x)$, the survival function. There are two routes to it, and it is worth seeing both.

Way 1: from first principles (integrate the density). Accumulate all the probability beyond $x$ by integrating the pdf from $x$ outward:

$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 have named the distribution, you can skip the calculus. The Exponential CDF is a standard result, $F(x) = P(X \le x) = 1 - e^{-\lambda x}$, and what we want is its complement:

$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. That is the payoff for recognising the distribution instead of re-deriving it; Way 1 is really just the on-the-spot derivation of that CDF.

The general result to carry away is that for any exponential process the survival function is a pure exponential decay, $P(X \ge x) = e^{-\lambda x}$. The fraction still unseen falls by the constant factor $e^{-\lambda}$ per extra meter, so there is a "half-distance" $\ln(2)/\lambda$ at which half the orangutans remain unseen, exactly like a half-life.

Practice 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)$.

Practice Exercise M3 · Beta-Binomial

Marten cameras with heterogeneous detection

Twenty trail cameras are deployed for 10 nights each, 200 camera-nights in total. The plain assumption is a per-camera detection probability $p = 0.03$ each night. But cameras vary, with $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"?
Reveal

(a) The Beta has mean $\dfrac{\alpha}{\alpha+\beta} = \dfrac{0.03\phi}{0.03\phi + 0.97\phi} = 0.03$: the $\phi$ cancels, so every choice of $\phi$ leaves the mean at exactly the 0.03 of the plain assumption. $\phi$ sets only the spread. Small $\phi$ makes cameras wildly unequal, large $\phi$ makes them nearly identical. That is why the two simulations below share a mean of ~6 and differ only in their tails.

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, the same mean with fatter tails.

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

Practice 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. That is 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, and 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, since 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. "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)."
Practice 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, with identical fitted probabilities and identical coefficients once the two data frames use the same reference class.
  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).

Practice 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.

What emmeans does. "emmeans" is short for estimated marginal means. Given a fitted model it reports the model's predicted value for each level of a factor, and the differences between those levels, with correct standard errors. Two things make it worth reaching for instead of reading coefficients off summary(): it forms contrasts on the model's own link scale and can back-transform them for you, and it handles multiplicity when you ask for all pairwise comparisons.

The syntax is a formula naming what you want compared. emmeans(fit, ~ class) gives one estimate per class; ~ class | sex repeats the class comparison separately within each sex; and prefixing pairwise, as in pairwise ~ class | sex, adds every pairwise difference. Add type = "response" to get probabilities and odds ratios instead of log-odds. One thing to know now is that any predictor you do not name is averaged over, so emmeans describes an average passenger while predict(..., age = "adult") describes a specific one. They give different numbers on purpose.

(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.

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. Before you open it, spend a few minutes with the interactive interaction explorer: drag the interaction coefficient and watch the same model under four views: fanning lines, a heatmap, contours, and the effect of $x_1$ plotted against $x_2$. Setting its link to logit while holding the interaction at exactly zero is the quickest way to see the fourth lesson below. 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 is not a bug, it is 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 for 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