Generalized Linear Models (glm)
Reviewed & published by Brayan K
A generalized linear model extends ordinary linear regression to outcomes that aren't continuous — like yes/no results or counts — by adding a "family" and a link function, with logistic regression (family = binomial) being the most common example.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll see why lm() fails for 0/1 outcomes, fit a logistic model with glm(..., family = binomial), read the summary() coefficients, convert them to odds ratios, and get probabilities with predict(type = "response").
What You'll Learn in This Lesson
- Why lm() is wrong for binary outcomes
- How glm() families generalize linear regression
- How to fit logistic regression with family = binomial
- How to read the summary() coefficients table
- How to convert log-odds to interpretable odds ratios
- How predict(type = "response") returns probabilities
1️⃣ Why glm()? From lm to Families
Ordinary lm() predicts any real number, so for a 0/1 outcome it can return nonsense like a probability of 1.4. glm() fixes this with a family that matches your outcome type and a link that keeps predictions valid.
# lm() fits a straight-line model for a CONTINUOUS outcome — it can
# predict any number, including impossible ones like a probability of
# 1.3 or -0.2. When the outcome is YES/NO (0/1), that's wrong.
#
# glm() GENERALIZES lm(): you add a 'family' that says what kind of
# outcome you have and how to link it to the predictors.
#
# family = gaussian -> same as lm() (continuous)
# family = binomial -> logistic regression (0/1 outcome)
# family = poisson -> counts (0, 1, 2, ...)
# Logistic regression keeps predictions in [0, 1] by modelling the
# LOG-ODDS of the outcome as a linear function of the predictors.
# That's the model we'll build next.Now fit a real logistic model. The formula syntax is identical to lm() — outcome ~ predictor — you just add family = binomial.
# Will a student pass an exam given hours studied? Outcome is 0/1.
study <- data.frame(
hours = c(1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9),
pass = c(0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1)
)
# Fit logistic regression: pass ~ hours, binomial family.
model <- glm(pass ~ hours, data = study, family = binomial)
summary(model)
# The summary's Coefficients table is the heart of the output:
# (Intercept) - log-odds of passing when hours = 0
# hours - change in LOG-ODDS per extra hour
# Std. Error - uncertainty of each estimate
# z value, Pr(>|z|) - is the coefficient significantly non-zero?
# Below the table: deviance and AIC measure overall fit (lower AIC
# is better when comparing models).2️⃣ Interpreting Coefficients: Odds Ratios
Logistic coefficients live on the log-odds scale, which is awkward to talk about. Exponentiating with exp() turns each into an odds ratio — a multiplier on the odds per unit change in the predictor.
# Coefficients are in LOG-ODDS, which are hard to read. Exponentiating
# turns them into ODDS RATIOS, which are interpretable.
model <- glm(pass ~ hours, data = study, family = binomial)
coef(model) # the raw log-odds coefficients
exp(coef(model)) # odds ratios
# Interpreting the 'hours' odds ratio: if exp(coef) for hours is, say,
# about 3, then each ADDITIONAL hour of study multiplies the ODDS of
# passing by ~3. An odds ratio > 1 increases the odds; < 1 decreases
# them; exactly 1 means no effect.
#
# Confidence intervals on the odds-ratio scale:
exp(confint(model)) # CI for each odds ratioThe rule of thumb: an odds ratio above 1 means the predictor raises the odds of the outcome, below 1 lowers them, and exactly 1 means no effect. This single sentence unlocks most of logistic-regression interpretation.
3️⃣ Predicting Probabilities
To predict for new data, use predict() — but remember it defaults to the log-odds scale. Add type = "response" to get back genuine probabilities you can act on.
# predict() has two scales. The DEFAULT gives log-odds; you almost
# always want type = "response" to get PROBABILITIES in [0, 1].
model <- glm(pass ~ hours, data = study, family = binomial)
new <- data.frame(hours = c(2, 5, 8))
predict(model, newdata = new) # log-odds (link scale)
predict(model, newdata = new, type = "response") # probabilities!
# Turn probabilities into a yes/no prediction with a 0.5 cutoff:
probs <- predict(model, newdata = new, type = "response")
ifelse(probs > 0.5, "pass", "fail")
# As hours rise, the predicted probability climbs toward 1 — the
# characteristic S-shaped (sigmoid) logistic curve.Plotting those probabilities against the predictor reveals the signature S-shaped (sigmoid) logistic curve: predictions hug 0 at low values, rise steeply through the middle, and flatten toward 1 — never escaping the valid [0, 1] range.
Your turn. Fill in the ___ blank and run it.
# 🎯 YOUR TURN — replace each ___ then run it.
# Does a customer buy (1) or not (0) based on minutes on the site?
visits <- data.frame(
minutes = c(1, 3, 5, 2, 8, 10, 6, 12, 4, 9),
bought = c(0, 0, 1, 0, 1, 1, 0, 1, 0, 1)
)
# 1) Fit a logistic regression of bought on minutes.
model <- glm(bought ~ minutes, data = visits, family = ___) # 👉 replace ___ with binomial
# 2) Print the summary and find the 'minutes' coefficient.
summary(model)
# 3) Convert coefficients to odds ratios.
print(exp(coef(model)))
# 4) Predict the PROBABILITY of buying for 7 minutes on site.
predict(model, newdata = data.frame(minutes = 7), type = "response")Mini-Challenge: Two-Predictor Model
Real models use several predictors, including categorical ones. Fit a default-risk model on income plus a credit rating, interpret both odds ratios, predict for a new applicant, and compare AIC.
# 🎯 MINI-CHALLENGE: a two-predictor logistic model.
#
# Predict loan default (default = 1/0) from income (thousands) and a
# categorical credit rating ("good"/"poor").
#
# loans <- data.frame(
# income = c(20, 35, 50, 25, 60, 40, 30, 55),
# rating = c("poor","good","good","poor","good","poor","poor","good"),
# default = c(1, 0, 0, 1, 0, 1, 1, 0)
# )
#
# 1. Fit: glm(default ~ income + rating, data = loans, family = binomial)
# (R auto-creates a dummy variable for 'rating'.)
# 2. summary() the model; note BOTH coefficients and their signs.
# 3. exp(coef(model)) for odds ratios; interpret each in a comment.
# 4. predict(..., type = "response") for a NEW applicant, e.g.
# income = 45, rating = "good".
# 5. Bonus: compare AIC with a model that drops 'rating' — does adding
# rating improve the fit?
#
# Write it yourself, then run in RStudio or rdrr.io/snippets.
# your code hereCommon Errors (and the fix)
- ❌ Forgetting family = binomial — ✅ without it glm() defaults to gaussian (i.e. lm()), giving wrong results for 0/1 data.
- ❌ Reading log-odds as probabilities — ✅ exponentiate for odds ratios, or use predict(type = "response") for probabilities.
- ❌ predict() returns numbers outside [0, 1] — ✅ those are log-odds; you forgot type = "response".
- ❌ Outcome stored as a character — ✅ encode it as 0/1 or a factor; binomial needs a numeric/factor response.
Pro Tips
- 💡 Count outcomes: swap to family = poisson for modelling counts like visits or defects.
- 💡 Tidy results: broom::tidy(model, exponentiate = TRUE, conf.int = TRUE) gives odds ratios with CIs in a neat tibble.
- 💡 Compare models: anova(m1, m2, test = "Chisq") tests whether extra predictors significantly improve fit.
📋 Quick Reference — glm
| Task | R Syntax |
|---|---|
| Logistic regression | glm(y ~ x, data = d, family = binomial) |
| View coefficients | summary(model) |
| Odds ratios | exp(coef(model)) |
| Predict probability | predict(model, new, type = "response") |
| Poisson counts | glm(y ~ x, family = poisson) |
| Compare fit | AIC(m1, m2) |
🎉 Lesson Complete!
- ✅ glm() generalizes lm() via a family and a link
- ✅ family = binomial gives logistic regression for 0/1 outcomes
- ✅ summary() shows coefficients, p-values, and AIC
- ✅ exp(coef()) converts log-odds to readable odds ratios
- ✅ predict(type = "response") returns probabilities in [0, 1]
Practice quiz
Which family argument fits a logistic regression for a 0/1 outcome?
- family = binomial
- family = gaussian
- family = poisson
- family = ordinal
Answer: family = binomial. family = binomial gives logistic regression for binary outcomes.
Which family makes glm() behave like ordinary lm() for a continuous outcome?
- family = poisson
- family = gaussian
- family = binomial
- family = gamma
Answer: family = gaussian. family = gaussian is the default and matches lm().
Which family is appropriate for COUNT data (0, 1, 2, ...)?
- family = binomial
- family = gaussian
- family = poisson
- family = uniform
Answer: family = poisson. family = poisson models count outcomes.
On what scale are raw logistic-regression coefficients reported?
- Probabilities
- Counts
- Percentages
- Log-odds (logit)
Answer: Log-odds (logit). Logistic coefficients are on the log-odds scale, which is why they are hard to read directly.
What does exp(coef(model)) give you?
- Odds ratios
- Probabilities
- p-values
- Standard errors
Answer: Odds ratios. Exponentiating the log-odds coefficients yields interpretable odds ratios.
An odds ratio of exactly 1 means:
- The predictor doubles the odds
- The predictor halves the odds
- No effect on the odds
- The model failed to fit
Answer: No effect on the odds. OR > 1 raises the odds, < 1 lowers them, and exactly 1 means no effect.
What does predict() on a glm return by DEFAULT?
- Probabilities
- Predictions on the link (log-odds) scale
- Class labels
- The raw data
Answer: Predictions on the link (log-odds) scale. predict() defaults to the link scale; for logistic that is log-odds.
Which argument makes predict() return PROBABILITIES in [0, 1]?
- type = "prob"
- scale = "response"
- type = "odds"
- type = "response"
Answer: type = "response". type = "response" applies the inverse link to give probabilities.
What happens if you forget family = binomial on 0/1 data?
- glm() defaults to gaussian (like lm), giving wrong results
- glm() errors out
- It silently uses poisson
- It returns exact probabilities anyway
Answer: glm() defaults to gaussian (like lm), giving wrong results. Without a family, glm() uses gaussian, which is inappropriate for binary outcomes.
In the summary(), what does the Pr(>|z|) column report?
- The odds ratio
- The p-value testing whether the coefficient differs from zero
- The AIC
- The confidence interval width
Answer: The p-value testing whether the coefficient differs from zero. Pr(>|z|) is the p-value for each coefficient; smaller values get significance stars.
Continue this course
- Previous: Hypothesis Testing (t.test, chisq.test)
- Next: Checkpoint: Data Analysis — Combine tibbles, pivots and a tidyverse pipeline in a build — then a quiz
- Quick reference: R cheat sheet
Frequently asked questions
When should I use glm() instead of lm()?
Use lm() when the outcome is a continuous number whose errors are roughly normal and a straight-line prediction makes sense. Use glm() whenever that breaks down: a binary 0/1 outcome (family = binomial, logistic regression), count data (family = poisson), or other non-normal responses. glm() generalises lm() by adding a 'family' (the distribution of the outcome) and a 'link function' that keeps predictions sensible — for binomial, the logit link guarantees predicted probabilities stay between 0 and 1, which lm() cannot promise.
Why are logistic regression coefficients so hard to read?
Because they're on the log-odds (logit) scale, not the probability scale. A raw coefficient tells you how the LOG-ODDS of the outcome change per unit of the predictor, which isn't intuitive. Exponentiating with exp(coef(model)) converts each to an ODDS RATIO: a value of 2 means the odds double per unit increase, 0.5 means they halve, and 1 means no effect. To get an actual probability for a specific case, skip the manual maths and use predict(model, newdata, type = "response").
What does type = "response" do in predict()?
predict() on a glm defaults to the LINK scale — for logistic regression that's log-odds, which range from minus infinity to plus infinity and aren't directly meaningful. Passing type = "response" applies the inverse link so you get predictions on the natural scale of the outcome: probabilities between 0 and 1 for binomial models, expected counts for poisson models. It's almost always what you want when communicating results. To then label cases, threshold the probabilities (commonly at 0.5).
How do I read the significance stars in the summary?
Each coefficient row has a Pr(>|z|) column — the p-value testing whether that coefficient is different from zero (no effect). Small p-values get stars: *** below 0.001, ** below 0.01, * below 0.05. A starred predictor is statistically associated with the outcome after accounting for the others. But significance isn't everything: also look at the size and sign of the (exponentiated) coefficient for practical importance, and at the AIC to compare whole models. With many predictors, beware reading too much into borderline stars.