Skip to content

Confounded incentives, closed loop

A synthetic observational marketplace: a demand confounder drives both the historical incentive — the past policy raised incentives in busy periods — and the completions it is meant to buy. This is confounded logging, not a randomised switchback, so the offline effect estimate is biased. The plant is x' = a*x + b_true*u + noise, controlled over a receding horizon toward a target, and the business cost is asymmetric: missing riders (churn) costs more than wasted budget.

Two controllers act on it in closed loop:

  • certainty-equivalence trusts the biased estimate, under-actuates and undershoots the target;
  • confounding-robust takes an assumed sensitivity level Γ, turns it into a radius on the effect, and shifts the incentive to hedge the costlier error.

The radius calibration — the effect's half-width set to (Γ-1)/(Γ+1) times the estimate — is a benchmark assumption, not a general consequence of the sensitivity model. The script sweeps the true confounding strength, which neither controller knows.

uv run python scripts/run_dynamic_confounding_demo.py

What it printed when this site was built:

== confounding-robust control in CLOSED LOOP on a confounded dynamic plant ==
   (x' = a*x + b_true*u + noise; churn 4x budget-waste; assumed MSM Gamma=2.5)

     true confounding    CE cost  robust cost  robust wins
                  0.0      3.011        3.672      premium
                  0.4      7.440        2.367          yes
                  0.8     13.213        2.428          yes
                  1.2     18.840        4.840          yes

   worst-case cost  CE 18.840 -> robust 4.840
   savings at realistic confounding : 82%
   premium when unconfounded : 4% of CE downside

   The static minimax controller now runs in closed loop: pessimism bounds the
   accumulated downside, wins beyond a confounding threshold, pays a bounded premium.

Pessimism is not free: near zero confounding the robust controller pays a premium, because there its conservatism costs more than it saves. The sensitivity level Γ explains the radius; tutorial 5 runs the static version step by step. Code: confounding_robust_tracking_benchmark in chc.regret, surfaced by chc.sensitivity.