Skip to content

chc.marketplace

Stability tier: evolving.

Offline causal decision-making under equilibrium interference.

The problem, end to end: learn an incentive policy from confounded, switchback-logged marketplace data where (a) the logging policy is confounded (operators already incentivise busy zones, so demand drives both treatment and outcome), and (b) SUTVA fails because drivers are mobile -- incentivising a zone pulls drivers from its neighbours through a shared equilibrium (Munro-Wager-Xu market-equilibrium interference; Wager-Xu supply cannibalisation; shared-state DML, arXiv 2504.08836). Three failure modes, each a real baseline:

  • predictive / MOPO fits an outcome model on the confounded logs -- confounding inflates the response of already-busy zones, so it over-allocates to them (where drivers only cannibalise).
  • naive causal de-confounds per zone (backdoor on demand) but assumes SUTVA -- it allocates by local uplift, double-counting drivers the market cannot supply (predicts a lift it never gets).
  • CHC de-confounds and plans through the structural equilibrium (chc.games) with an offline-pessimism margin (a Wasserstein/DRO-style shrink by the estimate's influence-function SE), so cannibalisation is built in and extrapolation is not trusted.

Everything is differentiable and reuses the equilibrium solver in chc.games.

SharedStateMarket dataclass

Mobile drivers reach a congestion equilibrium; the platform allocates a fixed budget.

The shared state is the equilibrium driver distribution x(u) (sum x = mass is conserved, so an incentive to one zone is a driver taken from the others -- the interference channel). attract is aligned with demand so the zones a naive allocator wants to feed (high local uplift) are the already-saturated ones, where extra drivers only cannibalise.

completions(u)

Total realised rides = sum_i min(demand_i, drivers_i) at the equilibrium.

oracle_allocation(steps=1200)

Equilibrium-aware optimum with full model access (the regret reference).

generate_logs(n_blocks, key)

Confounded switchback logs, shape (n_blocks, n_zones): each block draws a demand season and a budget scale (switchback), assigns a demand-chasing (confounded) allocation, and records the shared-state exposure aggregate and realised completions.

ExposureResponse dataclass

A calibrated per-zone incentive response dY/du plus its influence-function SE.

calibrate_predictive(logs)

MOPO-style: regress completions on the incentive per zone, ignoring the demand confounder.

calibrate_naive_causal(logs)

Backdoor per zone: adjust for demand (de-confound) but assume SUTVA (no shared state).

calibrate_shared_state(logs)

AIPW-style shared-state calibration: de-confound and control the shared exposure aggregate, so the marginal is the direct response net of the cannibalisation the aggregate absorbs.

sutva_allocation(market, response, radius)

Baseline allocation: fund zones by their pessimistic local uplift (the SUTVA assumption).

pessimistic_equilibrium_allocation(market, response, *, radius=1.0, steps=600)

CHC allocation: plan the budget through the congestion equilibrium (chc.games) so the driver-conservation (cannibalisation) channel the SUTVA baselines assume away is structural. The de-confounded response gates the plan through a W-DRO pessimism tilt (radius * SE): where the offline estimate is weak or unsupported, the incentive is discounted rather than trusted.

interference_bias(market, response, allocation)

SUTVA over-count: the lift a per-zone (additive) model predicts minus the lift realised at the equilibrium -- the interference the naive planner ignores (positive = over-prediction).