Overview
This tutorial shows how to use leanfe for instrumental variables (IV) regression with the three-part formula syntax.
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
| Basic IV |
y ~ endog \| fe \| z |
| Multiple instruments |
y ~ endog \| fe \| z1 + z2 |
| With controls |
y ~ endog + x1 \| fe \| z |