Skip to content

We've built 200+ tools — open your toolbox, there's something in here you probably need.

Module 11 of 1355 min readBeginner

Beyond OLS: GLMs, logistic regression, and the modelling ecosystem

glm() and logistic regression, log-odds and odds ratios, and where to reach for IV (ivreg), panel (fixest/plm) and time series (fable).

Module 11 of 13

Listen along

Read “Beyond OLS: GLMs, logistic regression, and the modelling ecosystem” aloud

Plays in your browser using on-device text-to-speech — nothing leaves the page.

Learning objectives

By the end of this module, you should be able to:

  • 01Fit logistic regression with glm(y ~ x, family = binomial("logit")) and read coefficients as log-odds
  • 02Convert log-odds to odds ratios with exp() and produce fitted probabilities with predict(type = "response")
  • 03Recognise the wider modelling ecosystem — GLMs for counts, IV, fixed effects, and time series — and where to reach for each
  • 04Run instrumental-variables and fixed-effects regressions with ivreg() and fixest::feols()

So far every regression in this course has ended at lm() — ordinary least squares on a continuous outcome. Real applied work reaches further: outcomes that are 0/1 (did the household adopt M-PESA?), counts (how many loan defaults this month?), endogenous regressors that need instruments, panels that need fixed effects, and series that need forecasting. This module is the map of that wider territory, starting with the single most important step past OLS: the generalised linear model.

Why OLS is not enough for a 0/1 outcome

Suppose the outcome is binary: adopted <- 1 if a household uses M-PESA, 0 otherwise. Fit lm(adopted ~ income) and you get a linear probability model. It runs, and the coefficients are sometimes a fine first look — but the fitted line can predict probabilities below 0 or above 1, and the errors are heteroskedastic by construction. For a proper probability model we need to bend the linear predictor onto the 0-1 interval. That is exactly what a generalised linear model does.

The generalised linear model

A GLM keeps the familiar linear predictor beta0 + beta1*x but passes it through a link function that maps it to the natural scale of the outcome. lm() is the special case with a Gaussian family and an identity link. Change the family and you change the model: binomial for 0/1 data, poisson for counts, Gamma for positive skewed data. The interface is one function — glm() — with the same formula syntax you already know.

Logistic regression — the logit link

For a binary outcome the logistic model writes the probability that y = 1 as a logistic (S-shaped) function of the linear predictor. This guarantees a value strictly between 0 and 1:

Pr(y=1x)=11+e(β0+β1x)\Pr(y = 1 \mid x) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 x)}}

Rearranged, the model is linear not in the probability but in the log-odds — the logit. The link function is the log of the odds, and that is the scale the coefficients live on:

logit(Pr(y=1x))=log ⁣Pr(y=1x)1Pr(y=1x)=β0+β1x\operatorname{logit}\bigl(\Pr(y=1\mid x)\bigr) = \log\!\frac{\Pr(y=1\mid x)}{1 - \Pr(y=1\mid x)} = \beta_0 + \beta_1 x

So beta1 is the change in log-odds for a one-unit change in x. Log-odds are hard to feel intuitively, which is why we almost always exponentiate them into odds ratios — see below.

Fitting a logistic model with glm()

r
# Toy example: does M-PESA adoption rise with income?
set.seed(1)
hh <- data.frame(
income_k = round(runif(400, 5, 120)), # monthly income, KES thousands
urban = rbinom(400, 1, 0.5)
)
# true model: higher income and urban households more likely to adopt
p <- plogis(-2 + 0.03 * hh$income_k + 0.8 * hh$urban)
hh$adopted <- rbinom(400, 1, p)
model <- glm(adopted ~ income_k + urban,
data = hh,
family = binomial("logit"))
summary(model)
glm() mirrors lm(): same formula, plus a family argument that names the distribution and link.

The coefficients in summary(model) are on the log-odds scale. A positive coefficient means higher x raises the probability that y = 1; the size, though, is not a probability and not directly comparable to an lm() slope.

Coefficients as log-odds; exp() to odds ratios

Because logit(p) = beta0 + beta1*x is additive in log-odds, exponentiating turns a coefficient into a multiplicative effect on the odds — an odds ratio. If beta1 = 0.03, then exp(0.03) is about 1.03: each extra KES 1,000 of monthly income multiplies the odds of adoption by about 1.03, a 3% rise in the odds. An odds ratio of 1 means no effect; above 1 raises the odds, below 1 lowers them.

r
# Odds ratios by hand
exp(coef(model))
# Tidy odds-ratio table with confidence intervals
library(broom)
tidy(model, exponentiate = TRUE, conf.int = TRUE)
# term estimate std.error statistic p.value conf.low conf.high
# (Intercept) ...
# income_k ~1.03 ... ... ... ... ...
# urban ~2.2 ... ... ... ... ...
exponentiate = TRUE tells broom to report exp(estimate) and exp() of the interval endpoints — an odds-ratio table ready to pipe into more dplyr.

Odds ratios are multiplicative

An lm() coefficient adds to the outcome; a logistic coefficient, once exponentiated, multiplies the odds. OR = 2 means the odds double per unit of x; OR = 0.5 means they halve. Never read exp(coef) as a change in probability — the effect on probability depends on where you are on the S-curve.

Fitted probabilities with predict(type = "response")

predict() on a glm defaults to type = "link", returning log-odds. To get probabilities on the 0-1 scale — the thing you usually want — pass type = "response", which applies the inverse link for you.

r
# Probability of adoption for a new household
newhh <- data.frame(income_k = c(20, 60, 100), urban = c(0, 1, 1))
predict(model, newdata = newhh, type = "link") # log-odds
predict(model, newdata = newhh, type = "response") # probabilities, 0-1
# Same by hand: the inverse logit is plogis()
plogis(predict(model, newdata = newhh))
# Fitted probabilities and residuals as a tidy data frame
library(broom)
augment(model, type.predict = "response")
type = "response" runs the linear predictor back through the logistic function; plogis() is that same inverse-logit if you want it explicitly.

Poisson regression for counts (briefly)

When the outcome is a non-negative count — number of defaults, number of mobile-money transactions in a day — the Poisson family with its log link is the natural GLM. The same glm() call, a different family. Coefficients are on the log scale, so exp() turns them into multiplicative rate ratios, exactly as odds ratios work for logistic.

r
counts <- data.frame(
defaults = rpois(200, 3),
branch_age = runif(200, 1, 30)
)
pois_model <- glm(defaults ~ branch_age,
data = counts,
family = poisson("log"))
exp(coef(pois_model)) # rate ratios: multiplicative effect on the expected count
If the counts are over-dispersed (variance far above the mean) reach for MASS::glm.nb() — a negative-binomial GLM.

The wider ecosystem — where to go next

GLMs cover a lot, but three whole families of applied methods live in dedicated packages. You do not need to master them today; you need to know they exist and what to type when the question arrives.

  • Instrumental variables (endogenous regressors): AER::ivreg() or the standalone ivreg::ivreg()
  • Panel / fixed effects: fixest::feols() (fast, high-dimensional FE, cluster-robust SEs) or the classic plm::plm()
  • Time series and forecasting: the fable package (tidyverse-native) and the older forecast package, with Arima() / ARIMA() for ARIMA models

Instrumental variables — ivreg()

When a regressor is correlated with the error term (simultaneity, omitted variables, measurement error), OLS is biased and you instrument. ivreg uses a formula with a vertical bar separating the structural equation from the instruments: y ~ x | z, read as 'regress y on x, using z as the instrument for the endogenous part'.

r
library(ivreg) # or: library(AER); ivreg is exported there too
# Structural eq: consumption ~ mpesa_use (endogenous)
# Instrument: distance to nearest agent (affects use, not consumption directly)
iv <- ivreg(consumption ~ mpesa_use + income | # structural equation
agent_distance + income, # instruments (income is exogenous, in both)
data = survey)
summary(iv, diagnostics = TRUE) # includes weak-instrument and Wu-Hausman tests
Everything left of | is the model; everything right is the instrument set. Exogenous controls (income) appear on both sides.

Fixed effects — fixest::feols()

For panel data — the same units observed over time — fixed effects absorb every time-invariant difference between units. fixest::feols() puts the fixed-effect factors after a vertical bar and is fast enough for millions of rows and multiple high-dimensional FE dimensions, with cluster-robust standard errors built in.

r
library(fixest)
# Two-way fixed effects: household and year absorbed
fe <- feols(consumption ~ mpesa_use + income | hh_id + year,
data = panel,
cluster = ~hh_id) # cluster-robust SEs at the household level
summary(fe)
# broom works on feols too
library(broom)
tidy(fe, conf.int = TRUE)
The | hh_id + year syntax absorbs household and year fixed effects; cluster = ~hh_id clusters the standard errors. plm::plm(..., model = "within") is the classic equivalent.

Time series — fable and Arima()

For forecasting, the modern tidyverse-native tool is fable, built on tsibble (a time-aware tibble); the older, widely cited toolkit is Hyndman's forecast package with Arima() and auto.arima(). Both fit ARIMA, ETS, and related models and produce forecasts with prediction intervals.

r
library(forecast)
# mpesa monthly volume as a time series, 12 periods per year
vol <- ts(mpesa$volume_bn, frequency = 12, start = c(2018, 1))
fit <- auto.arima(vol) # picks the ARIMA(p,d,q) orders for you
forecast(fit, h = 12) # 12-month-ahead forecast with intervals
# Or fit a specific ARIMA(1,1,1)
Arima(vol, order = c(1, 1, 1))
auto.arima() searches ARIMA orders; the fable equivalent is data |> model(ARIMA(volume_bn)) |> forecast(h = 12). See fpp3 for the full treatment.

One interface, many models

The reason R rewards learning lm() well is that glm(), ivreg(), feols() and Arima() all reuse the same formula syntax, the same summary() habit, and — for most — the same broom::tidy() tidying. Master the OLS workflow and the rest of the ecosystem is a change of function name, not a change of language.

Check your understanding

You have a 0/1 outcome (adopted vs not). Which call fits a logistic regression?

Check your understanding

A logistic regression returns a coefficient of 0.6931 on urban. What is the odds ratio for urban (exp of the coefficient)?

Check your understanding

You call predict(model, newdata = newhh) on a logistic glm without a type argument. What scale is the result on?

Check your understanding

In ivreg(consumption ~ mpesa_use + income | agent_distance + income, data = survey), what is the role of agent_distance?

Exercise · try it first

Fit a logistic regression of adopted on income_k and urban in the hh data frame, then produce a tidy table of odds ratios with 95% confidence intervals.

Stuck? Ask Mwalimu (bottom-right) to check your reasoning.

Exercise · try it first

Using a fitted logistic model, compute the predicted probability of adoption for a household with income_k = 80 and urban = 1. Show two equivalent ways.

Stuck? Ask Mwalimu (bottom-right) to check your reasoning.

Key takeaways

  • glm() generalises lm(): the family argument picks the distribution and link — binomial("logit") for 0/1 outcomes, poisson for counts
  • A logistic coefficient is a change in log-odds; exp(coef) is an odds ratio — multiplicative, not additive
  • predict(model, type = "response") returns probabilities on the 0-1 scale; the default type = "link" returns log-odds
  • broom::tidy(model, exponentiate = TRUE, conf.int = TRUE) gives a tidy odds-ratio table; the ecosystem covers IV (ivreg), panel/FE (fixest, plm) and time series (fable, forecast)

Further reading

  1. 01

    An R Companion to Applied Regression (3rd Edition)

    John Fox & Sanford Weisberg · Sage · 2019The canonical bridge from lm() to glm() and beyond, with the car and effects packages.

  2. 02

    R for Data Science (2nd Edition)

    Hadley Wickham, Mine Çetinkaya-Rundel & Garrett Grolemund · O'Reilly · 2023

  3. 03

    Forecasting: Principles and Practice (3rd Edition)

    Rob J. Hyndman & George Athanasopoulos · OTexts · 2021Free online; the reference for the fable / forecast time-series ecosystem.

Loading progress…
LeadAfrikPublic Economics Hub