
Adjusting for the confounders you can see
2026-07-28
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.
One part per goal. Our running question: back to Legewie’s Bali attack — but this time the natural experiment is not quite as-if random.
Three lectures, three ways to earn a causal claim:
Beware: these are 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.
Last week we treated the day of a respondent’s ESS interview as as-if random — so the people interviewed after the Bali attack should look like those interviewed before.
Legewie checked. 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?
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}[Y_{0i} \mid D = 0] \;\color{#901A1E}{>}\; \text{Avg}[Y_{0i} \mid D = 1]\]
The treatment group had a lower baseline to begin with — so the raw comparison understates the attack’s effect.
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?
We cannot re-run the survey. The confounder is already baked into the data.
Discuss: age is measured — it is sitting right there in the data set. Can we use that somehow to repair the comparison after the fact?
Yes — that is exactly what multiple OLS does. It compares people of the same age who differ only in whether they were interviewed after the attack. The rest of today is how, and how much you should trust it.
Real data never tells you the right answer. So let’s simulate data where we set the true effect — then check whether OLS finds it.
The true causal effect of Bali is 1. Remember that number — it is the only time in this course you will know it.
# 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"
)Discuss: we know the true effect is 1. Which of these two models found it?
| 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. Same data, same outcome, one extra column.
Part 2 of 3
The gap between those two numbers is not random noise. It has a formula.
When we compare xenophobia between the exposed and the unexposed, we are comparing people who differ in two ways:
They differ in exposure — that is the causal effect we want, 1.14 in our simulation.
They also differ in age. How much? Regress the confounder on the treatment:
Discuss: the exposed are on average 3.1 years younger. Given that each year of age adds ≈ 0.3 xenophobia, how much xenophobia does that age gap alone account for?
| Age | |
|---|---|
| (Intercept) | 41.546*** |
| (0.289) | |
| Exposed to Bali | −3.070*** |
| (0.395) | |
| Num.Obs. | 500 |
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 |

\[\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.17}_{\text{bivariate}} \;\approx\; \underbrace{1.14}_{\text{multiple}} \;+\; (\underbrace{0.31}_{C \rightarrow Y} \times \underbrace{-3.07}_{D \rightarrow C})\]
It adds up. The bias is not a mystery: it is the confounder’s effect on the outcome, times the imbalance in the confounder between the groups.
The formula also tells you the sign — before you run anything:
| \(\beta_{C \rightarrow Y}\) | \(\beta_{D \rightarrow C}\) | Bias |
|---|---|---|
| \(+\) | \(+\) | too large |
| \(+\) | \(-\) | too small |
| \(-\) | \(+\) | too small |
| \(-\) | \(-\) | too large |
Same rule as multiplying two signed numbers — because that is exactly what it is.
Discuss: in Legewie’s data, age raises xenophobia (\(+\)) and the exposed group is younger (\(-\)). Was his raw estimate too large or too small?
Too small. The bias is negative, so the unadjusted estimate understates the effect of the attack. Adjusting for age should push it up.
You will meet the same idea under several names. They are the same problem seen 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” sounds like a metaphor. Frisch and Waugh (1933) showed it is an arithmetic procedure.
Regress the outcome on the confounder, keep the residuals — the part of xenophobia that age does not explain.
Each red line is one person’s residual: how far they sit from what their age alone predicted.

Regress the treatment on the confounder, keep the residuals — the part of Bali exposure that age does not explain.
Discuss: 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-regression 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 all “controlling for age” does — it removes age from both variables and fits a line to what is left.
We meet this again in Lecture 9, on real cross-country data.
Look at step 2 again: after removing age, the people with the most residual variation in treatment are the untypical ones — the older person who was still interviewed late, the younger one interviewed early.
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}\]
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 — even long after the data were collected.
It cannot do anything about a confounder you never measured. And no test will tell you one is there.
If the confounders you can see turned out to be imbalanced — is it likely that the ones you cannot see are perfectly balanced?

The double circle is a closed backdoor path — we controlled for it. The red dashed one stays wide open.
The same wrangling as last week — now keeping age as well, and restricting to Portugal.
Before Bali (N=187)
|
After Bali (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, which group is older — and so, by the sign rule, is the raw estimate too large or too small here?
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 Bali" = "After Bali", "age" = "Age"),
stars = TRUE, gof_map = c("nobs", "r.squared"),
output = "kableExtra"
)| Bivariate | Adjusted | |
|---|---|---|
| (Intercept) | 3.621*** | 3.328*** |
| (0.082) | (0.175) | |
| After Bali | 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 |
The same decomposition, now on real data:
\[\underbrace{0.33}_{\text{bivariate}} \;\approx\; \underbrace{0.313}_{\text{adjusted}} \;+\; (\underbrace{0.006}_{C \rightarrow Y} \times \underbrace{2.62}_{D \rightarrow C})\]
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.006 per year). A big imbalance times a tiny effect is still a tiny bias.

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.
Frisch–Waugh: “controlling for \(C\)” means residualise \(Y\) on \(C\), residualise \(D\) on \(C\), and regress the leftovers. Nothing more mysterious than that.
Multiple OLS is still OLS — one plane, 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.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