SPB Git

spb/wp26_uqo

Public
9.5 KB · 187 lines python
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
Files · b780d9effbc0d688f848faf14e38219f6088143b
Menu

Theme