
Adjusting for the confounders you can see
2026-10-26
explain omitted variable bias: why leaving a confounder out of a regression shifts a coefficient, and in which direction;
read a multiple OLS coefficient as “holding the other variables constant”, and show what that means with the Frisch–Waugh three-step;
estimate an adjusted effect in R, and say honestly what multiple OLS does not fix.
A gunman attacked a debate on free speech at Krudttønden, then the Great Synagogue in Krystalgade. Two people were killed.
Discuss: did the attacks make people in Denmark more hostile towards immigrants? How could you find out?
Why not for Denmark? Of everyone the ESS interviewed in the five weeks around the Bali attack, only 2 were living in Denmark; interviewing only got going at the very end of October, after the window had closed. Natural experiments need lucky timing.
Three lectures, three ways to estimate causal effects:

Beware: read from left to right in descending order of credibility. Today’s tool is the weakest of the three, and the one you will use most often.
Part 1 of 3
Legewie’s Bali design assumed interview date was as-if random. The balance table says: almost, but not quite.
The Bali bombing memorial, Kuta. Photo: Jorge Láscar, CC BY 2.0.
Last week we treated the day of a respondent’s ESS interview as as-if random: the people interviewed after the Bali attack should look like those interviewed before.
Legewie did not just assume it. The paper checks it, and honestly reports that they do not quite:
“The treatment group is on average slightly younger, and the proportion of people who are retired and who work from home is lower” Legewie (2013, p. 1211).

Discuss: why would people interviewed early in a survey period differ systematically from those interviewed late?
An ESS interviewer rings at two doors on the first day of fieldwork.
Reachability bias: the easy to reach (retired, at home, older) are interviewed early, before the attack. The hard to reach (working, out, younger) only after several visits, often after it.
So the interview day is not quite a coin flip: it depends on age.
Xenophobia tends to increase with age.
The younger a respondent, the more likely they were still to be interviewed after the attack.
\[\Rightarrow \text{Avg}_n[Y_{0i} \mid D_i = 0] \;\color{#901A1E}{>}\; \text{Avg}_n[Y_{0i} \mid D_i = 1]\]
In Portugal the imbalance runs the other way. Older respondents were more likely to land in the treatment group. Same logic, opposite sign.
Discuss: does this imbalance make our estimate of the Bali effect too large or too small?

The treatment group had a lower baseline xenophobia to begin with \(\rightarrow\) the raw comparison understates the attack’s effect.
Lecture 5 split group comparisons into two parts:
\[\underbrace{\text{Avg}_n[Y_{1i} \mid D_i = 1] - \text{Avg}_n[Y_{0i} \mid D_i = 0]}_{\text{what we compare}} = \text{ATT} \; + \; \underbrace{\text{Avg}_n[Y_{0i} \mid D_i = 1] - \text{Avg}_n[Y_{0i} \mid D_i = 0]}_{\text{Selection Bias } = \; 0 \text{ if exposure is as-if random (L6, L7)}}\]
\[\underbrace{\text{Avg}_n[Y_{1i} \mid D_i = 1] - \text{Avg}_n[Y_{0i} \mid D_i = 0]}_{\text{what we compare}} = \text{ATT} \; + \; \color{#901A1E}{\underbrace{\text{Avg}_n[Y_{0i} \mid D_i = 1] - \text{Avg}_n[Y_{0i} \mid D_i = 0]}_{< \; 0 \text{: the treated are younger}}}\]
Last week: we assumed the interview day was as-if random, and that both groups shared the same baseline \(Y_0\). We thought the selection-bias term was zero and the raw difference was the effect.
Beware: in Legewie’s data the term is not quite zero. The treated are younger, so their baseline \(Y_0\) is lower, and the raw comparison understates the effect.
Today: for a confounder we measured, we can put a number on this hidden term (Part 2) and remove it (Part 3).
Like engineers who crash a car with a dummy before they trust it with people, we simulate data where we set the true effect, then check how OLS works.


# Older people are interviewed EARLIER, so they less often
# end up in the post-attack (treatment) group.
toydat <- toydat %>%
mutate(
int_day = (-0.1 * age) + rnorm(n = 500, mean = 0, sd = 1),
bali = case_when(
int_day < mean(int_day) ~ 0, # before the attack
int_day >= mean(int_day) ~ 1) # after the attack
)
The older someone is, the less likely they land in the post-attack group.

Two overlapping clouds. The red ones sit a little further left: they are younger.
# Bivariate: the treatment only
ols_bi <- lm_robust(xeno ~ bali, data = toydat)
# Multiple: add the confounder
ols_mult <- lm_robust(xeno ~ bali + age, data = toydat)
modelsummary(
list("Bivariate" = ols_bi, "Multiple" = ols_mult),
coef_rename = c("bali" = "Exposed to Bali", "age" = "Age"),
stars = TRUE, gof_map = c("nobs", "r.squared"),
output = "kableExtra"
)| Bivariate | Multiple | |
|---|---|---|
| (Intercept) | 12.504*** | −0.499 |
| (0.113) | (0.428) | |
| Exposed to Bali | 0.174 | 1.135*** |
| (0.154) | (0.096) | |
| Age | 0.313*** | |
| (0.010) | ||
| Num.Obs. | 500 | 500 |
| R2 | 0.003 | 0.649 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |
The bivariate model is badly wrong. It misses the truth by a wide margin. Adding one variable, age, recovers ≈ 1.
Part 2 of 3
The gap between those two numbers has a formula.
The exposed and the unexposed differ in two ways:
Discuss: each year of age adds ≈ 0.313 xenophobia. How much xenophobia does that age gap alone account for?
Take 1 and 2 together: 1.135 + (-0.961) = 0.174, exactly the bivariate estimate.
| Age | |
|---|---|
| Exposed to Bali | −3.070*** |
| (0.395) | |
| Num.Obs. | 500 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |
The age gap alone: \(-3.07 \times 0.313 = -0.961\) xenophobia.




Zoomed in on the middle of the toy data.

\[\underbrace{\tilde\beta_{D \rightarrow Y}}_{\text{what the biased model gives}} = \underbrace{\beta_{D \rightarrow Y}}_{\text{the truth}} + \underbrace{(\beta_{C \rightarrow Y} \times \beta_{D \rightarrow C})}_{\textbf{omitted variable bias}}\]
Put our simulated numbers in: the two paths, multiplied:
\[\underbrace{0.174}_{\text{bivariate}} \;=\; \underbrace{1.135}_{\text{multiple}} \;+\; (\underbrace{0.313}_{C \rightarrow Y} \times \underbrace{-3.07}_{D \rightarrow C})\]
It adds up, exactly. The bias is the confounder’s effect on the outcome, times the imbalance in the confounder between the groups. It is the size of Lecture 5’s hidden term, for one confounder.
The formula also tells you the sign:
| \(\beta_{C \rightarrow Y}\) | \(\beta_{D \rightarrow C}\) | Bias |
|---|---|---|
| \(+\) | \(+\) | too large |
| \(+\) | \(-\) | too small |
| \(-\) | \(+\) | too small |
| \(-\) | \(-\) | too large |
These terms name the same problem, but from different angles:
“Omitted variable bias” is the useful name today, because it points at the fix: the variable is omitted, so put it in.
Beware: that only works for variables you actually measured. There is no formula for the confounders you never recorded.
You estimate the adjusted Bali effect for Portugal, controlling for age, gender and employment status. And compare it to the unadjusted one.
Open exercise 1 in a new tab ↗
Part 3 of 3
“Holding age constant”: Frisch and Waugh (1933) showed it is an arithmetic procedure.

Ragnar Frisch (1895–1973), a Norwegian, coined the word econometrics, was the first editor of the journal Econometrica, where this theorem appeared in volume 1, and shared the very first Nobel prize in economics, 1969. His co-author Frederick Waugh was an agricultural economist in the US.
Photo: Borgens Atelier.

“comparing people with the same values on the other variables, …”. That is what holding constant, controlling for and adjusted for mean.
The multiple model gave 1.135 for Exposed to Bali. And 0.313 for Age: the slope of both lines.
Discuss: say in one sentence what each of the two coefficients means.
“Comparing people of the same age, the ones interviewed after the attack score 1.14 points higher on xenophobia.”
“Among people interviewed before/after the attack, each extra year of age goes with 0.31 points more.”*
Frisch and Waugh (1933) demonstrated that “controlling” for a confounder is mathematically equivalent to a three-step procedure.
Regress the outcome on the confounder, keep the residuals: the part of xenophobia that age does not explain.
(toydat <- toydat %>%
add_residuals(
model = lm(xeno ~ age, data = .),
var = "e_xeno" # A new column
))
# # A tibble: 500 × 5
# age int_day bali xeno e_xeno
# <dbl> <dbl> <dbl> <dbl> <dbl>
# 1 42.5 -3.90 1 15.2 1.97
# 2 45.3 -5.10 0 13.3 -0.764
# 3 42.7 -2.89 1 12.8 -0.532
# 4 37.1 -4.40 0 11.8 -0.00133
# # ℹ 496 more rowsEach red line is one person’s residual: how far they sit from what their age alone predicted.

The double circle: we control for age, which closes the backdoor path.

Regress the treatment on the confounder, keep the residuals: the part of Bali exposure that age does not explain.
(toydat <- toydat %>%
add_residuals(
model = lm(bali ~ age, data = .),
var = "e_bali" # Another new column
))
# # A tibble: 500 × 6
# age int_day bali xeno e_xeno e_bali
# <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
# 1 42.5 -3.90 1 15.2 1.97 0.582
# 2 45.3 -5.10 0 13.3 -0.764 -0.318
# 3 42.7 -2.89 1 12.8 -0.532 0.590
# 4 37.1 -4.40 0 11.8 -0.00133 -0.609
# # ℹ 496 more rowsDiscuss: what kind of person has a large positive e_bali?


Regress the two sets of residuals on each other. Its slope is identical to the multiple OLS coefficient.
ols_resid <- lm_robust(e_xeno ~ e_bali, data = toydat)
modelsummary(
list("Multiple OLS" = ols_mult, "Residualised" = ols_resid),
coef_rename = c("bali" = "Exposed to Bali", "age" = "Age",
"e_bali" = "Exposed to Bali (resid.)"),
coef_omit = "(Intercept)",
stars = TRUE, gof_map = c("nobs"),
output = "kableExtra"
)
| Multiple OLS | Residualised | |
|---|---|---|
| Exposed to Bali | 1.135*** | |
| (0.096) | ||
| Age | 0.313*** | |
| (0.010) | ||
| Exposed to Bali (resid.) | 1.135*** | |
| (0.096) | ||
| Num.Obs. | 500 | 500 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |
Same number, two routes. That is what “controlling for age” does: it removes age from both variables and fits a line to what is left.
Look at step 2 again: after removing age, who still has a lot of unexplained exposure? The untypical cases: the older people who were still interviewed after the attack, and the younger ones interviewed before it.

Multiple OLS quietly down-weights typical cases and up-weights untypical ones. The comparison is carried by people who broke the pattern.

Beware: if nobody breaks the pattern (if age predicted exposure perfectly) there would be no residual variation left, and nothing to estimate from.
Nothing new is being minimised. The best-fitting plane still minimises the sum of squared residuals:
\[\min \sum_{i=1}^{n} \left(y_i - (\alpha + \beta_1 x_{1i} + \dots + \beta_k x_{ki})\right)^2\]
\[y_{i} = \alpha + \beta_{1}x_{1} + \beta_{2}x_{2} + \ldots + \beta_{k}x_{k} + \epsilon_{i}\]
With a 0/1 treatment and one control, the “plane” is simply the two parallel lines from Seeing the gap.
Beware — two things that follow:
Multiple OLS assumes a linear relation for every continuous predictor, not just the one you care about.
Every coefficient is adjusted for all the others. There is no such thing as adding a control “just for one variable”.
It can close a backdoor path through a confounder you measured.
It cannot do anything about a confounder you never measured. And no test will tell you one is there.

The streetlight effect. A man searches for his lost keys under a street lamp. Did he lose them here? “No, but this is where the light is.” We control for what we happened to measure, not necessarily for what matters.

The double circle is a closed backdoor path: we controlled for it. The red dashed one stays wide open.
Discuss: if the confounders you can see turned out to be imbalanced, is it likely that the ones you cannot see are perfectly balanced?
The same wrangling as last week. Now keeping age as well, and restricting to Portugal.
Before the attack (N=187)
|
After the attack (N=104)
|
|||||
|---|---|---|---|---|---|---|
| Mean | Std. Dev. | Mean | Std. Dev. | Diff. in Means | Std. Error | |
| age | 45.8 | 19.4 | 48.5 | 17.6 | 2.6 | 3.0 |
| anti_immi | 3.6 | 0.9 | 4.0 | 0.8 | 0.3 | 0.1 |
Discuss: in Portugal, is the raw estimate too large or too small?
Careful: the direction is the opposite of the all-country pattern Legewie reports. Portugal is its own case.
ols_pt_bi <- lm_robust(anti_immi ~ treat,
weights = pspwght, data = ESS)
ols_pt_mult <- lm_robust(anti_immi ~ treat + age,
weights = pspwght, data = ESS)
modelsummary(
list("Bivariate" = ols_pt_bi, "Adjusted" = ols_pt_mult),
coef_rename = c(
"treatAfter the attack" = "After the attack",
"age" = "Age"),
stars = TRUE, gof_map = c("nobs", "r.squared"),
output = "kableExtra"
)\[\underbrace{0.330}_{\text{bivariate}} \;\approx\; \underbrace{0.313}_{\text{adjusted}} \;+\; (\underbrace{0.0064}_{C \rightarrow Y} \times \underbrace{2.62}_{D \rightarrow C})\]
| Bivariate | Adjusted | |
|---|---|---|
| (Intercept) | 3.621*** | 3.328*** |
| (0.082) | (0.175) | |
| After the attack | 0.330** | 0.313** |
| (0.116) | (0.118) | |
| Age | 0.006+ | |
| (0.003) | ||
| Num.Obs. | 291 | 291 |
| R2 | 0.032 | 0.050 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |
Adjusting for age moves the estimate by only -0.017 SD. Why so little? The bias is a product, and in Portugal age barely predicts xenophobia (0.0064 per year).

plotdata <- bind_rows( # Stack the two tidy tables on top of each other
tidy(ols_pt_bi), # Model 1: bivariate
tidy(ols_pt_mult), # Model 2: adjusted for age
.id = "model") %>% # ... and remember which row came from which model
filter(term != "(Intercept)") %>%
mutate(model = case_when(model == "1" ~ "Bivariate",
model == "2" ~ "Adjusted for age"),
term = case_when(term == "treatAfter the attack" ~ "After the attack",
term == "age" ~ "Age"))
ggplot(data = plotdata, aes(y = estimate, x = term, color = model)) +
geom_hline(yintercept = 0, color = "#901A1E", lty = "dashed") +
geom_pointrange(aes(ymin = conf.low, ymax = conf.high),
position = position_dodge(width = 0.5)) + # Side by side
coord_flip() +
labs(x = "", y = "Difference in xenophobia (SD)", color = "") +
theme_minimal(base_size = 15)
Source: Legewie (2013, p. 1210)
Legewie’s published models adjust for a whole battery of observed confounders: age, gender, education, employment, and more.
Alternative names for the same thing, all of which you will meet in papers: controlling for \(X\) · partialling out \(X\) · adjusted for \(X\) · conditional on \(X\).
You become the simulator: set the confounder’s strength and sign yourself, predict the bias from the formula, then check whether OLS agrees.
Open exercise 2 in a new tab ↗
A confounder you leave out of your model creates omitted variable bias: \(\tilde\beta = \beta + (\beta_{C \rightarrow Y} \times \beta_{D \rightarrow C})\). It is the same problem as selection bias, named after the missing variable.
Because the bias is a product of two effects, you can predict its sign before running anything. And it is zero if either effect is zero.
Holding \(C\) constant means comparing people with the same \(C\): the gap between two parallel lines. Frisch–Waugh shows how OLS does it: residualise \(Y\) on \(C\), residualise \(D\) on \(C\), and regress the leftovers.
Multiple OLS is still OLS: minimising squared residuals, with every coefficient adjusted for all the others.
It can repair imbalance in observed confounders. It can do nothing about unobserved ones, which is why a good design still beats a good regression.
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).modelsummary::datasummary_balance(~ group, data = ...): the balance table — your empirical read on \(\beta_{D \rightarrow C}\).modelsummary(list("Bivariate" = ..., "Adjusted" = ...)): put the unadjusted and adjusted models side by side.set.seed() + rnorm(): simulate data where you know the true effect.bind_rows(tidy(m1), tidy(m2), .id = "model") + position_dodge(): two models in one coefficient plot.Frisch, R. and F. V. Waugh (1933). “Partial Time Regressions as Compared with Individual Trends”. In: Econometrica, pp. 387-401.
Legewie, J. (2013). “Terrorist Events and Attitudes toward Immigrants: A Natural Experiment”. In: American Journal of Sociology, pp. 1199-1245.

Lecture 8 · Multiple OLS regression