1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Monte Carlo illustration of the aggregation / deliberation propositions (theory.tex).3Outputs: results/theory/aggregation_sim.csv, figures/theory_mse_vs_rho.pdf, figures/theory_deliberation_rounds.pdf.4numpy only for the computations; seeded; runs in a few seconds."""5from __future__ import annotations67import csv8import sys9import time10from pathlib import Path1112import numpy as np1314ROOT = Path(__file__).resolve().parents[1]15sys.path.insert(0, str(ROOT / "src"))16from agentic_valuation.theory import aggregation_sim as ag # noqa: E4021718SEED = 2026102719OUT = ROOT / "results" / "theory"20FIG = ROOT / "figures"21OUT.mkdir(parents=True, exist_ok=True)22FIG.mkdir(parents=True, exist_ok=True)2324C1, C2, C3, C4 = "#2a78d6", "#eb6834", "#1baf7a", "#eda100" # categorical slots, fixed order25BLUES = ["#9ec5f4", "#5598e7", "#256abf", "#104281"] # one-hue ordinal ramp for K26INK, MUTED, GRID = "#1a1a19", "#6b6a66", "#e6e5e1"272829def style(ax):30 for side in ("top", "right"):31 ax.spines[side].set_visible(False)32 for side in ("left", "bottom"):33 ax.spines[side].set_color(GRID)34 ax.tick_params(colors=MUTED, labelsize=8)35 ax.yaxis.grid(True, color=GRID, lw=0.6)36 ax.set_axisbelow(True)373839def main() -> None:40 t0 = time.time()41 rng = np.random.default_rng(SEED)42 rows: list[dict] = []4344 # ---- Experiment 1: MSE of the equal-weight mean vs K and rho (Proposition 1) ----45 rhos = np.linspace(0.0, 0.9, 10)46 Ks = [1, 3, 12, 48]47 sigma = 1.048 n_draws = 20_00049 for K in Ks:50 for rho in rhos:51 closed = ag.mse_mean_closed_form(K, sigma, rho)52 sim = ag.simulate_mean_mse(K, sigma, rho, n_draws, rng)53 Sigma = ag.exchangeable_cov(K, sigma, rho)54 rows.append({"experiment": "mse_vs_rho", "K": K, "rho": round(float(rho), 3), "lam": "", "round": "",55 "variant": "", "closed_form": closed, "simulated": sim, "optimal": ag.mse_optimal(Sigma)})5657 # ---- Experiment 2: nested common-model structure (Proposition 3): M providers at fixed K = 12 ----58 for M in (1, 2, 3, 4, 6, 12):59 n = 12 // M60 closed = ag.mse_nested_closed_form(n, M, 0.3, 0.4, 0.5)61 Sigma = ag.nested_cov(n, M, 0.3, 0.4, 0.5)62 rows.append({"experiment": "providers", "K": 12, "rho": "", "lam": "", "round": "", "variant": f"M={M}",63 "closed_form": closed, "simulated": ag.mse_equal(Sigma), "optimal": ag.mse_optimal(Sigma)})6465 # ---- Experiment 3: deliberation rounds (Propositions 4, 6) in the evidence model ----66 # Views: 4 "popular" comparables seen by every agent plus 2 private comparables per agent (J = 28).67 # This mimics a shared comparable pool with role-specific evidence; the popular items are over-weighted68 # in the round-0 pool (||omega||^2 = 0.116 against the uniform minimum 1/28 = 0.036).69 K, J = 12, 2870 views = [np.array([0, 1, 2, 3, 4 + 2 * k, 5 + 2 * k]) for k in range(K)]71 providers = np.arange(K) % 372 tau2 = np.full(K, 0.10**2)73 comm = ag.EvidenceCommittee(J=J, views=views, providers=providers, tau2=tau2, sigma_c=0.05, sigma_u=0.12,74 sigma_g=0.04)75 rounds, lam = 4, 0.576 variants = {77 "opinion_only_median": dict(anchor="median"),78 "opinion_only_mean": dict(anchor="mean"),79 "exchange_round1": dict(anchor="median", exchange_at=1),80 "persuasion_noise": dict(anchor="median", persuasion_sd=0.05),81 }82 traj = {}83 for name, kw in variants.items():84 res = ag.simulate_deliberation(comm, rounds, lam, 40_000, np.random.default_rng(SEED + 1), **kw)85 traj[name] = res86 for r in range(rounds + 1):87 rows.append({"experiment": "deliberation", "K": K, "rho": "", "lam": lam, "round": r, "variant": name,88 "closed_form": "", "simulated": res["mse_mean"][r], "optimal": "",89 "dispersion": res["dispersion"][r], "mse_median": res["mse_median"][r]})90 rows.append({"experiment": "deliberation_closed_form", "K": K, "rho": "", "lam": "", "round": "", "variant": "round0",91 "closed_form": comm.mse_equal_closed_form(), "simulated": traj["opinion_only_median"]["mse_mean"][0],92 "optimal": ""})93 rows.append({"experiment": "deliberation_closed_form", "K": K, "rho": "", "lam": "", "round": "",94 "variant": "after_full_exchange", "closed_form": comm.mse_equal_closed_form(ag.exchange_views(views)),95 "simulated": traj["exchange_round1"]["mse_mean"][-1], "optimal": ""})9697 # ---- Experiment 3b: a partial exchange can raise the pooled MSE (Proposition 6, example) ----98 ex = ag.EvidenceCommittee(J=2, views=[np.array([0]), np.array([1]), np.array([1])], providers=np.zeros(3, int),99 tau2=np.zeros(3), sigma_u=1.0)100 rows.append({"experiment": "partial_exchange_example", "K": 3, "variant": "before", "closed_form": ex.mse_equal_closed_form()})101 rows.append({"experiment": "partial_exchange_example", "K": 3, "variant": "agent1_adds_item2",102 "closed_form": ex.mse_equal_closed_form(views=[np.array([0, 1]), np.array([1]), np.array([1])])})103 rows.append({"experiment": "partial_exchange_example", "K": 3, "variant": "full_exchange",104 "closed_form": ex.mse_equal_closed_form(views=ag.exchange_views(ex.views))})105106 # ---- Experiment 4: placebo roles (Proposition 8): permute prompt variances across slots ----107 tau2_het = np.linspace(0.05, 0.30, K) ** 2108 comm_h = ag.EvidenceCommittee(J=J, views=views, providers=providers, tau2=tau2_het, sigma_c=0.05, sigma_u=0.12,109 sigma_g=0.04)110 base = comm_h.mse_equal_closed_form()111 shocks = comm_h.draw_shocks(40_000, np.random.default_rng(SEED + 2))112 for i in range(6):113 perm = np.arange(K) if i == 0 else rng.permutation(K)114 x = comm_h.draw(40_000, rng, tau2=tau2_het[perm], shocks=shocks)115 rows.append({"experiment": "placebo", "K": K, "rho": "", "lam": "", "round": "", "variant": f"perm{i}",116 "closed_form": ag.placebo_mse(comm_h, perm), "simulated": float(np.mean(x.mean(axis=1) ** 2)),117 "optimal": base})118119 # ---- Experiment 5: over-shrinkage (Proposition 7): inverse-variance pool after deliberation ----120 sig2 = np.linspace(0.05, 0.4, K) ** 2121 Sigma0 = np.diag(sig2)122 for r in range(0, 5):123 Sr = ag.deliberation_cov(Sigma0, r, 0.5)124 w_iv = ag.inverse_variance_weights(Sr)125 rows.append({"experiment": "over_shrinkage", "K": K, "rho": "", "lam": 0.5, "round": r, "variant": "inverse_variance",126 "closed_form": ag.mse_pool(w_iv, Sr), "simulated": "", "optimal": ag.mse_optimal(Sigma0),127 "mse_equal": ag.mse_equal(Sigma0)})128129 fields = ["experiment", "K", "rho", "lam", "round", "variant", "closed_form", "simulated", "optimal", "dispersion",130 "mse_median", "mse_equal"]131 with open(OUT / "aggregation_sim.csv", "w", newline="") as f:132 w = csv.DictWriter(f, fieldnames=fields)133 w.writeheader()134 for row in rows:135 w.writerow({k: row.get(k, "") for k in fields})136137 # ---- Figures ----138 import matplotlib139140 matplotlib.use("Agg")141 import matplotlib.pyplot as plt142143 plt.rcParams.update({"font.family": "serif", "font.size": 9, "axes.labelcolor": INK, "text.color": INK})144145 fig, ax = plt.subplots(figsize=(5.2, 3.4))146 for K, col in zip(Ks, BLUES):147 sub = [r for r in rows if r["experiment"] == "mse_vs_rho" and r["K"] == K]148 ax.plot([r["rho"] for r in sub], [r["closed_form"] for r in sub], color=col, lw=2, label=f"$K={K}$")149 ax.scatter([r["rho"] for r in sub], [r["simulated"] for r in sub], color=col, s=14, zorder=3)150 ax.plot(rhos, rhos * sigma**2, color=C2, lw=1.2, ls="--", label=r"floor $\rho\sigma^2$")151 ax.set_xlabel(r"pairwise error correlation $\rho$")152 ax.set_ylabel(r"MSE of the equal-weight mean / $\sigma^2$")153 ax.set_ylim(0, 1.02)154 ax.legend(frameon=False, fontsize=8, loc="upper left")155 style(ax)156 fig.tight_layout()157 fig.savefig(FIG / "theory_mse_vs_rho.pdf")158 plt.close(fig)159160 fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2))161 labels = {"opinion_only_median": "opinion only (median anchor)", "opinion_only_mean": "opinion only (mean anchor)",162 "exchange_round1": "evidence exchange at round 1", "persuasion_noise": "persuasion noise"}163 cols = {"opinion_only_median": C1, "opinion_only_mean": BLUES[0], "exchange_round1": C3, "persuasion_noise": C2}164 rr = np.arange(rounds + 1)165 for name in variants:166 axes[0].plot(rr, traj[name]["dispersion"], color=cols[name], lw=2, marker="o", ms=4, label=labels[name])167 axes[1].plot(rr, traj[name]["mse_mean"], color=cols[name], lw=2, marker="o", ms=4, label=labels[name])168 axes[0].plot(rr, traj["opinion_only_median"]["dispersion"][0] * (1 - lam) ** rr, color=INK, lw=0.8, ls=":",169 label=r"$(1-\lambda)^r$ contraction")170 axes[0].set_title("Cross-sectional dispersion of estimates", fontsize=9)171 axes[1].set_title("MSE of the pooled mean", fontsize=9)172 for ax in axes:173 ax.set_xlabel("deliberation round $r$")174 ax.set_xticks(rr)175 style(ax)176 axes[1].set_ylim(0, None)177 axes[1].legend(frameon=False, fontsize=7, loc="lower left")178 axes[0].legend(frameon=False, fontsize=7, loc="upper right")179 fig.tight_layout()180 fig.savefig(FIG / "theory_deliberation_rounds.pdf")181 plt.close(fig)182 print(f"wrote {OUT / 'aggregation_sim.csv'} ({len(rows)} rows) and 2 figures in {time.time() - t0:.1f} s")183184185if __name__ == "__main__":186 main()187