The Question

Act I

Could a central bank have stopped a banking panic?

In 1930 a Nashville banking giant, Caldwell and Company, collapsed — and bank failures spread across the American South within weeks.

If the Federal Reserve had flooded struggling banks with cash, would more banks have survived, or would the Depression have swept them away regardless?

A Fed district border split Mississippi into two policy regimes

Caldwell collapse, 1930

6th District: Atlanta Fed lends

8th District: St. Louis Fed tightens

Banks still in business?

Same state, same shock, opposite policies — a natural experiment drawn by a map.

Even in the rescue district, banks kept disappearing

Year 6th District (Atlanta Fed) 8th District (St. Louis Fed)
1930 135 165
1931 121 132
1933 102 111

Mississippi banks in business on July 1 of each year.

The Atlanta district lost 33 banks by 1933. Did the rescue fail — or would it have been worse?

Our route through the chapter

  • The question — why one-way comparisons (before vs after, treated vs untreated) mislead
  • The investigation — the DD formula and parallel trends; the Mississippi banks; regression DD with fixed effects; the drinking-age case study and its robustness checks
  • The resolution — the traps to avoid and what DD can and cannot promise

Three ideas carry the whole chapter

Counterfactual

The control group’s path stands in for what the treated group would have done untreated.

Without treatment, both groups would have changed by the same amount — levels may differ.

Panel data

The same units tracked over time; fixed effects absorb constant gaps; DD gives the ATT.

DD trades the coin flip of an experiment for a borrowed trend.

The Investigation

Act II

A before-after change bundles the policy with everything else that happened

\[\underbrace{\text{After} - \text{Before}}_{\text{6th District: } -14 \text{ banks}} \;=\; \text{Policy effect} \;+\; \text{Depression trend}\]

From 1930 to 1931 the Atlanta district lost 14 banks — but the Depression was hitting every bank in the country at the same time.

With one group, we cannot tell the policy’s effect from the times it happened in.

Your friend’s weight change reveals what your diet did

Before → after the holidays Change
You (on a diet) gained weight +2 lbs
Your friend (no diet) gained weight +7 lbs
Difference in changes −5 lbs

Without the diet you would have gained 7 lbs like your friend — so the diet saved you 5.

DD subtracts the control group’s change from the treated group’s change

\[\delta_{DD} = \underbrace{\left(\bar{Y}_{\text{treat, after}} - \bar{Y}_{\text{treat, before}}\right)}_{\text{change in treated}} - \underbrace{\left(\bar{Y}_{\text{control, after}} - \bar{Y}_{\text{control, before}}\right)}_{\text{change in control}}\]

Two differences: first over time within each group, then across the groups.

The control group’s change is our estimate of what time alone would have done to the treated group.

Differencing twice removes both fixed gaps and common shocks

Treated: before

Change in treated

Treated: after

Control: before

Change in control

Control: after

DD estimate

Differencing over time cancels each group’s permanent level; differencing across groups cancels shocks that hit both.

If the groups would have trended differently anyway, DD absorbs that gap as bias

\[\delta_{DD} \;=\; \text{Treatment effect} \;+\; \underbrace{\left(\text{Trend}_{\text{treat}} - \text{Trend}_{\text{control}}\right)}_{\text{zero under parallel trends}}\]

Parallel trends: absent treatment, both groups would have changed by the same amount.

Parallel trends is the assumption that carries the whole design — and it can never be fully tested.

DD needs the same trend, not the same level

Levels in 1930

  • Atlanta district: 135 banks
  • St. Louis district: 165 banks
  • A gap of 30 banks before the crisis

What DD requires

  • No need to close the 30-bank gap
  • Only that, without the policy, both would have lost banks at the same pace

Groups can start miles apart; they only need to travel in the same direction at the same speed.

Both districts shrank, but St. Louis shrank far faster

Banks in business in each district, 1929–1934. Solid: actual; dashed: the 6th District’s counterfactual.

After 1930 the tight-credit 8th District lost banks much more quickly than the easy-credit 6th.

The counterfactual puts the Atlanta district on St. Louis’s path

\[\text{Counterfactual 6th}_t = 135 + \left(\text{8th}_t - 165\right)\]

Year 6th District, actual 6th District, counterfactual
1931 121 102
1932 113 90
1933 102 81
1934 102 79

The gap between actual and counterfactual is the number of banks the policy kept open.

DD: the Atlanta Fed kept roughly 19–23 extra banks alive

Year Change in 6th (treated) Change in 8th (control) DD (banks saved)
1931 −14 −33 19
1932 −22 −45 23
1933 −33 −54 21
1934 −33 −56 23

Changes measured relative to 1930.

Easy credit saved about 20 banks — and the gap persisted for years after the panic.

A few lines of pandas compute the banking DD

import pandas as pd                       # pandas → load and reshape tables

# every course dataset lives in the repo's data/ folder on GitHub
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
banks = pd.read_csv(GITHUB_DATA_URL + "ch5/banks_clean.csv").set_index("year")

change_6 = banks["bib6"] - banks.loc[1930, "bib6"]   # treated (Atlanta) change since 1930
change_8 = banks["bib8"] - banks.loc[1930, "bib8"]   # control (St. Louis) change since 1930
dd = change_6 - change_8                             # DD = treated change − control change
print(dd.loc[1931:])                                 # 19, 23, 21, 23 banks saved

↗ Try it in Colab

With many states and years, DD becomes a regression with fixed effects

\[Y_{st} = \alpha + \delta \, D_{st} + \sum_s \beta_s \, \text{STATE}_s + \sum_t \gamma_t \, \text{YEAR}_t + e_{st}\]

\(Y_{st}\): death rate in state \(s\), year \(t\) (mrate) · \(D_{st}\): treatment (legal) · \(\delta\): the DD effect.

The coefficient \(\delta\) generalizes “treated change minus control change” to many groups and periods.

State effects absorb permanent gaps; year effects absorb nationwide shocks

State fixed effects \(\beta_s\)

  • Geography, culture, road quality
  • Anything constant over time within a state
  • Replaces “before vs after” within each group

Year fixed effects \(\gamma_t\)

  • Safer cars, national campaigns, recessions
  • Anything that hits every state in a year
  • Replaces “minus the control group’s change”

Together they leave only within-state, over-time variation that differs from the national pattern.

Death rates are sticky within a state, so standard errors must be clustered

Standard error for the legal effect SE t
Robust, treats every state-year as independent 2.72 3.97
Clustered by state 4.42 2.44

All-cause death rate, state and year fixed effects; estimate 10.80 in both rows.

Ignoring serial correlation would shrink the standard error by nearly 40% — false confidence.

States changed their drinking ages at different times

When What changed
1933 Prohibition ends; each state sets its own drinking age
1970–1975 Many states lower the age to 18, each on its own date
1980s States raise it back to 21, again at different dates
1984 Federal legislation pushes every state toward 21

Panel: 51 states \(\times\) 14 years (1970–1983) = 714 state-years of death rates for 18–20 year olds.

Staggered adoption creates many treated-vs-control comparisons, not just one.

Treatment is the share of young adults who may legally drink

\[D_{st} = \text{fraction of 18–20 year olds in state } s \text{ allowed to drink in year } t\]

  • \(D_{st} = 0\): nobody aged 18–20 may drink · \(D_{st} = 1\): all of them may
  • Values in between when the age is 19 or 20, or the law changed mid-year

A continuous dose: \(\delta\) is the effect of moving from no legal access to full legal access.

One pf.feols call runs the regression DD

!pip install -q pyfixest                  # pyfixest is not preinstalled in Colab
import pandas as pd                       # pandas → load tables
import pyfixest as pf                     # pyfixest → regressions with fixed effects
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
deaths = pd.read_csv(GITHUB_DATA_URL + "ch5/deaths_clean.csv")   # state × year × cause
allcause = deaths[deaths["dtype"] == "all"]          # 51 states × 14 years = 714 rows

# death rate on legal-drinking share; absorb state and year fixed effects after "|"
m = pf.feols("mrate ~ legal | state + year", data=allcause,
             vcov={"CRV1": "state"})      # cluster SEs by state
m.summary()                               # legal: 10.80 (SE 4.42)

↗ Try it in Colab

Motor-vehicle crashes account for most of the extra deaths

Cause of death legal effect (SE) t
All causes 10.80 (4.42) 2.44
Motor-vehicle accidents 7.59 (2.41) 3.16
Suicide 0.59 (0.57) 1.04
Homicide 0.50 (0.93) 0.54

About 7–8 of the ~11 extra deaths happened on the road.

Deaths from disease did not respond — the placebo test passes

Cause of death legal effect (SE) t
Motor-vehicle accidents 7.59 (2.41) 3.16
Internal causes (disease) 1.33 (1.53) 0.87

Internal causes: cancer, heart disease and other illnesses with no short-run link to legal drinking.

If DD “found” an effect on disease, we would suspect a confounding trend; it finds none.

A loop runs the same DD for every cause of death

!pip install -q pyfixest                  # pyfixest is not preinstalled in Colab
import pandas as pd                       # pandas → load tables
import pyfixest as pf                     # pyfixest → regressions with fixed effects
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
deaths = pd.read_csv(GITHUB_DATA_URL + "ch5/deaths_clean.csv")   # state × year × cause

for cause in ["all", "MVA", "suicide", "homicide", "internal"]:   # one DD per cause
    d = deaths[deaths["dtype"] == cause]                           # keep one cause
    # same regression DD each time: state + year FE, SEs clustered by state
    m = pf.feols("mrate ~ legal | state + year", data=d, vcov={"CRV1": "state"})
    print(cause, round(m.coef()["legal"], 2), round(m.se()["legal"], 2))  # coef, SE

↗ Try it in Colab

Giving each state its own trend barely dents the estimate

Cause of death Baseline DD With state-specific trends
All causes 10.80 (4.42) 8.47 (4.90)
Motor-vehicle accidents 7.59 (2.41) 6.64 (2.55)
Internal causes (placebo) 1.33 (1.53) 0.08 (1.86)

Standard errors in parentheses, clustered by state.

The drinking-age effect is a break from each state’s own path, not a pre-existing drift.

One extra term adds a linear trend for every state

!pip install -q pyfixest                  # pyfixest is not preinstalled in Colab
import pandas as pd                       # pandas → load tables
import pyfixest as pf                     # pyfixest → regressions with fixed effects
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
deaths = pd.read_csv(GITHUB_DATA_URL + "ch5/deaths_clean.csv")   # state × year × cause
mva = deaths[deaths["dtype"] == "MVA"].copy()         # motor-vehicle deaths only
mva["year_num"] = mva["year"] - 1970                  # years since 1970 (0, 1, …, 13)

# i(state, year_num) gives every state its own linear time trend
m = pf.feols("mrate ~ legal + i(state, year_num) | state + year",
             data=mva, vcov={"CRV1": "state"})        # cluster SEs by state
print(m.coef()["legal"], m.se()["legal"])             # 6.64 (2.55) vs 7.59 without trends

↗ Try it in Colab

Weighting states by population leads to the same conclusion

Cause of death Unweighted Population-weighted
All causes 10.80 (4.42) 12.41 (4.43)
Motor-vehicle accidents 7.59 (2.41) 7.50 (2.18)
Suicide 0.59 (0.57) 1.49 (0.85)
Internal causes (placebo) 1.33 (1.53) 1.89 (1.71)

Whether each state or each young person counts equally, the effect is about 7–8 on the road.

weights="pop" turns the regression into weighted least squares

!pip install -q pyfixest                  # pyfixest is not preinstalled in Colab
import pandas as pd                       # pandas → load tables
import pyfixest as pf                     # pyfixest → regressions with fixed effects
GITHUB_DATA_URL = "https://raw.githubusercontent.com/cmg777/intro2causal/main/data/"
deaths = pd.read_csv(GITHUB_DATA_URL + "ch5/deaths_clean.csv")   # state × year × cause
allcause = deaths[deaths["dtype"] == "all"]          # all-cause death rates

# weights="pop" → weighted least squares: big states count more
m = pf.feols("mrate ~ legal | state + year", data=allcause,
             weights="pop", vcov={"CRV1": "state"})   # cluster SEs by state
print(m.coef()["legal"], m.se()["legal"])             # 12.41 (4.43) vs 10.80 unweighted

↗ Try it in Colab

Controlling for beer taxes barely moves the drinking-age effect

Cause of death legal effect (SE) Beer-tax effect (SE)
All causes 10.98 (4.52) 1.51 (8.74)
Motor-vehicle accidents 7.59 (2.47) 3.82 (5.20)

State and year fixed effects; SEs clustered by state. Baseline without the control: 10.80 and 7.59.

A policy that changed at the same time does not explain the result away.

Each design buys identification with a different assumption

RCT (Ch 1) IV (Ch 3) RD (Ch 4) DD (Ch 5)
Key requirement Random assignment Valid instrument Sharp cutoff Parallel trends
Handles unobservables? Yes, by randomization Yes, via the instrument Yes, at the cutoff Only time-invariant ones
Estimates ATE LATE (compliers) Local effect at cutoff ATT (treated group)
Data structure Cross-section Cross-section or panel Running variable Panel (group \(\times\) time)

DD is the workhorse when policy varies across places and over time but no one flipped a coin.

Two chapters, one question: age cutoffs versus policy timing

RD (Chapter 4)

  • Compares young adults just under vs just over age 21
  • Assumption: nothing else jumps at the birthday

DD (Chapter 5)

  • Compares states before vs after their laws change
  • Assumption: without the change, states trend in parallel

Different designs with different assumptions pointing the same way make the evidence far stronger.

John Snow used DD logic to trace cholera to dirty water

Water company Before 1852 After 1852 Cholera deaths
Lambeth (treated) Polluted stretch of the Thames Moved intake upstream Fell sharply
Southwark & Vauxhall (control) Polluted stretch of the Thames Same polluted intake No comparable fall

The change appeared only where the water changed — so cholera traveled by water, not by “bad air”.

The Resolution

Act III

The tempting mistake: “DD needs groups that look alike”

Objection. The Atlanta district had 135 banks and St. Louis 165 — they are too different to compare.

Response. DD differences away any fixed gap. It needs the groups to share a trend, not a level — the 30-bank gap cancels in the first difference.

The tempting mistake: “Fixed effects remove every confounder”

Objection. With state and year fixed effects, nothing else can explain the result.

Response. Fixed effects remove only what is constant within a state or common to all states in a year. A state-specific change over time — like a new beer tax — survives them, which is why we add trends, controls and placebos.

Key takeaways

  • DD = change in the treated group − change in the control group
  • Parallel trends is the assumption that carries the design — same trend, not same level
  • Regression DD: state and year fixed effects, standard errors clustered by state
  • Banks: the Atlanta Fed’s easy credit kept roughly 19–23 banks in business
  • Drinking age: legal access raised youth deaths, mostly on the road — robust to trends, weights and placebos

When a policy shifts for some groups but not others, the untreated group’s change supplies the missing counterfactual.