Every assumption, in one place
A simulation is an argument whose premises are code. This page lists the premises; the code is in spo/ and every number in this site can be regenerated with make reproduce.
Consumers
Each consumer i has a permanent valuation vi drawn from the configured family (lognormal by default: median 2.5, log-sd 0.5, marginal cost 1). Every other attribute is a noisy correlate of the standardised log valuation zi, so the seller's problem is genuinely one of prediction:
- income
- log-normal, correlation 0.5 with z; median 1
- demographic-proxy group
- Bernoulli with logit −1.5 × log income (30% overall, concentrated at low income). It carries no information about v beyond income — pricing on it is pure proxy discrimination
- urgency, loyalty
- probit-transformed correlates of z (0.4, 0.2)
- browsing signals ×3
- correlates of z (0.5) observed with noise sd 1.0 ÷ quality
- purchase history
- Poisson, rate increasing in income and loyalty
- profile signal
- z + N(0, 0.35 ÷ quality): the third-party "enriched" record available only in the individualized regime
- dynamic features
- exponentially-weighted purchase rate and a last-period-declined flag, updated from the consumer's own responses to this seller
- period valuation
- vit = vi × mean-one log-normal shock (sd 0.15)
- vulnerable
- bottom income tercile × top urgency tercile (a definition, used for measurement only)
Behaviour types (baseline shares)
- myopic (50%)
- buy iff p ≤ v
- price-sensitive (15%)
- buy iff p ≤ v and p ≤ 1.15 × last period's average price (reference-dependent refusal)
- strategic (10%)
- buy iff p ≤ 0.8 v: hold out to depress the seller's inference; costly today, may pay off through the dynamic features
- privacy-aware (10%)
- buy iff p ≤ v; behavioural and profile signals masked (population means) at a utility cost of 0.03 per period, zero under opt-out
- switcher (10%)
- buy iff p ≤ v; compares every seller under competition. Other types compare with probability 0.25
- bounded (5%)
- buy iff p ≤ v × exp(0.25 × logistic error): may overpay; negative surplus is counted
Sellers
Sellers observe only (features, price, bought) for offers they made. All model-based sellers price on a 40-point geometric grid from cost to the 99.5th valuation percentile, choosing the grid price that maximises (p − c)·P̂(buy | x, p). Exploration is ε-greedy with εt = max(0.03, 0.65t).
- uniform
- pooled logistic demand in price only; one price for everyone
- segment
- price-only logistic demand per (income tercile × group) cell, pooled fallback below 60 rows
- regression
- logistic demand linear in (x, p) — the classic WTP-regression pricer
- boosting
- histogram gradient boosting P̂(buy | x, p) with a monotone-decreasing constraint on price, 80 trees, refit every 2 periods
- bandit
- linear Thompson sampling over 12 price arms with context x (one Bayesian linear model per arm, batched posterior draws)
- oracle
- knows vi and prices against the period shock; the information upper bound, used for validation
Information regimes determine the feature set: uniform none; segment income tercile + group; behavioral + browsing ×3, history, loyalty, urgency signal, dynamic features (+ is-home under competition); individualized + the profile signal.
Competition
K sellers price every consumer each period. Consumer-seller valuations are vit × log-normal taste (sd 0.10); the home seller is the best taste match. Switchers, and other consumers with probability switch_prob, see every offer; the rest see only their home seller. The consumer picks the best considered option and applies their type rule. Each seller observes only the offers in a consumer's consideration set and learns from those. No seller observes rivals' prices; we make no claims about tacit collusion.
Welfare accounting
Per period: CS = Σ (vchosen − p)·1[buy] − privacy costs; PS = Σ (p − c)·1[buy]; TW = CS + PS; first best = Σ max(vbest − c, 0); DWL = first best − TW. Steady state = mean over periods 10–29. Distributional measures: Gini of consumer surplus (non-buyers at zero, negatives clipped to zero for the Gini only), surplus shares by income quintile, group price gap, vulnerable-group price-to-income ratio. Data-exposure index = share of sensitive signals the seller may use × share of consumers whose signals are visible.
Validation
Run before any result was interpreted (make validate, and make test): no sale below cost; no rational type buys above its valuation; CS + PS = TW and TW + DWL = first best in every period; the oracle with myopic consumers and no shock leaves zero consumer surplus and under 1% deadweight loss; the learned uniform price converges to the analytic monopoly optimum for lognormal, uniform and normal valuations; producer surplus is ordered oracle ≥ individualized ≥ uniform with myopic consumers; price caps and parity constraints hold in every period.
| check | result | detail |
|---|---|---|
| impossible_transactions_monopoly | pass | {"transactions": 7914, "below_cost": 0, "above_value_rational": 0} |
| impossible_transactions_duopoly | pass | {"transactions": 10313, "below_cost": 0, "above_value_rational": 0} |
| surplus_identity_monopoly | pass | {"max_err_cs_ps_tw": 0.0, "max_err_tw_dwl_fb": 0.0} |
| surplus_identity_duopoly | pass | {"max_err_cs_ps_tw": 0.0, "max_err_tw_dwl_fb": 0.0} |
| oracle_toy | pass | {"cs": 0.0, "dwl": 0.0067, "fb": 1093.4823} |
| uniform_analytic_lognormal | pass | {"analytic": 2.5496, "learned": 2.6467, "rel_log_err": 0.0374, "grid_log_step": 0.0596, "profit_pc": 0.7716, "analytic_profit_pc": 0.7505} |
| uniform_analytic_uniform | pass | {"analytic": 2.3748, "learned": 2.2129, "rel_log_err": 0.0706, "grid_log_step": 0.0369, "profit_pc": 0.7515, "analytic_profit_pc": 0.7562} |
| monotone_information | pass | {"uniform": {"ps": 1132.4504434183555, "cs": 1021.0516580927153}, "regression": {"ps": 1809.3263865630831, "cs": 347.80247728902594}, "oracle": {"ps": 2733.4617136396464, "cs": 0.0}} |
| policy_invariants | pass | {"price_cap_max_offer": 2.0, "price_cap_pass": true, "parity_max_rel_gap": 0.02, "parity_pass": true} |
Econometric recovery
See the Econometrics page for the data-generating process. Estimators are implemented with statsmodels (OLS, HC1 errors), hand-written 2SLS, a within-transformed TWFE with market-clustered errors, and a clean-control DiD with a Welch-type standard error.
Reviewer attack — and responses
- Agents are unrealistic
- True and deliberate: each type is a one-line rule so its effect is attributable. The consumer-mix experiment varies shares from 0 to 40%; results that survive all six mixes are reported as robust, the rest as behaviour-dependent.
- Welfare weights are arbitrary
- Unweighted surplus is the transparent default. Every distributional cut is reported so a reader can re-weight; we do not choose for them.
- Missing competition
- Included as an extension with 1–5 sellers and two switching intensities; not a full oligopoly equilibrium (sellers learn myopically), and we say so.
- Policies are oversimplified
- Each is one exact rule; the frontier experiment sweeps its parameter. The alternative — simulating legal ambiguity — would not be more informative.
- External validity
- Calibrated only in sign and order of magnitude against Dubé & Misra (2023) and Shiller (2014); see
docs/literature.md. No claim is made about any specific market. - Fairness definitions
- Two are used: an input rule (feature restriction) and an outcome rule (mean-price parity). They give different answers, which is the point.
- Sensitivity to distributions
- Five valuation families and three dispersions in the robustness sweep.
- Causal language
- In the simulation every effect is causal by construction. The econometrics page is about whether an empiricist could say the same, and mostly they could not.
Ethics
All consumers are synthetic. The "demographic proxy" group is a generic construct correlated with income so that proxy discrimination can be measured; it is not any real protected class. The repository contains no code that scores real people, no scraped data, and no interface to a live pricing system. The purpose is to audit what such systems do and which rules constrain them. The literature review (docs/literature.md) explains why no real dataset is used.
Reproduce: make test validate simulate-smoke simulate-main simulate-extensions robustness econometrics analyze figures paper site (or make reproduce). Configuration in configs/, pre-registration in hypotheses.json, manifests in runs/.