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).