Module 8 · Day 71 of 80

Causal Inference Foundations

About 2.5 hours
Video lesson coming soon
Day 71 · Causal Inference Foundations
Module 8: Causal Inference with DoubleML · The recording will appear here as soon as it is published. The written lesson below is available now.

What you'll learn today

  • I can distinguish association, prediction and causation, and explain why good forecasts can mislead policy
  • I can define potential outcomes, ATE/ATT/CATE and a per-unit effect for a continuous treatment
  • I can state unconfoundedness, overlap, SUTVA and no anticipation, and why unconfoundedness is untestable
  • I built a simulation where confounding flips the sign of a naive estimate, and verified the OVB formula
  • I can explain why measurement error and non-linearity leave residual bias

Today's plan

Timed blocks for the day. Hover a bar for details.

LessonHands-on projectBreak2:30 total
Written lesson
TimeBlock
0:00 – 0:2071.1 Correlation, prediction and causation in macroeconomics
0:20 – 0:4871.2 Potential outcomes: the language of causal effects
0:48 – 0:55Break
0:55 – 1:2071.3 Identification: the assumptions that make effects estimable
1:20 – 1:4571.4 Simulation lab: watching confounding happen
1:45 – 1:53Break
1:53 – 2:1071.5 Experiments, natural experiments and observational macro data
2:10 – 2:3071.6 Mini-project: breaking and fixing the simulation

71.1 Correlation, Prediction and Causation in Macroeconomics (20 min)

Three different questions about the same data:

QuestionTypeAnswered by
Are high rates and strong housing seen together?associationa correlation
Given today's data, what will housing starts be next year?predictionModule 7's models
If the Bank raised rates by 25bp, what would housing starts be?causationModule 8

A good predictive model can be a terrible causal model. If rate hikes reliably occur in booms, a forecasting model should use "rates are rising" as a signal of a strong economy, and may predict more housing starts after a hike. That's a correct forecast and a wrong policy conclusion.

Why macro causality is especially hard

DifficultyIn our question
Policy responds to the economythe Bank sets rates because of inflation, growth and housing (a reaction function)
Expectations and anticipationmarkets and builders anticipate hikes (forward guidance), so effects can appear before the decision
Few independent observationsone country, a few hundred months, a handful of tightening cycles
Everything moves togetherbusiness-cycle variables are highly correlated
Lagsmonetary policy affects housing over months to years

Macroeconomists have developed specific strategies for these problems, for example narrative identification of policy shocks (Romer & Romer, 2004), high-frequency identification around announcements (e.g. Gürkaynak, Sack & Swanson, 2005), and local projections for dynamic effects (Jordà, 2005). This module uses a selection-on-observables design: control for the information the central bank reacted to, and use machine learning to do so flexibly (DoubleML). You'll also learn exactly what that design assumes, and therefore where it can fail.


71.2 Potential Outcomes: The Language of Causal Effects (28 min)

The potential outcomes framework (associated with Neyman and Rubin) defines causal effects precisely.

For each unit (here, a month t) and a binary treatment D (e.g. "the Bank hiked this month"):

  • Y(1) = housing starts that would occur if the Bank hiked;
  • Y(0) = housing starts that would occur if it didn't.

The causal effect for that month is Y(1) − Y(0).

The fundamental problem of causal inference

In any month we observe only one of the two: the one corresponding to what the Bank actually did. The other is a counterfactual, never observed. Causal inference is therefore always about estimating something we can't see, using assumptions to fill the gap.

MonthHiked?Y(1)Y(0)Effect
2022-07yesobserved??
2019-07no?observed?

Average effects

Since individual effects are unobservable, we estimate averages:

EstimandDefinitionQuestion
ATEE[Y(1) − Y(0)]average effect over all months
ATTE[Y(1) − Y(0) | D = 1]average effect in months when the Bank actually hiked
CATEE[Y(1) − Y(0) | X = x]effect in particular conditions (e.g. when inflation is high)

Continuous treatments

Policy rates change by different amounts (25bp, 50bp, 100bp). With a continuous treatment D (the change in the overnight rate, in percentage points), the natural estimand is the effect per unit: how much housing starts change per 1pp (or per 25bp) rate increase, holding the economic situation fixed. That's exactly what the partially linear model in DoubleML estimates (Days 74–77):

Y = θ · D + g(X) + ε

where θ is the causal effect per unit of D, and g(X) captures everything the economic situation X does to Y, in any flexible form.

Defining the outcome over horizons

Monetary policy works with lags, so the outcome must specify a horizon h:

Y_{t,h} = 100 × [log(housing starts_{t+h}) − log(housing starts_{t−1})]

the percentage change in housing starts from just before the decision to h months after it. Estimating θ for h = 3, 6, 12 and 24 months traces out a dynamic response, the same idea as local projections (Jordà, 2005).


71.3 Identification: The Assumptions That Make Effects Estimable (25 min)

Identification means: under which assumptions can the causal effect be computed from the data we have, given infinite data? Estimation (how to compute it well from finite data) comes later.

For a selection-on-observables design, the key assumptions are:

AssumptionStatementIn our question
Unconfoundedness (conditional ignorability)given controls X, treatment is "as good as random": (Y(1), Y(0)) ⫫ D | Xonce we control for what the Bank reacted to (inflation, growth, labour market, housing, global rates), remaining variation in rate changes is unrelated to what housing would otherwise have done
Overlap (positivity)every combination of X has some probability of each treatment levelwe observe hikes and non-hikes across a range of economic conditions
SUTVAone month's treatment doesn't change another month's potential outcomes, and the treatment is well definedstrained in macro: policy has persistent effects across months; we address this by defining dynamic outcomes and controlling for past policy
No anticipationoutcomes before the treatment aren't affected by itstrained by forward guidance; tested with placebo outcomes (Day 73.5)

Unconfoundedness is untestable. No statistic can prove that you controlled for every common cause. You can only make it plausible (with economic reasoning and a DAG, Day 73), probe it (placebo and negative-control tests), and quantify how strong an unobserved confounder would need to be to overturn your result (sensitivity analysis, Days 78–80). Machine learning helps you control for observed confounders flexibly. It does nothing about unobserved ones.

Where machine learning fits

Classical regression with controls assumes g(X) is linear. Reaction functions and housing dynamics are not obviously linear: the Bank may respond more strongly when inflation is far from target, and housing may react differently in booms and busts. DoubleML (Chernozhukov et al., 2018) uses flexible ML models to learn g(X) and the reaction function m(X) = E[D | X], while still delivering a valid estimate and standard error for θ. That combination is why it suits this question.


71.4 Simulation Lab: Watching Confounding Happen (25 min)

The best way to understand bias is to build a world where you know the true effect, then see which methods recover it.

Create causal/simulate.py:

"""Synthetic monthly economies where the true causal effect is known."""
import numpy as np
import pandas as pd


def confounded_world(T: int = 600, true_effect: float = -5.0, cycle_on_rates: float = 0.5,
                     cycle_on_housing: float = 10.0, seed: int = 42) -> pd.DataFrame:
    """A business cycle drives both rate changes (policy reaction) and housing.

    D = rate change (pp), Y = housing-starts growth (%), C = cycle (confounder).
    True causal effect of D on Y: `true_effect` per 1pp.
    """
    rng = np.random.default_rng(seed)
    c = np.zeros(T)
    for t in range(1, T):
        c[t] = 0.9 * c[t - 1] + rng.normal(0, 1)               # persistent business cycle
    d = cycle_on_rates * c + rng.normal(0, 0.5, T)             # the Bank reacts to the cycle
    y = cycle_on_housing * c + true_effect * d + rng.normal(0, 3, T)
    return pd.DataFrame({"cycle": c, "rate_change": d, "housing_growth": y})

Now estimate the effect two ways:

import statsmodels.formula.api as smf

from causal.simulate import confounded_world

df = confounded_world()
naive = smf.ols("housing_growth ~ rate_change", data=df).fit()
adjusted = smf.ols("housing_growth ~ rate_change + cycle", data=df).fit()

print(f"true effect:      -5.00")
print(f"naive estimate:   {naive.params['rate_change']:+.2f}  (se {naive.bse['rate_change']:.2f})")
print(f"adjusted estimate:{adjusted.params['rate_change']:+.2f}  (se {adjusted.bse['rate_change']:.2f})")

The naive estimate is typically positive and large: the regression "finds" that rate hikes raise housing starts. Controlling for the cycle recovers approximately −5.

Why: the omitted-variable bias formula

From econometrics: if the true model is Y = θD + βC + ε, and you omit C, the naive coefficient converges to

θ_naive = θ + β · δ, where δ = Cov(D, C) / Var(D)

the coefficient from regressing the omitted C on D. Here θ = −5, β = 10 (the cycle raises housing), and δ > 0 (the Bank hikes in booms), so the bias β·δ is large and positive, enough to flip the sign.

Verify the formula numerically:

delta = smf.ols("cycle ~ rate_change", data=df).fit().params["rate_change"]
print(f"θ + β·δ = -5 + 10 × {delta:.2f} = {-5 + 10 * delta:+.2f}   vs naive {naive.params['rate_change']:+.2f}")

The economist's sign test: before estimating anything, reason about the direction of likely bias. Common causes that raise both rate hikes and housing activity bias the naive estimate upwards (towards zero or positive). If a naive estimate shows that hikes raise housing, suspect confounding before believing it.


71.5 Experiments, Natural Experiments and Observational Macro Data (18 min)

DesignHow treatment is assignedUnconfoundednessFeasible for our question?
Randomised experimentby a coin flipguaranteed by designno: central banks don't randomise policy
Natural experimentby an "as-if random" event (e.g. an unexpected policy surprise)plausible if the event is truly unrelated to outcomespartly: monetary policy surprises (narrative or market-based)
Instrumental variablesvia a variable affecting treatment onlyrequires a valid instrumenthard to find in macro
Selection on observablesby the decision-maker, based on observed informationassumed, given good controlsyes: this module's main design, with ML controls

Selection on observables is plausible here for a specific reason: the Bank of Canada publishes its reasoning. Its decisions respond to a fairly well-understood set of indicators (inflation and its outlook, the output gap, the labour market, global conditions, housing and financial stability). If we control for that information set, much of the remaining variation in rate changes reflects timing, judgement and surprises, which is closer to "as good as random". This is the logic behind estimating policy shocks as residuals from a reaction function, an idea DoubleML formalises (Day 74).

The design's main weaknesses are worth stating up front:

  • variables the Bank saw but we don't (internal forecasts, market intelligence) are unobserved confounders;
  • anticipation through forward guidance blurs the timing of the treatment;
  • few tightening cycles limit overlap and precision.

Days 72–73 and 78–80 address each one explicitly.


71.6 Mini-Project: Breaking and Fixing the Simulation (20 min)

Extend causal/simulate.py and answer each question in notebooks/day71.ipynb, with numbers.

  1. Weak vs strong reaction. Set cycle_on_rates to 0.1, then 2.0. How does the naive bias change? Explain with δ in the OVB formula.
  2. A noisy confounder. Suppose we only observe cycle_obs = cycle + noise (measurement error, like a GDP-based output-gap estimate). Control for cycle_obs instead of cycle. Is the bias fully removed? Why not?
  3. Non-linear reaction. Make the Bank react only to strong booms: d = 0.8 * np.maximum(c, 0) + noise, and make housing respond non-linearly: y = 3 * c**2 + .... Does a linear control for cycle still recover −5? What does this suggest about flexible ML controls?
  4. Sample size. With T = 150 (about 12 years of monthly data), run 200 simulations with different seeds and plot the distribution of the adjusted estimate. How precise can we hope to be with real Canadian data?
Answers in brief
  1. δ grows with the strength of the reaction, so the bias grows. With a weak reaction, naive is closer to −5.
  2. No: measurement error in a confounder leaves residual confounding. Controlling for a noisy proxy removes only part of the bias. This is an important reason to use several indicators of the cycle.
  3. A linear control under-fits the non-linear confounding, so bias remains. Adding cycle**2 (or using a flexible model of g(X)) fixes it. That's the motivation for DoubleML's ML nuisance functions.
  4. The estimates spread widely. With roughly a decade of monthly data and modest variation in rate changes, confidence intervals will be wide. Expect that with real data too, and report it honestly.

Day 71 checklist

  • I can distinguish association, prediction and causation, and explain why good forecasts can mislead policy
  • I can define potential outcomes, ATE/ATT/CATE and a per-unit effect for a continuous treatment
  • I can state unconfoundedness, overlap, SUTVA and no anticipation, and why unconfoundedness is untestable
  • I built a simulation where confounding flips the sign of a naive estimate, and verified the OVB formula
  • I can explain why measurement error and non-linearity leave residual bias
git add causal notebooks forecasting/requirements-ml.txt
git commit -m "Day 71: potential outcomes, identification, confounding simulations"
git push