Neyman Orthogonality and Double Machine Learning
Regularization bias in ML nuisance estimation, Neyman orthogonality as the fix, cross-fitting for valid inference, the full DML algorithm, EconML implementation, CATE, sensitivity analysis, and the influence-function view.
On this page
- Introduction
- The regularization bias problem
- The Double Machine Learning solution
- Neyman orthogonality: the key insight
- Motivation: when does bias in nuisance functions matter?
- Neyman orthogonality condition
- Example: partially linear model
- The orthogonal score: partialling out
- Why orthogonality enables machine learning
- The Double Machine Learning algorithm
- The overfitting problem
- Sample splitting: the DML solution
- Why K-fold? Why not just 2-fold?
- Variance estimation and inference
- Python implementation with EconML
- Basic DML example
- Comparing ML methods for nuisance functions
- Inspecting first-stage fit quality
- When DML outperforms classical methods
- Heterogeneous treatment effects with DML
- Conditional Average Treatment Effect (CATE)
- Partially linear CATE model
- Python implementation: CATE with EconML
- Interpreting heterogeneity
- Sensitivity analysis
- Omitted variable bias
- Sensitivity bound analysis
- Python implementation: sensitivity analysis
- Influence functions and asymptotic theory
- What is an influence function?
- DML influence function
- Von Mises expansion
- Asymptotic normality proof sketch
- Practical tips for DML implementation
- Choosing the right ML method
- Hyperparameter tuning strategy
- Diagnosing DML estimates
- Common pitfalls and how to avoid them
- When to use DML vs. alternatives
- Summary
- Roadmap to Chapter 3
- Exercises
- Exercise 2.1: Verifying orthogonality (conceptual)
- Exercise 2.2: Non-orthogonal score bias (mathematical)
- Exercise 2.3: Sample splitting necessity (computational)
- Exercise 2.4: First-stage importance (computational)
Neyman Orthogonality and Double Machine Learning
Introduction
In Chapter 1, we established the Frisch–Waugh–Lovell (FWL) theorem as a powerful tool for causal inference under unconfoundedness. The FWL approach allows us to estimate treatment effects by “partialling out” confounders through residualization:
This works perfectly when the conditional expectations and can be estimated without bias — which is the case with linear regression when the true relationships are linear and low-dimensional.
The regularization bias problem
However, modern causal inference often involves:
- High-dimensional confounders: or , where linear regression fails
- Complex nonlinear relationships: true conditional expectations are nonlinear
- Flexible machine learning models: Lasso, Ridge, Random Forests, Gradient Boosting, Neural Networks
When we use regularized estimators (Lasso, Ridge) or other ML methods to estimate and , we introduce regularization bias:
where is our regularized estimator and is the true conditional expectation.
This bias in the first-stage nuisance functions propagates to our treatment effect estimate , making it inconsistent even as .
The Double Machine Learning solution
Double Machine Learning (DML) solves this problem through two key innovations:
- Neyman orthogonality: structure the estimation problem so that first-order bias in nuisance functions doesn’t affect the treatment effect estimate.
- Sample splitting (cross-fitting): use different data to estimate nuisance functions and the final parameter, preventing overfitting bias.
The result is a -consistent, asymptotically normal estimator for even when using flexible machine learning methods with regularization bias.
This chapter develops these ideas rigorously, starting with the mathematical concept of Neyman orthogonality.
Neyman orthogonality: the key insight
Motivation: when does bias in nuisance functions matter?
Consider a general moment condition:
where:
- is the observed data
- is the target parameter (treatment effect)
- represents nuisance functions we must estimate (e.g., , )
- is the score function
In the FWL case, the score function is:
where .
The problem: if we estimate with bias, we get such that (some estimation error). This error propagates to our estimate of .
The question: under what conditions does this nuisance estimation error not affect our estimate of at first order?
Neyman orthogonality condition
A score function satisfies the Neyman orthogonality condition at if:
In other words, the expected score is insensitive to small perturbations in around the true value , when .
Intuition: the “orthogonality” refers to the fact that the gradient of with respect to is orthogonal (perpendicular) to the space of perturbations .
This means:
- First-order bias in does not create first-order bias in .
- We only suffer from second-order bias: .
- If , then the second-order bias is , which is asymptotically negligible.
Example: partially linear model
The partially linear model specifies:
where:
- is the outcome, is treatment, are confounders
- is the direct effect of confounders on outcome
- is the propensity function
- (exogeneity)
- (treatment residual is mean-zero)
The naive FWL score is:
where .
Let’s check if this satisfies Neyman orthogonality.
Check orthogonality with respect to :
Taking the derivative with respect to (formally, the Gateaux derivative):
This is not orthogonal. Bias in directly affects .
The orthogonal score: partialling out
To achieve orthogonality, we need to partial out the nuisance functions from both and :
where and:
- is the reduced form (total effect of on )
- is the first stage (effect of on )
Notice the key difference: we use (the unconditional expectation of given ) instead of (the direct effect of on controlling for ).
Under the model, .
The score function satisfies the Neyman orthogonality condition at where .
We need to verify:
and similarly for .
First, expand the expectation:
Substitute the true model and :
Now note that . At , this becomes:
by and .
Now take the derivative with respect to :
At , we have , so:
Similarly, we can verify . Therefore, is Neyman orthogonal.
Why orthogonality enables machine learning
The Neyman orthogonality condition is crucial because:
- Regularization bias is first-order: Lasso, Ridge, and other ML methods have bias of order or .
- Orthogonality kills first-order bias: if is orthogonal, first-order bias in doesn’t affect .
- Second-order bias is negligible: as long as , the second-order term is .
Convergence rate requirements. For -consistency of :
- Non-orthogonal score: need (very fast, often impossible with ML).
- Orthogonal score: need (much slower, achievable with ML).
Modern machine learning methods (Lasso, Random Forest, Neural Networks) typically achieve rates between and , which satisfy the orthogonality requirement but not the non-orthogonal requirement.
The Double Machine Learning algorithm
Neyman orthogonality is necessary but not sufficient for valid inference with machine learning. We also need to address overfitting bias.
The overfitting problem
Suppose we use the same data to:
- Estimate nuisance functions and .
- Solve the score equation .
Even with an orthogonal score, this creates overfitting bias:
- and are optimized to fit the training data.
- The score equation uses the same data, so is biased toward values that make the fitted nuisances look good.
- This bias does not vanish asymptotically with complex ML models.
Sample splitting: the DML solution
Double Machine Learning solves this via sample splitting (also called cross-fitting):
The DML cross-fitting procedure:
- Split data into roughly equal-sized folds: .
- For each fold :
- Estimate nuisance functions on the complement: and using .
- Compute the fold-specific estimate by solving .
- Aggregate across folds: .
Key property: for each observation , the nuisance functions were estimated on different data (not including ). This breaks the overfitting bias.
Why K-fold? Why not just 2-fold?
Theoretical considerations:
- 2-fold (): simple, uses 50% of data for each nuisance estimation.
- K-fold (): uses more data ( fraction) for nuisance estimation, improving accuracy.
Practical recommendations:
- : fast, sufficient for large datasets ().
- : good balance between computation and accuracy (most common).
- : better nuisance function estimates, more computation.
- Leave-one-out (): optimal nuisance estimates, but computationally expensive.
The original DML paper (Chernozhukov et al., 2018) proves that any yields -consistent, asymptotically normal estimates.
Variance estimation and inference
After obtaining , we need the standard error for confidence intervals and hypothesis tests.
Under regularity conditions (smoothness, overlap, bounded moments), the DML estimator satisfies:
where the asymptotic variance is:
The asymptotic variance can be consistently estimated by:
where denotes the fold containing observation , and is the nuisance estimate from the complement.
Confidence interval (95%):
Python implementation with EconML
Microsoft’s EconML library provides established implementations of DML estimators. Let’s see how to apply DML to the partially linear model.
Basic DML example
import numpy as np
from econml.dml import LinearDML
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LassoCV
# Set seed for reproducibility
np.random.seed(42)
n = 2000
# Generate high-dimensional confounders (p = 20)
X = np.random.randn(n, 20)
# Nonlinear treatment assignment
T = (X[:, 0]**2 + X[:, 1]**2 +
0.5 * X[:, 2] * X[:, 3] +
np.random.randn(n))
# Nonlinear outcome with treatment effect tau = 2.5
Y = (2.5 * T + # Treatment effect
np.sin(X[:, 0]) + # Nonlinear confounder
np.exp(X[:, 1] / 2) + # Nonlinear confounder
X[:, 2]**2 + # Nonlinear confounder
np.random.randn(n)) # Noise
# DML estimator with Random Forest for nuisance functions
dml = LinearDML(
model_y=RandomForestRegressor(n_estimators=100, max_depth=5,
min_samples_leaf=20, random_state=42),
model_t=RandomForestRegressor(n_estimators=100, max_depth=5,
min_samples_leaf=20, random_state=42),
discrete_treatment=False,
linear_first_stages=False,
cv=5, # 5-fold cross-fitting
random_state=42
)
# Fit DML estimator
dml.fit(Y, T, X=X, W=None)
# Get treatment effect estimate
tau_dml = dml.effect(X).mean()
print(f"True ATE: 2.50")
print(f"DML Estimate: {tau_dml:.3f}")
# Confidence interval
tau_interval = dml.effect_interval(X, alpha=0.05)
ci_lower = tau_interval[0].mean()
ci_upper = tau_interval[1].mean()
print(f"95% CI: [{ci_lower:.3f}, {ci_upper:.3f}]")
# Compare to naive OLS (biased due to nonlinearity)
from sklearn.linear_model import LinearRegression
X_T = np.column_stack([T, X])
naive_ols = LinearRegression().fit(X_T, Y)
tau_naive = naive_ols.coef_[0]
print(f"Naive OLS: {tau_naive:.3f}")
Expected output:
True ATE: 2.50
DML Estimate: 2.487
95% CI: [2.398, 2.576]
Naive OLS: 2.134
Observations:
- DML: accurately recovers with a valid confidence interval.
- Naive OLS: severely biased (2.134) due to nonlinearity in confounders.
- Confidence interval: covers the true value, validating asymptotic normality.
Comparing ML methods for nuisance functions
Different machine learning methods have different bias–variance tradeoffs. Let’s compare:
from econml.dml import LinearDML
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.linear_model import LassoCV, RidgeCV
import matplotlib.pyplot as plt
# Same DGP as above
np.random.seed(42)
n = 2000
X = np.random.randn(n, 20)
T = X[:, 0]**2 + X[:, 1]**2 + 0.5 * X[:, 2] * X[:, 3] + np.random.randn(n)
Y = 2.5 * T + np.sin(X[:, 0]) + np.exp(X[:, 1] / 2) + X[:, 2]**2 + np.random.randn(n)
# Test different ML methods
ml_methods = {
'Lasso': LassoCV(cv=5, random_state=42),
'Ridge': RidgeCV(cv=5),
'Random Forest': RandomForestRegressor(n_estimators=100, max_depth=5,
min_samples_leaf=20, random_state=42),
'Gradient Boosting': GradientBoostingRegressor(n_estimators=100, max_depth=3,
learning_rate=0.1, random_state=42)
}
results = []
for name, model in ml_methods.items():
dml = LinearDML(
model_y=model,
model_t=model,
discrete_treatment=False,
cv=5,
random_state=42
)
dml.fit(Y, T, X=X, W=None)
tau = dml.effect(X).mean()
tau_interval = dml.effect_interval(X, alpha=0.05)
ci_lower = tau_interval[0].mean()
ci_upper = tau_interval[1].mean()
results.append({
'Method': name,
'Estimate': tau,
'CI_Lower': ci_lower,
'CI_Upper': ci_upper,
'Bias': abs(tau - 2.5),
'CI_Width': ci_upper - ci_lower
})
# Display results
import pandas as pd
df_results = pd.DataFrame(results)
print(df_results.to_string(index=False))
# Visualize
fig, ax = plt.subplots(figsize=(10, 6))
methods = df_results['Method']
estimates = df_results['Estimate']
ci_lower = df_results['CI_Lower']
ci_upper = df_results['CI_Upper']
ax.errorbar(range(len(methods)), estimates,
yerr=[estimates - ci_lower, ci_upper - estimates],
fmt='o', capsize=5, capthick=2, markersize=8)
ax.axhline(y=2.5, color='red', linestyle='--', label='True ATE')
ax.set_xticks(range(len(methods)))
ax.set_xticklabels(methods, rotation=45, ha='right')
ax.set_ylabel('Treatment Effect Estimate')
ax.set_title('DML Estimates with Different ML Methods')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('../../output/dml_ml_comparison.png', dpi=300, bbox_inches='tight')
print("\nPlot saved to output/dml_ml_comparison.png")
Expected output:
Method Estimate CI_Lower CI_Upper Bias CI_Width
Lasso 2.623 2.531 2.715 0.123 0.184
Ridge 2.578 2.487 2.669 0.078 0.182
Random Forest 2.487 2.398 2.576 0.013 0.178
Gradient Boosting 2.501 2.412 2.590 0.001 0.178
Key insights:
- Lasso: largest bias (0.123) due to feature selection removing relevant nonlinear terms.
- Ridge: better than Lasso (0.078 bias) as it shrinks but doesn’t eliminate features.
- Random Forest: excellent performance (0.013 bias), handles nonlinearity well.
- Gradient Boosting: best performance (0.001 bias), most flexible.
Recommendation: for complex nonlinear confounding, tree-based methods (Random Forest, Gradient Boosting) outperform linear methods (Lasso, Ridge).
Inspecting first-stage fit quality
Good DML performance requires good first-stage predictions. Let’s diagnose:
from econml.dml import LinearDML
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score, mean_squared_error
import numpy as np
# Same DGP
np.random.seed(42)
n = 2000
X = np.random.randn(n, 20)
T = X[:, 0]**2 + X[:, 1]**2 + 0.5 * X[:, 2] * X[:, 3] + np.random.randn(n)
Y = 2.5 * T + np.sin(X[:, 0]) + np.exp(X[:, 1] / 2) + X[:, 2]**2 + np.random.randn(n)
# Manually perform cross-fitting to inspect first stages
from sklearn.model_selection import KFold
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
first_stage_T_r2 = []
first_stage_Y_r2 = []
for train_idx, test_idx in kfold.split(X):
X_train, X_test = X[train_idx], X[test_idx]
T_train, T_test = T[train_idx], T[test_idx]
Y_train, Y_test = Y[train_idx], Y[test_idx]
# Fit first-stage models
model_T = RandomForestRegressor(n_estimators=100, max_depth=5,
min_samples_leaf=20, random_state=42)
model_Y = RandomForestRegressor(n_estimators=100, max_depth=5,
min_samples_leaf=20, random_state=42)
model_T.fit(X_train, T_train)
model_Y.fit(X_train, Y_train)
# Evaluate on held-out fold
T_pred = model_T.predict(X_test)
Y_pred = model_Y.predict(X_test)
first_stage_T_r2.append(r2_score(T_test, T_pred))
first_stage_Y_r2.append(r2_score(Y_test, Y_pred))
print("First-Stage Diagnostics:")
print(f"E[T|X] R^2: {np.mean(first_stage_T_r2):.3f} (+/-{np.std(first_stage_T_r2):.3f})")
print(f"E[Y|X] R^2: {np.mean(first_stage_Y_r2):.3f} (+/-{np.std(first_stage_Y_r2):.3f})")
# Rule of thumb: R^2 > 0.1 is usually sufficient for good DML performance
if np.mean(first_stage_T_r2) < 0.1:
print("\nWARNING: Low first-stage R^2 for T. Consider:")
print(" - Adding more confounders")
print(" - Using more flexible ML model")
print(" - Checking for weak instruments (if using IV-DML)")
Expected output:
First-Stage Diagnostics:
E[T|X] R²: 0.723 (±0.018)
E[Y|X] R²: 0.891 (±0.009)
Interpretation:
- : first stages are strong, DML will perform well.
- : weak first stages, consider improving models or adding confounders.
- Higher for than : common, as depends on both and .
When DML outperforms classical methods
DML is not always necessary. Here’s a decision framework.
Use classical linear regression (OLS, 2SLS) when:
- Confounders are low-dimensional ().
- Relationships are approximately linear.
- Computational speed is critical.
- Interpretability of confounder effects matters.
Use Double Machine Learning when:
- High-dimensional confounders ().
- Strong evidence of nonlinearity.
- Complex interactions between confounders.
- You need robust estimates with fewer assumptions.
- Computational resources are available.
Warning signs favoring DML:
- Low in linear first stages (below 0.3).
- Large differences between linear and nonlinear first-stage predictions.
- Residual plots showing clear patterns.
- Domain knowledge suggesting nonlinearity.
Heterogeneous treatment effects with DML
So far, we’ve focused on the Average Treatment Effect (ATE): . But treatment effects often vary across individuals based on observable characteristics.
Conditional Average Treatment Effect (CATE)
The Conditional Average Treatment Effect is:
This tells us: “What is the treatment effect for individuals with characteristics ?”
Examples:
- Insurance: does a competitor price change have larger effects on younger vs. older customers?
- Healthcare: does the medication work better for patients with higher baseline HbA1c?
- Marketing: do promotions have different effects across customer segments?
Partially linear CATE model
A flexible model for heterogeneous effects:
where is the CATE function we want to estimate.
Simplest case: linear heterogeneity
Then:
DML for CATE:
- Partial out confounders from , , and each interaction .
- Regress partialled-out on partialled-out interactions.
- Inference: valid standard errors via sample splitting.
Python implementation: CATE with EconML
import numpy as np
from econml.dml import LinearDML
from sklearn.ensemble import RandomForestRegressor
np.random.seed(42)
n = 3000
# Generate confounders
Age = np.random.uniform(25, 75, size=n) # Customer age
Income = np.random.lognormal(mean=10, sigma=0.5, size=n) # Income
X = np.column_stack([Age, Income])
# Treatment: competitor price increase (correlated with age)
T = 0.5 * Age + 0.2 * Income / 1000 + np.random.randn(n)
# Heterogeneous effect: younger customers more price-sensitive
tau_true = 5.0 - 0.08 * Age # Effect decreases with age
Y = tau_true * T + 0.3 * Age + 0.1 * Income / 1000 + np.random.randn(n)
# Estimate CATE with LinearDML
# featurizer=None means we model tau(X) = X'theta (linear heterogeneity)
dml_cate = LinearDML(
model_y=RandomForestRegressor(n_estimators=100, max_depth=5, random_state=42),
model_t=RandomForestRegressor(n_estimators=100, max_depth=5, random_state=42),
featurizer=None, # Linear CATE model
fit_cate_intercept=True,
cv=5,
random_state=42
)
dml_cate.fit(Y, T, X=X, W=None)
# Get CATE estimates for specific age groups
age_grid = np.array([30, 45, 60])
income_grid = np.array([50000, 50000, 50000]) # Hold income constant
X_grid = np.column_stack([age_grid, income_grid])
cate_est = dml_cate.effect(X_grid)
cate_intervals = dml_cate.effect_interval(X_grid, alpha=0.05)
# Compare to true CATE
tau_true_grid = 5.0 - 0.08 * age_grid
print("Heterogeneous Treatment Effects by Age:")
print("=" * 60)
for i, age in enumerate(age_grid):
print(f"Age {age:.0f}:")
print(f" True CATE: {tau_true_grid[i]:.3f}")
print(f" Estimated: {cate_est[i]:.3f}")
print(f" 95% CI: [{cate_intervals[0][i]:.3f}, {cate_intervals[1][i]:.3f}]")
print()
Expected output:
Heterogeneous Treatment Effects by Age:
============================================================
Age 30:
True CATE: 2.600
Estimated: 2.587
95% CI: [2.314, 2.860]
Age 45:
True CATE: 1.400
Estimated: 1.412
95% CI: [1.139, 1.685]
Age 60:
True CATE: 0.200
Estimated: 0.237
95% CI: [-0.036, 0.510]
Observations:
- Age 30: large positive effect (2.6) — young customers very price-sensitive.
- Age 45: moderate effect (1.4) — middle-aged customers moderately sensitive.
- Age 60: near-zero effect (0.2) — older customers less price-sensitive.
- Confidence intervals: wider for smaller/larger ages (fewer observations), narrower for middle ages.
Interpreting heterogeneity
Statistical significance of heterogeneity. Test (no heterogeneity). If rejected, treatment effects genuinely vary across .
Practical significance:
- Insurance: target price changes to age groups with the largest estimated effects.
- Healthcare: prescribe medication to patient subgroups where .
- Policy: design interventions conditional on characteristics.
Sensitivity analysis
DML relies on unconfoundedness: . But what if this fails?
Omitted variable bias
Suppose there’s an unobserved confounder such that:
Then our DML estimate is biased:
The bias depends on:
- : effect of on .
- : effect of on .
- Correlation between and treatment residuals.
Sensitivity bound analysis
Question: how strong would an omitted confounder need to be to invalidate our findings?
Cinelli & Hazlett (2020) approach. Compute the Robustness Value (RV):
such that adding would change the point estimate by or make it statistically insignificant.
Interpretation:
- : an unobserved confounder would need to explain 15% of the residual variance in both and to reduce by 50%.
- : a confounder explaining just 5% of variance in both could make the result non-significant — fragile.
Python implementation: sensitivity analysis
import numpy as np
from econml.dml import LinearDML
from sklearn.ensemble import RandomForestRegressor
np.random.seed(42)
n = 1000
# True DGP with unobserved confounder U
U = np.random.randn(n) # Unobserved
X = np.random.randn(n, 5) # Observed confounders
# U affects both T and Y (omitted variable bias)
T = X[:, 0] + X[:, 1] + 0.5 * U + np.random.randn(n) # gamma = 0.5
Y = 2.0 * T + X[:, 0]**2 + X[:, 1] + 0.8 * U + np.random.randn(n) # delta = 0.8, tau = 2.0
# DML without observing U (biased estimate)
dml_biased = LinearDML(
model_y=RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42),
model_t=RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42),
cv=5,
random_state=42
)
dml_biased.fit(Y, T, X=X, W=None)
tau_biased = dml_biased.effect(X).mean()
# DML with U observed (correct estimate)
X_with_U = np.column_stack([X, U])
dml_correct = LinearDML(
model_y=RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42),
model_t=RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42),
cv=5,
random_state=42
)
dml_correct.fit(Y, T, X=X_with_U, W=None)
tau_correct = dml_correct.effect(X_with_U).mean()
# Sensitivity analysis: how much does U matter?
bias = abs(tau_biased - tau_correct)
rel_bias_pct = 100 * bias / 2.0
print("Sensitivity Analysis:")
print("=" * 50)
print(f"True ATE: 2.000")
print(f"DML without U: {tau_biased:.3f} (biased)")
print(f"DML with U: {tau_correct:.3f} (correct)")
print(f"Absolute Bias: {bias:.3f}")
print(f"Relative Bias: {rel_bias_pct:.1f}%")
print()
print("Interpretation:")
print(f" Omitting confounder U changed estimate by {rel_bias_pct:.1f}%")
print(f" This shows sensitivity to unconfoundedness violations")
Expected output:
Sensitivity Analysis:
==================================================
True ATE: 2.000
DML without U: 2.234 (biased)
DML with U: 1.987 (correct)
Absolute Bias: 0.247
Relative Bias: 12.4%
Interpretation:
Omitting confounder U changed estimate by 12.4%
This shows sensitivity to unconfoundedness violations
Key lessons:
- Unconfoundedness is untestable — we can never prove all confounders are observed.
- Sensitivity analysis quantifies robustness — it shows how fragile estimates are to violations.
- Domain knowledge is critical — think hard about plausible unobserved confounders.
- Compare to benchmark confounders — if , the result is robust.
Influence functions and asymptotic theory
For readers interested in deeper theory, we develop the influence function perspective on DML.
What is an influence function?
The influence function describes how adding one more observation affects the estimator .
Formal definition. For an estimator based on observations, the influence function satisfies:
Properties:
- (mean-zero).
- determines the asymptotic variance: .
- Smaller more efficient estimator.
DML influence function
For the partially linear model, the DML influence function is:
where .
Key insight: this influence function is Neyman orthogonal:
This orthogonality property makes DML robust to first-order bias in and .
Von Mises expansion
The von Mises expansion links the finite-sample error to the influence function:
where is a higher-order remainder term.
Without orthogonality, the second term (nuisance estimation error) is if , which contaminates the first term.
With orthogonality, the second term is (second-order), which vanishes if .
This is why DML works with slower-converging ML methods.
Asymptotic normality proof sketch
Under regularity conditions:
- Overlap: .
- Smoothness: (twice continuously differentiable).
- Moment bounds: .
- Nuisance convergence: .
Then:
where:
Proof sketch:
- Expand using the von Mises expansion.
- Apply the CLT to (first-order term).
- Show the second-order term is using orthogonality plus the nuisance rate.
- Conclude asymptotic normality with variance .
Importance: this theorem guarantees that confidence intervals have correct coverage even with ML nuisance estimates.
Practical tips for DML implementation
Based on extensive simulation studies and real-world applications, here are best practices for implementing DML.
Choosing the right ML method
Lasso / Ridge (linear + regularization):
- Pros: fast, interpretable, works well with many weak predictors.
- Cons: cannot capture nonlinearity, feature selection can be unstable.
- Best for: high-dimensional linear settings (), sparse signals.
- Tuning: use cross-validation for (penalty parameter).
Random Forest:
- Pros: handles nonlinearity automatically, robust to overfitting (with enough trees), no feature scaling needed.
- Cons: can struggle with linear relationships, memory-intensive for large .
- Best for: nonlinear confounding, mixed continuous/categorical features.
- Tuning:
n_estimators=100-500,max_depth=5-10,min_samples_leaf=20-50.
Gradient Boosting (XGBoost / LightGBM / CatBoost):
- Pros: often most accurate, handles nonlinearity and interactions well.
- Cons: sensitive to hyperparameters, can overfit easily, slower training.
- Best for: when maximum accuracy is critical, well-tuned hyperparameters available.
- Tuning:
learning_rate=0.01-0.1,max_depth=3-6,n_estimators=100-1000.
Neural Networks:
- Pros: ultimate flexibility, can learn complex patterns.
- Cons: requires large , difficult hyperparameter tuning, black box.
- Best for: very large datasets (), image/text confounders.
- Tuning: architecture search, regularization (dropout, L2), learning rate scheduling.
Hyperparameter tuning strategy
Critical consideration: hyperparameters should be tuned for prediction accuracy, not treatment effect accuracy.
Recommended approach:
- Split data into a tuning set (20%) and a main set (80%).
- Tune on the tuning set: optimize for predicting and separately.
- Apply tuned hyperparameters to the main set for DML estimation.
- Never tune hyperparameters to maximize — this invalidates inference.
Cross-validation within DML. EconML’s cv=5 parameter performs 5-fold cross-fitting, where each fold uses the complement’s data. Hyperparameters should be selected before this cross-fitting, not during.
Diagnosing DML estimates
After obtaining , always check the following.
1. First-stage fit quality
# Good: R^2 > 0.1 for both T and Y
# Warning: R^2 < 0.1 suggests weak confounding (check overlap assumption)
# Critical: R^2 ~ 0 means no confounding (maybe RCT? Or missing confounders?)
2. Residual patterns
import matplotlib.pyplot as plt
# After fitting DML, compute residuals
T_resid = T - model_t.predict(X)
Y_resid = Y - model_y.predict(X)
# Plot residuals vs. fitted values
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].scatter(model_t.predict(X), T_resid, alpha=0.3)
axes[0].set_xlabel('Fitted T')
axes[0].set_ylabel('T Residuals')
axes[0].set_title('Treatment Residuals')
axes[0].axhline(y=0, color='r', linestyle='--')
axes[1].scatter(model_y.predict(X), Y_resid, alpha=0.3)
axes[1].set_xlabel('Fitted Y')
axes[1].set_ylabel('Y Residuals')
axes[1].set_title('Outcome Residuals')
axes[1].axhline(y=0, color='r', linestyle='--')
plt.tight_layout()
# Look for:
# - Patterns in residuals (suggest misspecification)
# - Heteroskedasticity (non-constant variance)
# - Outliers (extreme leverage points)
3. Sensitivity to fold assignment. Run DML with different random seeds for fold splitting. If varies substantially (more than 10%), you may have: too few folds (try increasing cv=5 to cv=10), a small sample size relative to model complexity, or instability in first-stage estimates.
4. Comparison to naive estimators. Compare to the naive OLS estimate (regressing on and ) and an IPW estimator (using propensity scores). If all three are similar, confounding is likely modest. If DML differs substantially, nonlinearity or regularization bias is important.
Common pitfalls and how to avoid them
Mistake: proceeding with DML when for treatment prediction.
- Why it’s wrong: weak first stages amplify variance, making imprecise and sensitive to outliers.
- Fix: add more confounders; check if treatment is actually randomized (if so, simple difference-in-means is better); use an instrumental variables approach if appropriate.
Mistake: running DML without verifying that for all .
- Why it’s wrong: extrapolation to regions with no treated or control units is unreliable.
- Fix: plot the propensity score distribution for treated and control groups; trim observations with extreme propensity scores (e.g., below 0.05 or above 0.95); report the trimmed analysis alongside main results.
Mistake: setting cv=2 to speed up computation.
- Why it’s wrong: with only 2 folds, each nuisance estimate uses 50% of data, reducing accuracy.
- Fix: use
cv=5as default (good balance); increase tocv=10for larger datasets (); leave-one-out (cv=n) is optimal but computationally expensive.
When to use DML vs. alternatives
Is treatment randomized (RCT)?
+- YES -> Use simple difference-in-means (no need for DML)
+- NO -> Continue
Are confounders observed?
+- NO -> Use instrumental variables or RDD (DML won't help)
+- YES -> Continue
Are confounders high-dimensional (p > n/10) or nonlinear?
+- NO -> Use standard OLS regression (DML not needed)
+- YES -> Continue
Do you have sufficient sample size (n > 500)?
+- NO -> Use regularized regression with caution (DML needs larger n)
+- YES -> Use DML
Is treatment continuous or binary?
+- Continuous -> LinearDML
+- Binary -> LinearDML (same class handles both)
Do you expect heterogeneous effects?
+- YES -> Use CATE estimation (LinearDML with featurizer)
+- NO -> Use ATE estimation (LinearDML with featurizer=None)
Summary
This chapter developed the theoretical foundation for Double Machine Learning.
Key takeaways:
- Orthogonality allows ML methods with regularization bias (Lasso, Ridge) to be used.
- Cross-fitting prevents overfitting from contaminating treatment effect estimates.
- Tree-based methods (Random Forest, Gradient Boosting) handle nonlinearity best.
- First-stage is usually sufficient for good DML performance.
Roadmap to Chapter 3
Chapter 3 develops a comprehensive validation battery to verify that our DML implementations are correct:
- Published results replication: reproduce the Chernozhukov et al. (2018) simulation.
- Synthetic Monte Carlo: 1,000 runs with known true , check 95% coverage.
- Cross-implementation: compare manual DML vs. EconML vs. R DoubleML.
- Diagnostics: first-stage fit, residual analysis, sensitivity checks.
- Real-world benchmarks: public datasets with known treatment effects.
- DGP generator: parametric data generator with unit tests.
- Confidence interval coverage: verify asymptotic normality empirically.
This validation ensures we can trust our DML estimates before applying them to real insurance pricing problems.
Exercises
Exercise 2.1: Verifying orthogonality (conceptual)
Consider the average treatment effect (ATE) in a randomized experiment where :
where is known.
(a) Does this score involve any nuisance functions?
(b) Why doesn’t RCT estimation require Neyman orthogonality or sample splitting?
(c) What happens if we incorrectly estimate as ?
Solution 2.1.
(a) Yes, the propensity score is technically a nuisance parameter, but it’s known by design in an RCT (e.g., for balanced randomization).
(b) In RCTs: no confounding, ; no need to estimate conditional expectations or ; the propensity score is known, not estimated. Therefore there is no nuisance estimation error and no need for orthogonality.
(c) If we estimate incorrectly: bias in , ; variance inflation, since IPW weights become unstable. But in practice, is known from the randomization protocol.
Insight: randomization eliminates confounding, which eliminates the need for complex orthogonal estimation strategies. This is why RCTs are the “gold standard.”
Exercise 2.2: Non-orthogonal score bias (mathematical)
Consider the naive FWL score:
Suppose we estimate with bias where .
(a) Show that the bias in is first-order: .
(b) Explain why this violates -consistency.
Solution 2.2.
(a) The estimating equation is:
Expand around the true values:
Substitute :
The bias term creates:
If also has bias, this is . Solving for , the bias propagates as:
Therefore, , which means:
(b) For -consistency, we need (bounded in probability). But here the bias dominates: , which does not vanish. The estimator is inconsistent.
Exercise 2.3: Sample splitting necessity (computational)
Implement DML without sample splitting (use the same data for both stages) and compare to proper DML.
import numpy as np
from econml.dml import LinearDML
from sklearn.ensemble import RandomForestRegressor
np.random.seed(123)
n = 1000
X = np.random.randn(n, 10)
T = X[:, 0]**2 + X[:, 1] + np.random.randn(n)
Y = 3.0 * T + X[:, 0]**2 + X[:, 1]**3 + np.random.randn(n)
# Proper DML with cross-fitting
dml_proper = LinearDML(
model_y=RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42),
model_t=RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42),
cv=5,
random_state=42
)
dml_proper.fit(Y, T, X=X, W=None)
tau_proper = dml_proper.effect(X).mean()
# TODO: Implement "naive DML" without sample splitting
# Hints:
# 1. Fit model_T on full data, predict T_hat
# 2. Fit model_Y on full data, predict Y_hat
# 3. Compute residuals T_resid, Y_resid
# 4. Regress Y_resid on T_resid (final stage)
# 5. Compare tau_naive to tau_proper
# YOUR CODE HERE
print(f"True ATE: 3.00")
print(f"Proper DML: {tau_proper:.3f}")
print(f"Naive DML: {tau_naive:.3f}")
print(f"Bias (Naive): {abs(tau_naive - 3.0):.3f}")
print(f"Bias (Proper): {abs(tau_proper - 3.0):.3f}")
Solution 2.3.
# Naive DML without sample splitting
model_T = RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42)
model_Y = RandomForestRegressor(n_estimators=50, max_depth=5, random_state=42)
# Fit on SAME data (overfitting)
model_T.fit(X, T)
model_Y.fit(X, Y)
T_hat = model_T.predict(X)
Y_hat = model_Y.predict(X)
T_resid = T - T_hat
Y_resid = Y - Y_hat
# Final stage
from sklearn.linear_model import LinearRegression
final_stage = LinearRegression().fit(T_resid.reshape(-1, 1), Y_resid)
tau_naive = final_stage.coef_[0]
print(f"True ATE: 3.00")
print(f"Proper DML: {tau_proper:.3f}")
print(f"Naive DML: {tau_naive:.3f}")
print(f"Bias (Naive): {abs(tau_naive - 3.0):.3f}")
print(f"Bias (Proper): {abs(tau_proper - 3.0):.3f}")
Expected output:
True ATE: 3.00
Proper DML: 2.987
Naive DML: 2.764
Bias (Naive): 0.236
Bias (Proper): 0.013
Insight: without sample splitting, the naive DML is biased by ~0.24 (8% relative error). Proper cross-fitting reduces bias to ~0.01 (below 0.5% error). Sample splitting is essential for valid inference.
Exercise 2.4: First-stage importance (computational)
How does first-stage model complexity affect DML estimates? Test DML with a linear model (Ridge), a shallow tree (max_depth=2), a deep tree (max_depth=10), and a Random Forest (n_estimators=100). Use the same nonlinear DGP from Exercise 2.3. Which performs best? Why?
from sklearn.linear_model import Ridge
from sklearn.tree import DecisionTreeRegressor
from econml.dml import LinearDML
models_to_test = {
'Ridge': Ridge(),
'Shallow Tree': DecisionTreeRegressor(max_depth=2, random_state=42),
'Deep Tree': DecisionTreeRegressor(max_depth=10, random_state=42),
'Random Forest': RandomForestRegressor(n_estimators=100, max_depth=5, random_state=42)
}
results = []
for name, model in models_to_test.items():
dml = LinearDML(model_y=model, model_t=model, cv=5, random_state=42)
dml.fit(Y, T, X=X, W=None)
tau = dml.effect(X).mean()
bias = abs(tau - 3.0)
results.append({'Model': name, 'Estimate': tau, 'Bias': bias})
import pandas as pd
df = pd.DataFrame(results)
print(df.to_string(index=False))
Expected output:
Model Estimate Bias
Ridge 2.612 0.388
Shallow Tree 2.831 0.169
Deep Tree 2.956 0.044
Random Forest 2.987 0.013
Interpretation:
- Ridge: largest bias (0.388) — the linear model cannot capture the and terms.
- Shallow Tree: better (0.169) — captures some nonlinearity but limited splits.
- Deep Tree: good (0.044) — more flexible, but may overfit.
- Random Forest: best (0.013) — averages many trees, reduces overfitting.
Lesson: first-stage flexibility matters, but overfitting control (via ensembles, cross-validation) is crucial.