The Question

Act I

Is an elite private college worth roughly $20,000 a year more?

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?

The raw earnings gap mixes school quality with student quality

raises odds

raises

causal effect?

Ability and ambition

Private college

Earnings

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.

Our route through the chapter

  • The question — why the raw private-college premium is not a causal effect
  • The investigation — regression with controls; omitted variables bias in a simulation lab; the Dale–Krueger private-college study; sensitivity checks; bad controls
  • The resolution — when regression earns a causal reading, and the traps that remain

Five terms carry the whole chapter

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.

The Investigation

Act II

Choosing a college is Chapter 1’s selection problem — without the lottery

\[\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 works like an automated matchmaker

Alike on paper

  • Same test scores
  • Same family income
  • Same schools applied to

Different choice

  • One enrolled private
  • One enrolled public
  • Compare their earnings

Regression averages the earnings gap across many such matched pairs.

A regression with controls compares students who share the same \(X\)

\[Y_i = \alpha + \beta P_i + \gamma X_i + e_i\]

  • \(Y_i\) — earnings (earnings)
  • \(P_i\) — 1 if private, 0 if public (private)
  • \(X_i\) — a control, e.g. ability (ability)
  • \(\beta\) — private effect, holding \(X_i\) fixed
  • \(\gamma\) — effect of the control
  • \(e_i\) — residual: everything else

\(\beta\) is the private–public gap among students with the same value of the control.

OLS chooses the coefficients that make squared prediction errors smallest

\[\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.

Controls turn a difference in means into an “other things equal” comparison

Chapter 1

\[Y_i = \alpha + \beta D_i + e_i\] \(\beta\) = treated mean − control mean. Fair because \(D_i\) was randomized.

Chapter 2

\[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.

Regression is causal only if, given the controls, school choice is as good as random

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.

A simulation plants a true effect of $5,000 — so we can check who finds it

raises odds

+$800 per point

true effect +$5,000

Ability

Private college

Earnings

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.

Ten lines build the simulated students and show the raw gap

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

↗ Try it in Colab

The short regression reproduces the raw gap — nearly three times the truth

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.

Holding ability fixed brings the estimate back to about $5,000

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.

Two 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)

↗ Try it in Colab

The gap between short and long is the omitted variable’s footprint

\[\text{OVB} \;=\; \beta^s - \beta^l \;=\; \pi_1 \times \gamma\]

  • \(\beta^s\), \(\beta^l\) — private coefficient in the short and long regressions
  • \(\pi_1\) — how the omitted variable differs with treatment
  • \(\gamma\) — its effect on the outcome (long regression)

If either factor is zero — unrelated to treatment, or irrelevant to the outcome — there is no bias.

An auxiliary regression measures how the omitted variable travels with treatment

\[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.

In the simulation the formula matches short minus long exactly

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.

Three regressions verify the OVB formula in code

!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

↗ Try it in Colab

Signs alone predict the direction of the bias

\(\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.

Dale and Krueger let application choices reveal ambition

  • Data: the College and Beyond survey — more than 14,000 college students followed into the labor market
  • Insight: the schools a student applies to reveal ambition and ability
  • The data are not public, so we discuss their findings; the simulation showed the same mechanics

Applications are a pre-treatment signal of exactly the traits that bias the raw comparison.

The self-revelation design: same admissions, different enrollment choice

Admitted to same schools

Enrolled private

Enrolled public

Compare earnings

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.

The private premium shrinks from 14% to about zero as the controls sharpen

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.

Most of the elite-college premium is who enrolls, not what the school adds

≈ 0

private-college earnings premium once applications are held fixed (raw gap: about 14%)

Adding SAT scores, parental income and demographics barely moves the estimate

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.

Given application behavior, the extra traits no longer travel with private enrollment

\[\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.

When each new control moves the estimate a lot, more confounders may lurk

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.

A three-line loop runs the sensitivity analysis

!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,720

↗ Try it in Colab

Controlling for occupation blocks one of the paths a college works through

direct

opens doors

pays more

Private college

Earnings

Occupation

Holding 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.

Control only for traits fixed before the treatment

Good controls — pre-treatment

  • Ability, test scores
  • Parental income
  • Schools applied to

Bad controls — post-treatment

  • Occupation
  • Graduate degree
  • Industry

Outcomes of the treatment are not confounders. Chapter 6 returns to this trap.

Every later method in the course is built on regression

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.

Regression began as a tool for description, not causation

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.

The Resolution

Act III

The tempting mistake: “Elite graduates earn more, so an elite college pays off”

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.

The tempting mistake: “When in doubt, just add more controls”

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.

Key takeaways

  • Regression compares treated and untreated units with the same controls
  • \(\text{OVB} = \pi_1 \times \gamma\) — and the signs predict the direction of bias
  • Stable estimates across specifications build confidence
  • Never control for outcomes of the treatment
  • The private-college premium largely vanishes with the right controls

Regression delivers causal effects only when the right pre-treatment controls make treatment as good as random.