Act I
Elite private universities charge about $20,000 more per year in tuition than public ones — and their graduates go on to earn substantially more.
Does the school raise earnings, or do its students bring the earning power with them?
No admissions office flips a coin, so there is no experiment to lean on.
Private-college students are smarter, more driven and better connected — they might out-earn others wherever they enrolled.
| Term | Plain meaning |
|---|---|
| Control variable | A pre-treatment trait held fixed in the regression |
| Ceteris paribus | “Other things equal” — compare people with the same controls |
| Short vs long regression | Fewer vs more controls; the gap between them reveals bias |
| Omitted variables bias (OVB) | The distortion from leaving a confounder out |
| Conditional independence (CIA) | Given the controls, treatment is as good as random |
Every slide that follows uses these five ideas.
Act II
\[\text{Observed gap} \;=\; \text{Causal effect} \;+\; \text{Selection bias}\]
In Chapter 1 random assignment set the selection-bias term to zero. Here, students pick their own schools, so the bias term stays.
We need another way to make the comparison fair: compare students who are alike in the ways that matter.
Regression averages the earnings gap across many such matched pairs.
\[Y_i = \alpha + \beta P_i + \gamma X_i + e_i\]
earnings)private)ability)\(\beta\) is the private–public gap among students with the same value of the control.
\[\min_{\alpha,\,\beta,\,\gamma}\ \sum_{i=1}^{n} \big(Y_i - \alpha - \beta P_i - \gamma X_i\big)^2\]
Each term in the sum is a squared residual \(e_i^2\): the gap between actual and predicted earnings.
Like a tailor fitting a suit: no single measurement is perfect, but the total mismatch is as small as possible.
\[Y_i = \alpha + \beta D_i + e_i\] \(\beta\) = treated mean − control mean. Fair because \(D_i\) was randomized.
\[Y_i = \alpha + \beta P_i + \gamma X_i + e_i\] \(\beta\) = gap within groups that share \(X_i\). Needed because \(P_i\) is chosen.
Controls do for observational data what the lottery did for experiments — if they are the right controls.
Conditional independence assumption (CIA): among students with the same controls, who goes private is unrelated to what they would have earned.
\[E[Y_{0i} \mid P_i, X_i] = E[Y_{0i} \mid X_i]\]
Within a cell of identical \(X_i\), private and public students would have earned the same on average without the private school.
The CIA cannot be tested directly — the choice of controls has to be argued for.
1,000 simulated students · ability has mean 50, sd 10 · abler students are likelier to go private
Because we wrote the rules, we know the right answer: $5,000.
import numpy as np # numpy → random numbers
import pandas as pd # pandas → tables and group means
np.random.seed(42) # fix the seed so everyone gets the same draws
n = 1000 # 1,000 simulated students
ability = np.random.normal(50, 10, n) # ability (mean 50, sd 10): the confounder
p_private = 1 / (1 + np.exp(-(ability - 50) / 5)) # abler students lean private
private = np.random.binomial(1, p_private) # 1 = private, 0 = public
# truth: private adds $5,000; each ability point adds $800; plus noise
earnings = 30000 + 5000 * private + 800 * ability + np.random.normal(0, 5000, n)
students = pd.DataFrame({"earnings": earnings, "private": private, "ability": ability})
students.groupby("private")[["earnings", "ability"]].mean() # raw gaps by school type| Public mean | Private mean | Gap | |
|---|---|---|---|
| Earnings ($) | 65,387 | 80,146 | 14,760 |
| Ability (points) | 44.4 | 56.1 | 11.7 |
Short regression earnings ~ private: coefficient 14,760 (robust SE 512) · true effect 5,000
Private students earn more largely because they score 11.7 points higher on ability.
| Regression | Controls | Private coefficient (robust SE) |
|---|---|---|
| Short | none | 14,760 (512) |
| Long | ability | 5,079 (391) |
| Truth | — | 5,000 |
In the long regression each ability point adds $826 (SE 19) — close to the $800 we planted.
Control for the confounder and regression recovers the causal effect.
pf.feols calls contrast the short and the long regression!pip install -q pyfixest # pyfixest is not preinstalled in Colab
import numpy as np, pandas as pd # numbers + tables
import pyfixest as pf # pyfixest → regressions with robust SEs
np.random.seed(42); n = 1000 # rebuild the same 1,000 students
ability = np.random.normal(50, 10, n) # the confounder
private = np.random.binomial(1, 1 / (1 + np.exp(-(ability - 50) / 5))) # abler → private
earnings = 30000 + 5000 * private + 800 * ability + np.random.normal(0, 5000, n) # truth +5,000
df = pd.DataFrame({"earnings": earnings, "private": private, "ability": ability}) # one table
short = pf.feols("earnings ~ private", data=df, vcov="hetero") # ability left out
long = pf.feols("earnings ~ private + ability", data=df, vcov="hetero") # ability held fixed
pf.etable([short, long]) # private: 14,760 (short) vs 5,079 (long)\[\text{OVB} \;=\; \beta^s - \beta^l \;=\; \pi_1 \times \gamma\]
If either factor is zero — unrelated to treatment, or irrelevant to the outcome — there is no bias.
\[A_i = \pi_0 + \pi_1 P_i + u_i\]
\(A_i\) is ability (ability), regressed on the private dummy \(P_i\).
In the simulation, \(\pi_1 = 11.72\): private students score 11.72 ability points higher than public students.
\(\pi_1 > 0\): the omitted trait rides along with private enrollment.
| Piece | Value |
|---|---|
| Short coefficient \(\beta^s\) | 14,760 |
| Long coefficient \(\beta^l\) | 5,079 |
| OVB, direct: \(\beta^s - \beta^l\) | 9,681 |
| \(\pi_1\) (ability on private) | 11.72 |
| \(\gamma\) (ability in long regression) | 826 |
| OVB, formula: \(\pi_1 \times \gamma\) | 9,681 |
Positive × positive = upward bias: omitting ability made private college look too good.
!pip install -q pyfixest # pyfixest is not preinstalled in Colab
import numpy as np, pandas as pd # numbers + tables
import pyfixest as pf # pyfixest → regressions with robust SEs
np.random.seed(42); n = 1000 # rebuild the same 1,000 students
ability = np.random.normal(50, 10, n) # the confounder
private = np.random.binomial(1, 1 / (1 + np.exp(-(ability - 50) / 5))) # abler → private
earnings = 30000 + 5000 * private + 800 * ability + np.random.normal(0, 5000, n) # truth +5,000
df = pd.DataFrame({"earnings": earnings, "private": private, "ability": ability}) # one table
short = pf.feols("earnings ~ private", data=df, vcov="hetero") # short regression
long = pf.feols("earnings ~ private + ability", data=df, vcov="hetero") # long regression
aux = pf.feols("ability ~ private", data=df, vcov="hetero") # auxiliary regression
pi_1, gamma = aux.coef()["private"], long.coef()["ability"] # 11.72 and 826
print(short.coef()["private"] - long.coef()["private"], pi_1 * gamma) # both ≈ 9,681| \(\pi_1\) (omitted ↔︎ treatment) | \(\gamma\) (omitted → outcome) | OVB direction |
|---|---|---|
| Positive | Positive | Upward |
| Positive | Negative | Downward |
| Negative | Positive | Downward |
| Negative | Negative | Upward |
You can reason about bias from a confounder you never observe — just argue the two signs.
Applications are a pre-treatment signal of exactly the traits that bias the raw comparison.
Two students admitted to both an elite private college and a flagship public university, who chose differently.
Equally qualified on paper — the only difference left is where they enrolled.
| Comparison | Controls | Private premium |
|---|---|---|
| Raw | none | about 14% |
| Selectivity groups | Barron’s selectivity category | about 7% |
| Self-revelation | schools applied to and admitted by | close to zero |
Each better control strips away more selection bias — OVB at work on real data.
≈ 0
private-college earnings premium once applications are held fixed (raw gap: about 14%)
Once the application-set controls are in, piling on further traits leaves the private coefficient nearly unchanged.
Like stress-testing a bridge: if it holds under different loads, we trust it is sturdy.
Stable estimates across specifications build confidence in a causal reading.
\[\text{OVB from SAT, income, …} \;=\; \pi_1 \times \gamma \;\approx\; 0 \times \gamma \;=\; 0\]
Among students with the same applications, SAT scores and parental income barely differ between private and public enrollees.
The OVB formula explains the stability: when \(\pi_1 \approx 0\), omitting a trait costs almost no bias.
| Controls in the regression | Private coefficient |
|---|---|
| None | 18,506 |
| + ability | 12,429 |
| + ability + family income | 8,428 |
| + ability + family income + motivation | 4,720 |
A simulation with three confounders and a true effect of $5,000.
Big jumps are a warning sign; only the full set of confounders lands near the truth.
!pip install -q pyfixest # pyfixest is not preinstalled in Colab
import numpy as np, pandas as pd # numbers + tables
import pyfixest as pf # pyfixest → regressions with robust SEs
np.random.seed(42); n = 1000 # 1,000 students, now with THREE confounders
ability = np.random.normal(50, 10, n) # confounder 1
income = np.random.normal(60000, 20000, n) # confounder 2: family income
motivation = np.random.normal(5, 2, n) # confounder 3
index = (ability - 50) / 5 + (income - 60000) / 20000 + (motivation - 5) / 2 # all push to private
private = np.random.binomial(1, 1 / (1 + np.exp(-index))) # 1 = private
earnings = (10000 + 5000 * private + 800 * ability + 0.3 * income # truth: +5,000 ...
+ 2000 * motivation + np.random.normal(0, 5000, n)) # ... plus confounders
df = pd.DataFrame(dict(earnings=earnings, private=private, ability=ability,
income=income, motivation=motivation)) # one table
for x in ["", " + ability", " + ability + income", " + ability + income + motivation"]:
m = pf.feols("earnings ~ private" + x, data=df, vcov="hetero") # add controls one by one
print(x or "none", round(m.coef()["private"])) # 18,506 → ... → 4,720Holding occupation fixed asks: among people in the same job, do private graduates earn more?
A bad control removes part of the very effect we want to measure — here, biasing it downward.
Outcomes of the treatment are not confounders. Chapter 6 returns to this trap.
| Chapter | How regression appears |
|---|---|
| 1 · Randomized trials | A difference in means is a regression on a treatment dummy |
| 3 · Instrumental variables | First stage and reduced form are regressions; 2SLS uses fitted values |
| 4 · Regression discontinuity | Regression with a polynomial in the running variable |
| 5 · Differences-in-differences | Regression with group and time fixed effects |
| 6 · Wages of schooling | OLS as the baseline; twins fixed effects as a differenced regression |
Master pf.feols once and you can run every design in the course.
| When | Who | Contribution |
|---|---|---|
| 1886 | Francis Galton | Tall parents have tall but less extreme children — “regression to the mean” |
| 1899 | George Udny Yule | Used several controls at once to study changes in English pauperism |
The causal reading of \(\beta\) is modern — it rests on the assumptions of this lecture.
Act III
Objection. Graduates of top private colleges out-earn everyone else, so the tuition must be worth it.
Response. The raw gap is mostly selection. Compare students with the same applications and the premium falls from about 14% to close to zero.
Objection. More controls can only make the estimate cleaner.
Response. Only pre-treatment confounders reduce bias. Irrelevant controls waste precision, and bad controls — outcomes of the treatment — create new bias.