import numpy as np
import pandas as pd
from causalml.dataset import make_uplift_classification
from causalml.inference.tree import UpliftRandomForestClassifier
from causalml.metrics import plot_gain
from sklearn.model_selection import train_test_split
import importlib
print(importlib.metadata.version('causalml') )
df, x_names = make_uplift_classification()
df.head()
# Look at the conversion rate and sample size in each group
df.pivot_table(values='conversion',
index='treatment_group_key',
aggfunc=[np.mean, np.size],
margins=True)
# Split data to training and testing samples for model validation (next section)
df_train, df_test = train_test_split(df, test_size=0.2, random_state=111)
from causalml.inference.tree import UpliftTreeClassifier
clf = UpliftTreeClassifier(control_name='control')
clf.fit(df_train[x_names].values,
treatment=df_train['treatment_group_key'].values,
y=df_train['conversion'].values)
p = clf.predict(df_test[x_names].values)
df_res = pd.DataFrame(p, columns=clf.classes_)
df_res.head()
uplift_model = UpliftRandomForestClassifier(control_name='control')
uplift_model.fit(df_train[x_names].values,
treatment=df_train['treatment_group_key'].values,
y=df_train['conversion'].values)
df_res = uplift_model.predict(df_test[x_names].values, full_output=True)
print(df_res.shape)
df_res.head()
y_pred = uplift_model.predict(df_test[x_names].values)
# Put the predictions to a DataFrame for a neater presentation
# The output of `predict()` is a numpy array with the shape of [n_sample, n_treatment] excluding the
# predictions for the control group.
result = pd.DataFrame(y_pred,
columns=uplift_model.classes_[1:])
result.head()
# If all deltas are negative, assign to control; otherwise assign to the treatment
# with the highest delta
best_treatment = np.where((result < 0).all(axis=1),
'control',
result.idxmax(axis=1))
# Create indicator variables for whether a unit happened to have the
# recommended treatment or was in the control group
actual_is_best = np.where(df_test['treatment_group_key'] == best_treatment, 1, 0)
actual_is_control = np.where(df_test['treatment_group_key'] == 'control', 1, 0)
synthetic = (actual_is_best == 1) | (actual_is_control == 1)
synth = result[synthetic]
auuc_metrics = (synth.assign(is_treated = 1 - actual_is_control[synthetic],
conversion = df_test.loc[synthetic, 'conversion'].values,
uplift_tree = synth.max(axis=1))
.drop(columns=list(uplift_model.classes_[1:])))
plot_gain(auuc_metrics, outcome_col='conversion', treatment_col='is_treated')46 Uplift Modeling and CATE
Uplift modeling is about estimating who changes because of treatment. In a marketing example, this means separating people who buy because of the campaign from people who would have bought anyway.
This is the same object as the conditional average treatment effect (CATE), \[\tau(x) = E[Y(1) - Y(0) \mid X = x].\] The word “uplift” is more common in industry. The word “CATE” is more common in the causal inference literature.
46.1 Python: causalml uplift trees
The causalml library (Uber) provides uplift tree classifiers and the cumulative gain curve for evaluating targeting policies. The code below follows the library’s tutorial with synthetic data.
I am copying this code from https://github.com/uber/causalml/blob/master/docs/examples/uplift_trees_with_synthetic_data.ipynb.
A caution on this evaluation. The gain curve above keeps only the units whose observed treatment happened to match the recommended treatment (or who were controls), and then compares outcomes. This is a common tutorial shortcut, but it estimates the value of the recommended policy only if treatment was randomly assigned (so that “units who happened to get the recommended arm” are exchangeable with those who did not). With observational treatment assignment, this selection is itself confounded, and a valid off-policy evaluation needs an inverse-probability / policy-value estimator that reweights by the propensity of the observed treatment. Read the gain curve here as a diagnostic under randomized assignment, not as a general off-policy evaluation.
46.2 R: causal forest for CATE
In R, the grf package implements causal forests. The setup is simpler than the uplift tree example: provide outcomes, treatments, and covariates, and the forest estimates CATEs using honest splitting.
library(grf)
library(ggplot2)
# Simulate a marketing intervention
# True CATE: treatment helps high-engagement users more
set.seed(123)
n <- 5000
X <- matrix(rnorm(n * 5), n, 5)
colnames(X) <- paste0("x", 1:5)
W <- rbinom(n, 1, 0.5) # random treatment
tau_true <- 0.3 + 0.5 * X[, 1] # CATE depends on x1
Y <- tau_true * W + X[, 2] + rnorm(n, sd = 0.5) # observed outcome
# Fit causal forest
cf <- causal_forest(X, Y, W, num.trees = 2000, seed = 42)
# Estimate CATE for each unit
tau_hat <- predict(cf, estimate.variance = TRUE)
cate <- tau_hat$predictions
cate_se <- sqrt(tau_hat$variance.estimates)
cat("Mean estimated CATE:", round(mean(cate), 3), "\n")
cat("True mean CATE: ", round(mean(tau_true), 3), "\n")# Who benefits? Targeting policy
# Sort by predicted CATE, target top 30%
threshold <- quantile(cate, 0.70)
targeted <- cate >= threshold
cat("Average CATE in targeted group:", round(mean(cate[targeted]), 3), "\n")
cat("Average CATE in non-targeted: ", round(mean(cate[!targeted]), 3), "\n")
# Variable importance: which covariates drive heterogeneity?
imp <- variable_importance(cf)
barplot(imp, names.arg = colnames(X),
main = "Variable importance for CATE",
ylab = "Importance")# Omnibus test: is there significant CATE heterogeneity?
test_calibration(cf)The test_calibration() output is a useful diagnostic. It checks whether units with higher predicted CATEs also appear to have higher treatment effects.
46.3 Uplift vs CATE: the same thing with different vocabulary
| Uplift term | Causal inference term |
|---|---|
| Uplift score | \(\hat\tau(x)\) — estimated CATE |
| Persuadables | Units with \(\tau(x) > 0\) |
| Do-not-disturbs | Units with \(\tau(x) < 0\) |
| Cumulative gain curve | Policy value curve / AUTOC |
| Uplift tree | Causal tree / honest tree |
| Uplift forest | Causal forest (grf) |
The Python causalml package comes from the marketing/tech vocabulary. grf comes from the econometrics vocabulary. They are trying to estimate the same quantity. For publication, I would usually want the inference and diagnostics from grf. For a targeting exercise, the gain-curve tools in causalml are also useful.