IV Regression

Two-stage least squares with fixed effects

Overview

This tutorial shows how to use leanfe for instrumental variables (IV) regression with the three-part formula syntax.

The Three-Part Formula

y ~ endog | fe | instrument
  • Part 1 (y ~ endog): Outcome and endogenous variable(s)
  • Part 2 (fe): Fixed effects to absorb
  • Part 3 (instrument): Instrument(s)
Formula Description
y ~ x \| fe \| z One endogenous var, one instrument
y ~ x1 + x2 \| fe \| z1 + z2 Multiple instruments
y ~ endog + exog \| fe1 + fe2 \| z Exogenous controls + IV

Example

import numpy as np
import polars as pl
from leanfe import leanfe

np.random.seed(42)
n_obs = 50_000

# Generate data with endogeneity
ability = np.random.normal(0, 1, n_obs)  # Unobserved confounder
instrument = np.random.normal(0, 1, n_obs)  # Exogenous instrument

# Endogenous variable depends on ability AND instrument
education = 12 + 0.5 * ability + 0.3 * instrument + np.random.normal(0, 1, n_obs)

# Outcome depends on education AND ability (omitted)
true_return = 0.08
log_wage = 2 + true_return * education + 0.2 * ability + np.random.normal(0, 0.5, n_obs)

# Add fixed effects
region = np.random.randint(1, 11, n_obs)
year = np.random.randint(2015, 2025, n_obs)
log_wage = log_wage + np.random.normal(0, 0.1, 11)[region] + np.random.normal(0, 0.05, 11)[year - 2015]

df = pl.DataFrame({
    "log_wage": log_wage,
    "education": education,
    "instrument": instrument,
    "region": region,
    "year": year,
})

print(f"Data: {n_obs:,} observations")
print(f"True coefficient: {true_return}")
Data: 50,000 observations
True coefficient: 0.08

OLS vs IV

# OLS (biased)
result_ols = leanfe(
    formula="log_wage ~ education | region + year",
    data=df,
    vcov="iid"
)

# IV/2SLS
result_iv = leanfe(
    formula="log_wage ~ education | region + year | instrument",
    data=df,
    vcov="iid"
)

print(f"True effect:  {true_return}")
print(f"OLS:          {result_ols['coefficients']['education']:.4f} (SE: {result_ols['std_errors']['education']:.4f})")
print(f"IV:           {result_iv['coefficients']['education']:.4f} (SE: {result_iv['std_errors']['education']:.4f})")
print(f"\nIs IV: {result_iv['is_iv']}")
print(f"Instruments: {result_iv['n_instruments']}")
True effect:  0.08
OLS:          0.2348 (SE: 0.0008)
IV:           0.0906 (SE: 0.0083)

Is IV: True
Instruments: 1

Multiple Instruments

# Add second instrument
instrument2 = np.random.normal(0, 1, n_obs)
education_new = 12 + 0.5 * ability + 0.3 * instrument + 0.2 * instrument2 + np.random.normal(0, 1, n_obs)
log_wage_new = 2 + true_return * education_new + 0.2 * ability + np.random.normal(0, 0.5, n_obs)

df2 = df.with_columns([
    pl.Series("instrument2", instrument2),
    pl.Series("education", education_new),
    pl.Series("log_wage", log_wage_new),
])

result_iv2 = leanfe(
    formula="log_wage ~ education | region + year | instrument + instrument2",
    data=df2,
    vcov="iid"
)

print(f"IV with 2 instruments: {result_iv2['coefficients']['education']:.4f}")
print(f"Number of instruments: {result_iv2['n_instruments']}")
IV with 2 instruments: 0.0710
Number of instruments: 2

Quick Reference

Task Formula
Basic IV y ~ endog \| fe \| z
Multiple instruments y ~ endog \| fe \| z1 + z2
With controls y ~ endog + x1 \| fe \| z

Next Steps