This vignette demonstrates how to use causaldef for safe policy learning — making treatment decisions with quantified guarantees even when unobserved confounding exists.
The key insight is the policy regret transfer bound:
\[\text{Regret}_{do}(\pi) \leq \text{Regret}_{sim}(\pi) + 2|\mathcal{A}|\, M \cdot \delta\]
where: - \(\text{Regret}_{do}(\pi)\) = regret under the true interventional distribution - \(\text{Regret}_{sim}(\pi)\) = regret computed under the adjusted (simulated) interventional laws - \(|\mathcal{A}|\) = number of actions (2 for a binary treatment; the factor drops to 1 when comparing fixed actions) - \(M\) = utility range (max - min possible outcomes) - \(\delta\) = deficiency (quantifies confounding)
policy_regret_bound() reports two complementary quantities:
(Versions up to 0.2.1 used \(M\delta\) and \((M/2)\delta\); both constants were corrected in 0.3.0.)
If \(\delta>0\), no algorithm can guarantee zero worst-case regret without stronger assumptions or randomized data.
library(causaldef)
set.seed(123)
# Simulate a treatment decision problem
n <- 1000
# Covariates
age <- runif(n, 30, 70)
severity <- rbeta(n, 2, 5) * 10
# Confounded treatment assignment (sicker patients get treatment)
U <- rnorm(n) # Unmeasured health status
ps_true <- plogis(-1 + 0.02 * age + 0.1 * severity + 0.5 * U)
A <- rbinom(n, 1, ps_true)
# Outcome: recovery score (0-100)
# True effect is heterogeneous
tau_true <- 10 + 0.2 * (age - 50) # Older patients benefit more
Y <- 50 + tau_true * A - 0.3 * severity + 5 * U + rnorm(n, sd = 5)
# Clip to valid range
Y <- pmin(100, pmax(0, Y))
df <- data.frame(
age = age,
severity = severity,
A = A,
Y = Y
)What \(\delta\) is here.
estimate_deficiency()returns a propensity-score total-variation proxy, not an identified estimate of the exact deficiency. Bounds below that are stated in terms of \(\delta\) inherit that: they are only as good as the proxy standing in for it.
spec <- causal_spec(
data = df,
treatment = "A",
outcome = "Y",
covariates = c("age", "severity")
)
#> ✔ Created causal specification: n=1000, 2 covariate(s)
# Estimate deficiency with multiple methods
def_results <- estimate_deficiency(
spec,
methods = c("unadjusted", "iptw", "aipw"),
n_boot = 100
)
#> ℹ Estimating deficiency: unadjusted
#> ℹ Estimating deficiency: iptw
#> ℹ Estimating deficiency: aipw
print(def_results)
#>
#> -- Deficiency Proxy Estimates (PS-TV) ------
#>
#> Method Delta SE CI Quality
#> unadjusted 0.054 0.0139 [0.0454, 0.0965] Caution (Yellow)
#> iptw 0.011 0.0042 [0.0075, 0.023] Excellent (Green)
#> aipw 0.011 0.0042 [0.0073, 0.0223] Excellent (Green)
#> Note: delta is a propensity-score TV proxy (overlap/balance diagnostic).
#>
#> Best method: iptw (delta = 0.011 )# Define utility range (outcome is 0-100)
utility_range <- c(0, 100)
# Suppose our policy achieves 5% observed regret
obs_regret <- 5
# Compute bound
bounds <- policy_regret_bound(
deficiency = def_results,
utility_range = utility_range,
obs_regret = obs_regret
)
#> Warning: Multiple fitted methods are available but `method` was not specified.
#> ℹ Using the smallest available delta across methods is optimistic after model
#> selection.
#> ℹ For a pre-specified decision bound, call `policy_regret_bound()` with `method
#> = '<chosen method>'`.
#> ℹ Transfer penalty: 4.3892 (delta = 0.011)
print(bounds)
#>
#> -- Policy Regret Bounds -------------------------------------------------
#>
#> * Deficiency delta: 0.011
#> * Delta mode: point
#> * Delta method: iptw
#> * Delta selection: minimum across fitted methods
#> * Utility range: [0, 100]
#> * Policies: covariate with 2 actions
#> * Value error: 2.1946 (bound on |simulated - interventional| value)
#> * Transfer penalty: 4.3892 (additive regret upper bound)
#> * Minimax floor: 0.2743 (worst-case lower bound)
#>
#> * Simulated (adjusted) regret: 5
#> * Interventional bound: 9.3892
#>
#> Note: this is a plug-in bound using a deficiency proxy rather than an identified exact deficiency.
#> Note: minimum-across-methods selection is optimistic after model selection.
#>
#> Interpretation: Transfer penalty is 4.4 % of utility range given deltacat("=== Policy Deployment Decision ===\n\n")
#> === Policy Deployment Decision ===
delta_best <- min(def_results$estimates)
M <- diff(utility_range)
n_actions <- 2
transfer_penalty <- 2 * n_actions * M * delta_best
minimax_floor <- 0.25 * M * delta_best
cat(sprintf("Best achievable deficiency: %.3f\n", delta_best))
#> Best achievable deficiency: 0.011
cat(sprintf("Transfer penalty (2*|A|*M*delta): %.1f points\n", transfer_penalty))
#> Transfer penalty (2*|A|*M*delta): 4.4 points
cat(sprintf("Minimax safety floor (M/4*delta): %.1f points\n", minimax_floor))
#> Minimax safety floor (M/4*delta): 0.3 points
cat(sprintf("Observed regret: %.1f points\n", obs_regret))
#> Observed regret: 5.0 points
if (!is.null(bounds$regret_bound)) {
cat(sprintf("Worst-case regret: %.1f points\n", bounds$regret_bound))
}
#> Worst-case regret: 9.4 points
cat("\n")
# Illustrative deployment heuristic
if (transfer_penalty < 5) {
cat("✓ LOW additional decision risk on the chosen 0-100 utility scale.\n")
} else if (transfer_penalty < 10) {
cat("⚠ MODERATE additional decision risk. Deployment may be reasonable with monitoring.\n")
} else {
cat("✗ HIGH additional decision risk. Consider stronger design or randomized evidence before deployment.\n")
}
#> ✓ LOW additional decision risk on the chosen 0-100 utility scale.What if there’s additional unmeasured confounding?
# Map the confounding frontier
frontier <- confounding_frontier(
spec,
alpha_range = c(-2, 2),
gamma_range = c(-2, 2),
grid_size = 30
)
#> ℹ Computing benchmarks for observed covariates...
#> ✔ Computed confounding frontier: 30x30 grid
# Find an illustrative low-risk region for this utility scale
safe_region <- subset(frontier$grid, delta < 0.1)
cat(sprintf(
"Illustrative low-risk region covers %.1f%% of confounding space\n",
100 * nrow(safe_region) / nrow(frontier$grid)
))
#> Illustrative low-risk region covers 100.0% of confounding spaceIf you have the grf package installed, you can use causal forests for heterogeneous treatment effect estimation with deficiency bounds:
# Estimate deficiency using causal forests
if (requireNamespace("grf", quietly = TRUE)) {
def_grf <- estimate_deficiency(
spec,
methods = c("aipw", "grf"),
n_boot = 50
)
print(def_grf)
# Get individual treatment effect predictions
kernel_grf <- def_grf$kernel$grf
if (!is.null(kernel_grf$tau_hat)) {
cat("\nHeterogeneous Effects Detected:\n")
cat(sprintf("ATE from forest: %.2f\n", kernel_grf$ate))
cat(sprintf("CATE range: [%.2f, %.2f]\n",
min(kernel_grf$tau_hat),
max(kernel_grf$tau_hat)))
}
}These cutoffs are illustrative for the 0–100 utility scale in this vignette. In practice, calibrate them to the decision stakes.
| Check | Illustrative heuristic | Action if Failed |
|---|---|---|
| Transfer penalty < 5 points | Small additional regret budget | Deployment may be reasonable |
| Transfer penalty in [5, 10) points | Material but possibly manageable residual risk | Deploy only with active monitoring |
| Transfer penalty >= 10 points | Large residual regret budget | Consider pilot RCT or narrower deployment |
| NC diagnostic falsified | Any | Do not deploy without more data or stronger assumptions |
# Example: Re-estimate deficiency on new data
new_data <- ... # Your production data
new_spec <- causal_spec(
new_data,
treatment = "A",
outcome = "Y",
covariates = c("age", "severity")
)
# Quick check
def_monitor <- estimate_deficiency(
new_spec,
methods = "iptw",
n_boot = 50
)
# Alert if deficiency increased
if (def_monitor$estimates["iptw"] > 1.5 * delta_best) {
warning("Distribution shift detected! Deficiency increased.")
}For any policy \(\pi\) and bounded utility function \(u \in [0, M]\):
\[\text{Regret}_{do}(\pi) \leq \text{Regret}_{sim}(\pi) + 2|\mathcal{A}|\,M\delta\]
Proof sketch: For each action \(a\), the deficiency \(\delta\) bounds the total variation distance between the simulated and true interventional laws. A policy’s value is a sum over actions of expectations of functions with values in \([0, M]\), so its simulated value is off by at most \(|\mathcal{A}|M\delta\); a regret is a difference of two values, which gives the factor 2. The factor 2 cannot be removed in general.
Traditional ML focuses on: - Prediction error: How well does my model predict \(Y\)? - Generalization: Does performance hold on new data?
But for causal policy learning, we need: - Interventional validity: Does my policy work when deployed? - Confounding robustness: How much could unmeasured bias hurt me?
The safety floor answers these questions with formal guarantees.
| Concept | Definition | Function |
|---|---|---|
| Transfer penalty | \(2|\mathcal{A}|M\delta\) — additive regret inflation term | $transfer_penalty |
| Minimax safety floor | \((M/4)\delta\) — irreducible worst-case regret | $minimax_floor |
| Regret bound | simulated regret + transfer penalty | $regret_bound |
| Deficiency | Information gap between obs and do | estimate_deficiency() |
| Confounding Frontier | Deficiency as function of \((\alpha, \gamma)\) | confounding_frontier() |
Use these tools to make safe, accountable decisions from observational data.
Akdemir, D. (2026). Constraints on Causal Inference as Experiment Comparison. DOI: 10.5281/zenodo.18367346. See thm:policy_regret (Policy Regret Transfer) and thm:safety_floor (Minimax Safety Floor).
Athey, S., & Wager, S. (2021). Policy learning with observational data. Econometrica, 89(1), 133-161.
Kallus, N. (2020). Confounding-robust policy evaluation in infinite-horizon reinforcement learning. NeurIPS.