Statistical note
Generalized linear models: when your outcome isn't a bell curve
Ordinary linear regression assumes continuous, symmetric, constant-variance noise. GLMs swap in a distribution and link function that actually match counts, binary outcomes, or skewed intensities.
Ordinary linear regression quietly assumes the outcome is roughly continuous, symmetric, and has constant-variance noise around the trend — an assumption that fits body-weight measurements reasonably well and fits puncta counts, yes/no outcomes, or right-skewed intensities poorly. Generalized linear models (GLMs) keep the same “linear combination of predictors” backbone but let you choose a probability distribution and a link function that actually match the outcome’s real behaviour, instead of forcing everything through the Gaussian assumption.
The two choices that define a GLM
A GLM has two moving parts, chosen from the outcome type rather than fit to make residuals look nice after the fact:
- The distribution family — Gaussian for roughly symmetric continuous outcomes, Binomial for yes/no or proportion outcomes, Poisson (or negative binomial, if overdispersed) for counts, Gamma for positive, right-skewed continuous measurements like intensities.
- The link function — connects the linear predictor to the outcome’s mean on an appropriate scale: identity link for Gaussian, logit link for Binomial (keeps predicted probabilities between 0 and 1), log link for Poisson and Gamma (keeps predicted counts and intensities positive).
Getting the distribution wrong doesn’t just misstate uncertainty the way a mild normality violation might — it can produce nonsensical predictions, like a linear-regression fit that predicts a negative cell count.
Worked example: modelling puncta counts correctly
import numpy as np
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf
rng = np.random.default_rng(11)
condition = np.repeat([0, 1], 30)
true_rate = np.exp(1.6 + 0.5 * condition) # log link: rate is always positive
counts = rng.poisson(true_rate)
df = pd.DataFrame({'condition': condition, 'counts': counts})
model = smf.glm('counts ~ condition', data=df, family=sm.families.Poisson())
fit = model.fit()
print(fit.summary().tables[1])
print('rate ratio:', np.exp(fit.params['condition']))
A Poisson GLM with a log link reports its coefficient as a rate ratio once exponentiated — “treatment multiplies the expected count by this factor” — which is a more honest summary of count data than a linear-regression slope in raw count units, and it can never predict an impossible negative count.
Checking whether the distribution choice was right
Poisson assumes the variance equals the mean; real biological counts are frequently overdispersed (variance exceeds the mean), in which case a negative binomial family fits better and gives more honest (usually wider) uncertainty intervals. Comparing the residual deviance to its degrees of freedom, or directly comparing Poisson against negative binomial fits, is the standard check before trusting a count model’s reported uncertainty.
Takeaways
- Choose the distribution family from what the outcome physically is (counts, binary, positive-skewed continuous) — not by defaulting to Gaussian and checking residuals afterward.
- The link function keeps predictions on a valid scale: log link keeps counts and intensities positive, logit link keeps probabilities between 0 and 1.
- For count outcomes, check for overdispersion before trusting a plain Poisson model’s uncertainty — a negative binomial family is the usual fix when variance exceeds the mean.
- GLM coefficients are on the link scale, not the outcome scale — exponentiate a log-link coefficient to read it as a rate ratio, don’t interpret it directly as a raw count change.