From effects to decisions
A policy π(x) says whom to treat. Its value is the average outcome if everyone followed it. With a budget that covers only 30% of applicants and a cost per stipend, the question becomes: which 30%?
The guiding rule is simple: treat whoever has the largest expected net benefit, τ(x) − cost, until the budget runs out. The hard part is evaluating a rule honestly, on data not used to build it.
!pip install -q econml lightgbm scikit-learn matplotlib seaborn
import numpy as np, pandas as pd
from econml.dml import CausalForestDML
from econml.policy import DRPolicyTree
from lightgbm import LGBMRegressor, LGBMClassifier
from sklearn.model_selection import KFold
import matplotlib.pyplot as plt
rng = np.random.default_rng(12)
pd.set_option("display.width", 140)
Data, nuisance models and doubly robust scores
def make_data(n):
X = pd.DataFrame({
"months_unemployed": rng.gamma(2, 4, n),
"years_education": rng.integers(8, 21, n).astype(float),
"age": rng.uniform(20, 60, n),
"prior_earnings": rng.lognormal(10.3, 0.4, n) / 1_000,
})
tau = 500 + 250 * np.minimum(X.months_unemployed, 24) - 150 * (X.years_education - 12)
p = 1 / (1 + np.exp(-(-0.5 + 0.05 * X.months_unemployed - 0.03 * (X.age - 40))))
d = rng.binomial(1, p)
y = 15_000 + 400 * X.prior_earnings + 300 * (X.years_education - 12) - 120 * X.months_unemployed \
+ d * tau + rng.normal(0, 3_000, n)
return X, d, y.to_numpy(), tau.to_numpy()
X_train, d_train, y_train, _ = make_data(8_000) # used to learn the policy
X_eval, d_eval, y_eval, tau_eval = make_data(4_000) # used only to evaluate it
COST = 2_000 # cost of one stipend
To evaluate any policy on the evaluation sample, we need a credible estimate of each person's effect that does not depend on the policy model: the doubly robust (AIPW) score from Module 7.
def dr_scores(X, d, y, k=5):
mu1, mu0, e = np.zeros(len(y)), np.zeros(len(y)), np.zeros(len(y))
for tr, te in KFold(k, shuffle=True, random_state=0).split(X):
Xtr, dtr, ytr = X.iloc[tr], d[tr], y[tr]
mu1[te] = LGBMRegressor(n_estimators=300, verbose=-1).fit(Xtr[dtr == 1], ytr[dtr == 1]).predict(X.iloc[te])
mu0[te] = LGBMRegressor(n_estimators=300, verbose=-1).fit(Xtr[dtr == 0], ytr[dtr == 0]).predict(X.iloc[te])
e[te] = LGBMClassifier(n_estimators=200, verbose=-1).fit(Xtr, dtr).predict_proba(X.iloc[te])[:, 1]
e = e.clip(0.02, 0.98)
return mu1 - mu0 + d * (y - mu1) / e - (1 - d) * (y - mu0) / (1 - e)
gamma = dr_scores(X_eval, d_eval, y_eval)
print(f"DR estimate of ATE on the evaluation sample: {gamma.mean():,.0f} (true {tau_eval.mean():,.0f})")
The value (net gain per applicant, relative to treating nobody) of any rule π is estimated by mean(π(x) · (γ − cost)).
Budget-constrained targeting
cf = CausalForestDML(model_y=LGBMRegressor(n_estimators=300, verbose=-1),
model_t=LGBMClassifier(n_estimators=300, verbose=-1),
discrete_treatment=True, n_estimators=800, min_samples_leaf=20, random_state=0)
cf.fit(y_train, d_train, X=X_train)
cate_eval = cf.effect(X_eval)
def value(policy): # net dollars per applicant vs treating nobody
return np.mean(policy * (gamma - COST))
budget = 0.30
rules = {
"Treat nobody": np.zeros(len(X_eval)),
"Treat everyone": np.ones(len(X_eval)),
"Random 30%": rng.binomial(1, budget, len(X_eval)),
"Longest unemployed 30%": (X_eval.months_unemployed >= X_eval.months_unemployed.quantile(1 - budget)).astype(int).to_numpy(),
"Causal forest top 30%": (cate_eval >= np.quantile(cate_eval, 1 - budget)).astype(int),
"Oracle top 30%": (tau_eval >= np.quantile(tau_eval, 1 - budget)).astype(int),
}
report = pd.DataFrame({
"share treated": {k: v.mean() for k, v in rules.items()},
"DR value ($/applicant)": {k: value(v) for k, v in rules.items()},
"true value ($/applicant)": {k: np.mean(v * (tau_eval - COST)) for k, v in rules.items()},
}).round(2)
print(report)
"Treat everyone" may even lose money if many people's effect is below the cost. Targeting with the causal forest captures most of what a perfectly informed (oracle) planner would.
An interpretable policy tree
Ministries rarely deploy a black-box forest. A policy tree searches directly for a shallow decision
tree that maximises the doubly robust value: "Offer the stipend if unemployed more than m months and
education below e years." EconML's DRPolicyTree does this.
tree = DRPolicyTree(
max_depth=2, min_samples_leaf=200, random_state=0,
model_regression=LGBMRegressor(n_estimators=300, verbose=-1),
model_propensity=LGBMClassifier(n_estimators=200, verbose=-1),
)
# Encode the cost: treatment 'pays off' only if the effect exceeds COST
tree.fit(y_train - COST * d_train, d_train, X=X_train)
tree_policy = tree.predict(X_eval)
print(f"Policy tree treats {tree_policy.mean():.0%} | DR value {value(tree_policy):,.0f} $/applicant "
f"| true value {np.mean(tree_policy * (tau_eval - COST)):,.0f}")
tree.plot(feature_names=list(X_train.columns), treatment_names=["No stipend", "Stipend"])
plt.show()
Targeting is where causal inference turns into decision intelligence: the deliverable is not a coefficient but a rule, its expected dollar value and the evidence that it beats simple alternatives. In the capstone you will serve such a rule through an API (Module 15).
Exercise
Plot the DR value of "causal forest top q%" for budgets q from 5% to 100%. Where does the value peak, and how does that relate to the share of people whose effect exceeds the cost?
Show a solution
qs = np.linspace(0.05, 1.0, 20)
vals = [value((cate_eval >= np.quantile(cate_eval, 1 - q)).astype(int)) for q in qs]
plt.plot(qs, vals, marker="o"); plt.xlabel("Share treated"); plt.ylabel("DR value ($/applicant)")
plt.title("Value of targeting by budget"); plt.show()
print(f"Share with true effect above cost: {np.mean(tau_eval > COST):.2f}")
Checklist and recap
- I can turn CATEs into a budget-constrained rule.
- I evaluate rules with doubly robust scores on held-out data.
- I can fit and explain a shallow policy tree.