
Confounders, mediators, and controls you can’t measure
2026-07-23
tell a confounder from a mediator on a causal diagram — and know that you control the first but usually not the second;
read a multiple regression coefficient as “holding the other variables constant” — and see how it does that (Frisch–Waugh);
use a smart summary control to stand in for confounders you cannot measure.
One part per goal. Two running questions: the gender pay gap in Denmark, and — back to Lecture 2 — does state ownership of the economy reduce poverty?
Part 1 of 2
Adding a variable to a regression can sharpen a comparison — or quietly ruin it. The difference is the arrow’s direction.

Confounder \(C\) (red) — sits on a backdoor path: an arrow runs into the treatment \(D\) and into the outcome \(Y\).
\(\rightarrow\) Control it. Multiple OLS blocks the backdoor and makes the comparison fairer.
Mediator \(M\) (blue) — sits on the causal path \(D \rightarrow M \rightarrow Y\): an arrow runs out of the treatment.
\(\rightarrow\) Usually leave it in. Controlling a mediator removes part of the very effect you want.
To get closer to the overall causal effect \(D \rightarrow Y\), control observed confounders — but never control a mediator: you would throw away the part of the effect that runs through it.
The exception is deliberate: sometimes we want the partial effect of \(D\) that does not run through \(M\) — then controlling \(M\) is exactly the point. Know which question you are asking.
Danish women earn less per month than Danish men. How much of that gap is because women more often work fewer contracted hours?
Work hours is a mediator: gender shapes hours, and hours shape pay.

pacman::p_load(tidyverse, haven, estimatr, modelsummary)
# ESS round 9, Denmark. Monthly gross wage, gender, contracted work hours.
ESS <- read_spss("../assets/ESS9e03_1.sav") %>%
filter(cntry == "DK") %>%
select(pspwght, gndr, wkhct, grspnum, infqbst) %>%
mutate(
across(c(pspwght, wkhct, grspnum), zap_labels),
gndr = as_factor(gndr),
grwage = case_when( # Put everyone on a monthly basis
infqbst == 1 ~ 4 * grspnum, # weekly -> monthly
infqbst == 3 ~ grspnum / 12, # yearly -> monthly
TRUE ~ grspnum # already monthly
)
) %>%
filter(wkhct > 0) %>%
drop_na()mod1 <- lm_robust(grwage ~ gndr, data = ESS, weights = pspwght)
mod2 <- lm_robust(grwage ~ gndr + wkhct, data = ESS, weights = pspwght)
modelsummary(
list("Gross wage" = mod1, "Gross wage" = mod2),
coef_rename = c("gndrFemale" = "Female", "wkhct" = "Weekly hours"),
stars = TRUE, gof_map = c("nobs", "r.squared"),
output = "kableExtra"
)| Gross wage | Gross wage | |
|---|---|---|
| (Intercept) | 41997.795*** | 6778.675 |
| (4727.672) | (5160.501) | |
| Female | −9492.532+ | −6096.550 |
| (5174.552) | (5106.762) | |
| Weekly hours | 977.304*** | |
| (99.479) | ||
| Num.Obs. | 778 | 778 |
| R2 | 0.003 | 0.011 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |
The raw gap of 9,493 kr./month shrinks to 6,097 kr. once we hold work hours constant: about 36% of the gender pay gap runs through fewer contracted hours. The rest does not — and controlling hours would hide it.
In Lecture 2 we asked: do state-owned (socialist) economies reduce poverty? Descriptively — no. But they also offer fewer civil liberties, and civil liberties themselves predict less poverty.
Discuss: are civil liberties a confounder or a mediator of the state ownership → poverty relationship? Should we control for them?

The double-headed dashed arrow is the honest part: with cross-country data we cannot be sure whether fewer civil liberties cause state ownership or the reverse.
Would state-owned economies reduce poverty more — if they did not also curb their citizens’ civil liberties?
That is the partial effect of state ownership that does not run through civil liberties. Multiple OLS can estimate it.
# V-Dem: civil liberties (equality before the law and individual liberty) and state ownership
Dat_vdem <- vdem %>%
as_tibble() %>%
select(country_text_id, year,
civ_liberties = v2xcl_rol,
state_own_raw = v2clstown) %>%
mutate(state_ownership = -state_own_raw) # Reverse: higher = MORE state ownership
# World Bank: extreme poverty (< $3.00 a day). Live, with cached fallback.
Dat_poverty <- tryCatch(
wb_data("SI.POV.DDAY", start_date = 1972, end_date = 2025),
error = function(e) readRDS("data/wb_poverty_raw.rds")
) %>%
rename(poverty = SI.POV.DDAY, year = date, country_text_id = iso3c) %>%
select(country_text_id, year, country, poverty) %>%
drop_na(poverty) %>%
group_by(country) %>% filter(year == max(year)) %>% ungroup()
(Dat <- inner_join(Dat_poverty, Dat_vdem,
by = c("country_text_id", "year")) %>%
drop_na(poverty, state_ownership, civ_liberties))
# # A tibble: 161 × 7
# country_text_id year country poverty civ_liberties state_own_raw state_ownership
# <chr> <dbl> <chr> <dbl> <dbl> <dbl> <dbl>
# 1 ALB 2020 Albania 0.3 0.914 1.57 -1.57
# 2 DZA 2011 Algeria 0 0.575 -1.91 1.91
# 3 AGO 2018 Angola 39.3 0.559 -0.746 0.746
# 4 ARG 2024 Argentina 1 0.844 1.39 -1.39
# 5 ARM 2024 Armenia 0.8 0.839 1.64 -1.64
# 6 AUS 2020 Australia 0.9 0.949 1.44 -1.44
# 7 AUT 2023 Austria 0.5 0.937 0.479 -0.479
# 8 AZE 2005 Azerbaijan 0 0.38 -0.617 0.617
# 9 BGD 2022 Bangladesh 5.9 0.338 0.371 -0.371
# 10 BRB 2016 Barbados 1.7 0.919 0.753 -0.753
# # ℹ 151 more rows# Bivariate: state ownership only
ols <- lm_robust(poverty ~ state_ownership, data = Dat)
# Multiple: add civil liberties
ols_mult <- lm_robust(
poverty ~ state_ownership + civ_liberties,
data = Dat
)
modelsummary(
list("Poverty" = ols, "Poverty" = ols_mult),
coef_rename = c("state_ownership" = "State ownership",
"civ_liberties" = "Civil liberties"),
stars = TRUE, gof_map = c("nobs", "r.squared"),
output = "kableExtra"
)| Poverty | Poverty | |
|---|---|---|
| (Intercept) | 15.914*** | 37.041*** |
| (2.166) | (6.020) | |
| State ownership | 2.742 | −3.781+ |
| (1.705) | (2.058) | |
| Civil liberties | −37.012*** | |
| (8.501) | ||
| Num.Obs. | 161 | 161 |
| R2 | 0.017 | 0.124 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |
On its own, state ownership looks harmless. Holding civil liberties constant, its sign flips — among countries with the same civil liberties, more state ownership goes with less poverty. Civil liberties were masking it.
Frisch and Waugh (1933) showed that “controlling for” a variable is exactly a three-step residualisation — regression by subtraction.
Regress the outcome on the control, keep the residuals — the part of poverty that civil liberties do not explain.

Regress the treatment on the control, keep the residuals — the part of state ownership that civil liberties do not explain.

Regress the two sets of residuals on each other. Its slope is identical to the multiple-regression coefficient.
ols_resid <- lm_robust(e_poverty ~ e_stateown, data = Dat)
modelsummary(
list("Multiple OLS" = ols_mult,
"Residualised" = ols_resid),
coef_rename = c("state_ownership" = "State ownership",
"civ_liberties" = "Civil liberties",
"e_stateown" = "State ownership (resid.)"),
coef_omit = "(Intercept)",
stars = TRUE, gof_map = c("nobs"),
output = "kableExtra"
)| Multiple OLS | Residualised | |
|---|---|---|
| State ownership | −3.781+ | |
| (2.058) | ||
| Civil liberties | −37.012*** | |
| (8.501) | ||
| State ownership (resid.) | −3.781+ | |
| (2.049) | ||
| Num.Obs. | 161 | 161 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |
Same number, two routes: the residualised slope on e_stateown equals the multiple model’s state_ownership coefficient. That is all controlling for civil liberties does.


To predict, set the control to an informative constant (here: average civil liberties), then vary the treatment.
You practice multiple OLS on the carbon divide from Lecture 4: do regions differ in emissions beyond their wealth?
Open exercise 1 in a new tab ↗
Part 2 of 2
“To control for a wide range of factors seems daunting: the possibilities are virtually infinite, and many characteristics are hard to quantify.”
— Angrist and Pischke (2014, p. 51)

Discuss: what confounds the effect of a team losing on domestic violence? (Team quality, the kind of fan, the kind of city …) How could a single variable stand in for all of them?

The point spread already prices in everything the market knows about both teams — one number that summarises a world of confounders (Card and Dahl, 2011).
By comparing games with the same pre-game point spread, Card and Dahl (2011) isolate the emotional shock of an upset loss — a loss when your team was predicted to win.
An upset loss raises at-home male-on-female violence by about 10% — and it barely moves as more controls are added (columns 1→5). The summary control did the work.

Source: Card and Dahl (2011)
Graduates of selective universities earn more. But the ambitious, well-connected, and able both choose selective universities and earn more anyway — a swarm of confounders (\(C\)).
Discuss: what single thing could summarise a student’s ambition and ability as the admissions system saw it?

Dale and Krueger (2002) compare students who applied to and were admitted to the same set of schools — then some went to the more selective one, some did not.
Sharing an application portfolio is a summary of ambition and ability as the system measured it: a stand-in for the confounders no survey captures.

Source: Dale and Krueger (2002)

Without selection controls, each +100 points of school-average SAT buys about +7.6% earnings.
Among matched applicants, the effect collapses to −0.016 ≈ 0. The “selectivity premium” was mostly who chose selective schools, not the schools themselves.
Source: Dale and Krueger (2002)
If a selective US university barely raises earnings once you compare like with like, a selective Danish one probably doesn’t either.
\(\rightarrow\) Your KU degree pays off because you are able and put in the work — the diploma mostly reflects that, it does not manufacture it.
You visualise the adjustment: a before/after coefficient plot and model predictions — again on the carbon divide.
Open exercise 2 in a new tab ↗
A confounder opens a backdoor path (arrow into the treatment) — control it. A mediator lies on the causal path (arrow out of the treatment) — usually don’t, or you delete part of the effect.
Adding a control can move a coefficient a lot — even flip its sign (state ownership & poverty). Always ask why a variable belongs in the model.
Frisch–Waugh: “controlling for \(C\)” = residualise \(Y\) on \(C\), residualise \(D\) on \(C\), regress the residuals. Same number, clearer intuition.
When confounders are unmeasurable, a smart summary control — a point spread, a shared application portfolio — can stand in for many of them at once.
Multiple OLS improves comparisons; only a real experiment guarantees them.
Shaky on any of these? That is what this week’s Absalon quiz and the Friday exercise class are for.
lm_robust(y ~ d + c, ...): multiple OLS — the coefficient on d holds c constant.modelr::add_residuals(model = ..., var = ...): keep a model’s residuals (the Frisch–Waugh workflow).tidy() + geom_pointrange() + coord_flip(): the coefficient plot.predict(model, newdata = ..., interval = "confidence"): predictions — set controls to an informative constant.modelsummary(list(...)): put bivariate and multiple models side by side.Angrist, J. D. and J. Pischke (2014). Mastering ’Metrics: The Path from Cause to Effect. Princeton University Press.
Card, D. and G. B. Dahl (2011). “Family Violence and Football: The Effect of Unexpected Emotional Cues on Violent Behavior*“. In: The Quarterly Journal of Economics, pp. 103-143.
Dale, S. B. and A. B. Krueger (2002). “Estimating the Payoff to Attending a More Selective College: An Application of Selection on Observables and Unobservables*“. In: The Quarterly Journal of Economics, pp. 1491-1527.
Frisch, R. and F. V. Waugh (1933). “Partial Time Regressions as Compared with Individual Trends”. In: Econometrica, pp. 387-401.

Lecture 9 · Multiple OLS in practice