TeX 53.2%
Python 46.6%
1"""WP18 — experiment 10: robustness summary (discounting, preferences, transaction costs, grid2resolution, solver tolerance, simulation sample size, seed).34Reads results/data/<group>/<name>.json written by simulation.py and writes5results/data/robustness_summary.csv (key moments of every scenario next to the baseline, with6the relative deviation), results/data/grid_sensitivity.csv (grid / tolerance / sample-size7variants only) and results/data/diagnostics_table.csv (convergence of every run).8Usage: python -m src.robustness9"""10from __future__ import annotations1112import json13from pathlib import Path1415import numpy as np16import pandas as pd1718from .model import RESULTS_DIR1920DATA = RESULTS_DIR / "data"2122KEY_MOMENTS = [23 ("p", lambda d: d["p"]),24 ("rho", lambda d: d["rho"]),25 ("ownership", lambda d: d["moments"]["ownership_rate_all"]),26 ("ownership_under35", lambda d: d["moments"]["ownership_rate_under35"]),27 ("threshold_age1_zmid", lambda d: d["thresholds"]["1"][str(len(d["thresholds"]["1"]) // 2)]),28 ("gini_nw", lambda d: d["gini_nw"]),29 ("mean_nw", lambda d: d["mean_nw"]),30 ("mean_gift", lambda d: d["moments"]["mean_gift_received"]),31 ("lambda_age7", lambda d: d["mobility"]["lambda_persistence"]),32 ("q_RO_age3", lambda d: d["mobility"]["q_RO_age3"]),33 ("q_OO_age3", lambda d: d["mobility"]["q_OO_age3"]),34 ("rank_rank_nw", lambda d: d["mobility"]["rank_rank_slope_nw"]),35 ("nw_gap_age4", lambda d: d["gaps"]["nw_gap_owner_renter_age4"]),36 ("share_mortgage", lambda d: d["moments"]["share_with_mortgage"]),37]3839GRID_SCENARIOS = ("grid_fine", "grid_coarse", "tol_tight", "sample_small", "sample_large", "seed_alt")404142def load_all() -> dict[str, dict]:43 out = {}44 for f in sorted(DATA.glob("*/*.json")):45 if f.parent.name in ("welfare", "multiplicity"):46 continue47 with open(f) as fh:48 d = json.load(fh)49 if isinstance(d, dict) and "moments" in d:50 out[d["name"]] = d51 return out525354def _safe(fn, d):55 try:56 v = fn(d)57 return float(v) if v is not None else np.nan58 except (KeyError, TypeError, IndexError):59 return np.nan606162def build_summary(runs: dict[str, dict]) -> pd.DataFrame:63 base = runs.get("baseline")64 rows = []65 for name, d in runs.items():66 row = dict(name=name, group=d.get("group"), equilibrium=d.get("equilibrium"), converged=d.get("converged"),67 time_sec=d.get("time_sec"), excess_owner=d.get("excess_owner"), excess_rental=d.get("excess_rental"),68 bellman_owner_mean=(d.get("bellman") or {}).get("owner_mean"),69 bellman_renter_mean=(d.get("bellman") or {}).get("renter_mean"))70 for k, fn in KEY_MOMENTS:71 v = _safe(fn, d)72 row[k] = v73 if base is not None:74 b = _safe(fn, base)75 row[f"{k}_dev"] = (v - b) if np.isfinite(v) and np.isfinite(b) else np.nan76 rows.append(row)77 return pd.DataFrame(rows)787980def main():81 runs = load_all()82 if not runs:83 raise SystemExit("no scenario results found under results/data — run `make simulate` first")84 df = build_summary(runs)85 df.to_csv(DATA / "robustness_summary.csv", index=False)86 grid = df[df.name.isin(GRID_SCENARIOS + ("baseline",))].copy()87 grid.to_csv(DATA / "grid_sensitivity.csv", index=False)88 diag = df[["name", "group", "equilibrium", "converged", "time_sec", "excess_owner", "excess_rental",89 "bellman_owner_mean", "bellman_renter_mean"]]90 diag.to_csv(DATA / "diagnostics_table.csv", index=False)91 # max absolute deviation across the numerical variants, as a share of the baseline value92 base = df[df.name == "baseline"].iloc[0]93 dev = {}94 for k, _ in KEY_MOMENTS:95 vals = grid[k].values96 b = base[k]97 dev[k] = float(np.nanmax(np.abs(vals - b)) / max(abs(b), 1e-9)) if np.isfinite(b) else np.nan98 with open(DATA / "grid_sensitivity_max_relative_deviation.json", "w") as fh:99 json.dump(dev, fh, indent=1)100 print(df[["name", "equilibrium", "converged", "p", "ownership", "gini_nw", "lambda_age7"]].to_string())101 print("max relative deviation across numerical variants:", dev)102103104if __name__ == "__main__":105 main()106