# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca """Monte Carlo illustration of the aggregation / deliberation propositions (theory.tex). Outputs: results/theory/aggregation_sim.csv, figures/theory_mse_vs_rho.pdf, figures/theory_deliberation_rounds.pdf. numpy only for the computations; seeded; runs in a few seconds.""" from __future__ import annotations import csv import sys import time from pathlib import Path import numpy as np ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) from agentic_valuation.theory import aggregation_sim as ag # noqa: E402 SEED = 20261027 OUT = ROOT / "results" / "theory" FIG = ROOT / "figures" OUT.mkdir(parents=True, exist_ok=True) FIG.mkdir(parents=True, exist_ok=True) C1, C2, C3, C4 = "#2a78d6", "#eb6834", "#1baf7a", "#eda100" # categorical slots, fixed order BLUES = ["#9ec5f4", "#5598e7", "#256abf", "#104281"] # one-hue ordinal ramp for K INK, MUTED, GRID = "#1a1a19", "#6b6a66", "#e6e5e1" def style(ax): for side in ("top", "right"): ax.spines[side].set_visible(False) for side in ("left", "bottom"): ax.spines[side].set_color(GRID) ax.tick_params(colors=MUTED, labelsize=8) ax.yaxis.grid(True, color=GRID, lw=0.6) ax.set_axisbelow(True) def main() -> None: t0 = time.time() rng = np.random.default_rng(SEED) rows: list[dict] = [] # ---- Experiment 1: MSE of the equal-weight mean vs K and rho (Proposition 1) ---- rhos = np.linspace(0.0, 0.9, 10) Ks = [1, 3, 12, 48] sigma = 1.0 n_draws = 20_000 for K in Ks: for rho in rhos: closed = ag.mse_mean_closed_form(K, sigma, rho) sim = ag.simulate_mean_mse(K, sigma, rho, n_draws, rng) Sigma = ag.exchangeable_cov(K, sigma, rho) rows.append({"experiment": "mse_vs_rho", "K": K, "rho": round(float(rho), 3), "lam": "", "round": "", "variant": "", "closed_form": closed, "simulated": sim, "optimal": ag.mse_optimal(Sigma)}) # ---- Experiment 2: nested common-model structure (Proposition 3): M providers at fixed K = 12 ---- for M in (1, 2, 3, 4, 6, 12): n = 12 // M closed = ag.mse_nested_closed_form(n, M, 0.3, 0.4, 0.5) Sigma = ag.nested_cov(n, M, 0.3, 0.4, 0.5) rows.append({"experiment": "providers", "K": 12, "rho": "", "lam": "", "round": "", "variant": f"M={M}", "closed_form": closed, "simulated": ag.mse_equal(Sigma), "optimal": ag.mse_optimal(Sigma)}) # ---- Experiment 3: deliberation rounds (Propositions 4, 6) in the evidence model ---- # Views: 4 "popular" comparables seen by every agent plus 2 private comparables per agent (J = 28). # This mimics a shared comparable pool with role-specific evidence; the popular items are over-weighted # in the round-0 pool (||omega||^2 = 0.116 against the uniform minimum 1/28 = 0.036). K, J = 12, 28 views = [np.array([0, 1, 2, 3, 4 + 2 * k, 5 + 2 * k]) for k in range(K)] providers = np.arange(K) % 3 tau2 = np.full(K, 0.10**2) comm = ag.EvidenceCommittee(J=J, views=views, providers=providers, tau2=tau2, sigma_c=0.05, sigma_u=0.12, sigma_g=0.04) rounds, lam = 4, 0.5 variants = { "opinion_only_median": dict(anchor="median"), "opinion_only_mean": dict(anchor="mean"), "exchange_round1": dict(anchor="median", exchange_at=1), "persuasion_noise": dict(anchor="median", persuasion_sd=0.05), } traj = {} for name, kw in variants.items(): res = ag.simulate_deliberation(comm, rounds, lam, 40_000, np.random.default_rng(SEED + 1), **kw) traj[name] = res for r in range(rounds + 1): rows.append({"experiment": "deliberation", "K": K, "rho": "", "lam": lam, "round": r, "variant": name, "closed_form": "", "simulated": res["mse_mean"][r], "optimal": "", "dispersion": res["dispersion"][r], "mse_median": res["mse_median"][r]}) rows.append({"experiment": "deliberation_closed_form", "K": K, "rho": "", "lam": "", "round": "", "variant": "round0", "closed_form": comm.mse_equal_closed_form(), "simulated": traj["opinion_only_median"]["mse_mean"][0], "optimal": ""}) rows.append({"experiment": "deliberation_closed_form", "K": K, "rho": "", "lam": "", "round": "", "variant": "after_full_exchange", "closed_form": comm.mse_equal_closed_form(ag.exchange_views(views)), "simulated": traj["exchange_round1"]["mse_mean"][-1], "optimal": ""}) # ---- Experiment 3b: a partial exchange can raise the pooled MSE (Proposition 6, example) ---- ex = ag.EvidenceCommittee(J=2, views=[np.array([0]), np.array([1]), np.array([1])], providers=np.zeros(3, int), tau2=np.zeros(3), sigma_u=1.0) rows.append({"experiment": "partial_exchange_example", "K": 3, "variant": "before", "closed_form": ex.mse_equal_closed_form()}) rows.append({"experiment": "partial_exchange_example", "K": 3, "variant": "agent1_adds_item2", "closed_form": ex.mse_equal_closed_form(views=[np.array([0, 1]), np.array([1]), np.array([1])])}) rows.append({"experiment": "partial_exchange_example", "K": 3, "variant": "full_exchange", "closed_form": ex.mse_equal_closed_form(views=ag.exchange_views(ex.views))}) # ---- Experiment 4: placebo roles (Proposition 8): permute prompt variances across slots ---- tau2_het = np.linspace(0.05, 0.30, K) ** 2 comm_h = ag.EvidenceCommittee(J=J, views=views, providers=providers, tau2=tau2_het, sigma_c=0.05, sigma_u=0.12, sigma_g=0.04) base = comm_h.mse_equal_closed_form() shocks = comm_h.draw_shocks(40_000, np.random.default_rng(SEED + 2)) for i in range(6): perm = np.arange(K) if i == 0 else rng.permutation(K) x = comm_h.draw(40_000, rng, tau2=tau2_het[perm], shocks=shocks) rows.append({"experiment": "placebo", "K": K, "rho": "", "lam": "", "round": "", "variant": f"perm{i}", "closed_form": ag.placebo_mse(comm_h, perm), "simulated": float(np.mean(x.mean(axis=1) ** 2)), "optimal": base}) # ---- Experiment 5: over-shrinkage (Proposition 7): inverse-variance pool after deliberation ---- sig2 = np.linspace(0.05, 0.4, K) ** 2 Sigma0 = np.diag(sig2) for r in range(0, 5): Sr = ag.deliberation_cov(Sigma0, r, 0.5) w_iv = ag.inverse_variance_weights(Sr) rows.append({"experiment": "over_shrinkage", "K": K, "rho": "", "lam": 0.5, "round": r, "variant": "inverse_variance", "closed_form": ag.mse_pool(w_iv, Sr), "simulated": "", "optimal": ag.mse_optimal(Sigma0), "mse_equal": ag.mse_equal(Sigma0)}) fields = ["experiment", "K", "rho", "lam", "round", "variant", "closed_form", "simulated", "optimal", "dispersion", "mse_median", "mse_equal"] with open(OUT / "aggregation_sim.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=fields) w.writeheader() for row in rows: w.writerow({k: row.get(k, "") for k in fields}) # ---- Figures ---- import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt plt.rcParams.update({"font.family": "serif", "font.size": 9, "axes.labelcolor": INK, "text.color": INK}) fig, ax = plt.subplots(figsize=(5.2, 3.4)) for K, col in zip(Ks, BLUES): sub = [r for r in rows if r["experiment"] == "mse_vs_rho" and r["K"] == K] ax.plot([r["rho"] for r in sub], [r["closed_form"] for r in sub], color=col, lw=2, label=f"$K={K}$") ax.scatter([r["rho"] for r in sub], [r["simulated"] for r in sub], color=col, s=14, zorder=3) ax.plot(rhos, rhos * sigma**2, color=C2, lw=1.2, ls="--", label=r"floor $\rho\sigma^2$") ax.set_xlabel(r"pairwise error correlation $\rho$") ax.set_ylabel(r"MSE of the equal-weight mean / $\sigma^2$") ax.set_ylim(0, 1.02) ax.legend(frameon=False, fontsize=8, loc="upper left") style(ax) fig.tight_layout() fig.savefig(FIG / "theory_mse_vs_rho.pdf") plt.close(fig) fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2)) labels = {"opinion_only_median": "opinion only (median anchor)", "opinion_only_mean": "opinion only (mean anchor)", "exchange_round1": "evidence exchange at round 1", "persuasion_noise": "persuasion noise"} cols = {"opinion_only_median": C1, "opinion_only_mean": BLUES[0], "exchange_round1": C3, "persuasion_noise": C2} rr = np.arange(rounds + 1) for name in variants: axes[0].plot(rr, traj[name]["dispersion"], color=cols[name], lw=2, marker="o", ms=4, label=labels[name]) axes[1].plot(rr, traj[name]["mse_mean"], color=cols[name], lw=2, marker="o", ms=4, label=labels[name]) axes[0].plot(rr, traj["opinion_only_median"]["dispersion"][0] * (1 - lam) ** rr, color=INK, lw=0.8, ls=":", label=r"$(1-\lambda)^r$ contraction") axes[0].set_title("Cross-sectional dispersion of estimates", fontsize=9) axes[1].set_title("MSE of the pooled mean", fontsize=9) for ax in axes: ax.set_xlabel("deliberation round $r$") ax.set_xticks(rr) style(ax) axes[1].set_ylim(0, None) axes[1].legend(frameon=False, fontsize=7, loc="lower left") axes[0].legend(frameon=False, fontsize=7, loc="upper right") fig.tight_layout() fig.savefig(FIG / "theory_deliberation_rounds.pdf") plt.close(fig) print(f"wrote {OUT / 'aggregation_sim.csv'} ({len(rows)} rows) and 2 figures in {time.time() - t0:.1f} s") if __name__ == "__main__": main()