SPB Git forge

spb/wp19_uqo

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
18.0 KB · 261 lines python
Raw Blame History
1"""Publication figures for WP19, generated only from results/processed/*.csv (Phase 5).2Colour-blind-safe categorical palette (validated reference palette); vector PDF + PNG; copies to paper/figures/.34Run: python -m src.plotting5"""6from __future__ import annotations78import shutil9from pathlib import Path1011import matplotlib12matplotlib.use("Agg")13import matplotlib.pyplot as plt14import numpy as np15import pandas as pd16import yaml1718ROOT = Path(__file__).resolve().parents[1]19PRO = ROOT / "results" / "processed"; FIG = ROOT / "results" / "figures"; PFIG = ROOT / "paper" / "figures"20FIG.mkdir(parents=True, exist_ok=True); PFIG.mkdir(parents=True, exist_ok=True)2122PAL = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"]23INK = "#0b0b0b"; INK2 = "#52514e"; GRID = "#e6e5e1"2425plt.rcParams.update({26    "font.family": "serif", "font.size": 9, "axes.titlesize": 9.5, "axes.labelsize": 9, "legend.fontsize": 8,27    "xtick.labelsize": 8, "ytick.labelsize": 8, "axes.edgecolor": INK2, "axes.labelcolor": INK, "xtick.color": INK2,28    "ytick.color": INK2, "axes.grid": True, "grid.color": GRID, "grid.linewidth": 0.6, "axes.spines.top": False,29    "axes.spines.right": False, "lines.linewidth": 1.8, "legend.frameon": False, "figure.dpi": 150, "savefig.dpi": 300,30    "axes.prop_cycle": matplotlib.cycler(color=PAL),31})323334def save(fig, name):35    fig.tight_layout()36    for ext in ("pdf", "png"):37        fig.savefig(FIG / f"{name}.{ext}", bbox_inches="tight")38    shutil.copy(FIG / f"{name}.pdf", PFIG / f"{name}.pdf")39    plt.close(fig)40    print("figure", name)414243def rd(name):44    return pd.read_csv(PRO / name)454647def fig01_equilibrium():48    fe = rd("exp01_free_entry_curve.csv"); m = rd("exp01_baseline_moments.csv").iloc[0]49    cfg = yaml.safe_load((ROOT / "config" / "baseline.yaml").read_text()); A = cfg["matching"]["A"]; al = cfg["matching"]["alpha"]50    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))51    ax[0].semilogx(fe.theta, fe.residual, color=PAL[0]); ax[0].axhline(0, color=INK2, lw=0.8)52    ax[0].axvline(m.theta, color=PAL[1], ls="--", lw=1); ax[0].set_xlabel(r"tightness $\theta=B/S$ (log scale)")53    ax[0].set_ylabel("free-entry residual $R(\\theta)$"); ax[0].set_title("(a) Free entry: unique root")54    ax[0].annotate(fr"$\theta^*={m.theta:.2f}$", (m.theta, 0), xytext=(m.theta * 1.6, 0.4 * fe.residual.max()), color=PAL[1], fontsize=8,55                   arrowprops=dict(arrowstyle="-", color=PAL[1], lw=0.8))56    th = np.geomspace(0.1, 40, 200)57    ax[1].loglog(th, A * th ** al, color=PAL[0], label=r"seller meeting rate $\lambda_s(\theta)$")58    ax[1].loglog(th, A * th ** (al - 1), color=PAL[2], label=r"buyer meeting rate $\lambda_b(\theta)$")59    ax[1].axvline(m.theta, color=PAL[1], ls="--", lw=1); ax[1].set_xlabel(r"tightness $\theta$ (log scale)"); ax[1].set_ylabel("meetings per year (log scale)")60    ax[1].set_title("(b) Matching technology"); ax[1].legend(loc="lower left")61    save(fig, "fig01_equilibrium_matching")626364def fig02_urgency_prices():65    d = rd("exp02_urgency_gradient.csv")66    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))67    for i, (mult, g) in enumerate(d.groupby("A_mult")):68        g = g[g.participation]69        ax[0].plot(g.c, g.V, color=PAL[i], label=fr"$A\times{mult}$ ($\Lambda_s$={g.Lam_s.iloc[0]:.1f})")70        ax[1].plot(g.c, g.pbar, color=PAL[i])71    ax[0].set_xlabel("carrying cost $c$ (share of value per year)"); ax[0].set_ylabel("reservation value $V(c)$"); ax[0].set_title("(a) Reservation value")72    ax[1].set_xlabel("carrying cost $c$"); ax[1].set_ylabel(r"expected transaction price $\bar p(c)$"); ax[1].set_title("(b) Expected price conditional on sale")73    ax[0].legend(title="partial equilibrium, $\\theta$ fixed", loc="lower left")74    save(fig, "fig02_urgency_reservation_price")757677def fig03_urgency_hazard():78    d = rd("exp02_urgency_gradient.csv"); t = rd("exp01_baseline_types.csv")79    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))80    for i, (mult, g) in enumerate(d.groupby("A_mult")):81        g = g[g.participation]82        ax[0].plot(g.c, g.eta, color=PAL[i], label=fr"$A\times{mult}$")83        ax[1].plot(g.c, g.E_tau_months, color=PAL[i])84    ax[0].scatter(t.c, t.eta, color=INK, s=12, zorder=5, label="baseline types")85    ax[0].set_xlabel("carrying cost $c$"); ax[0].set_ylabel(r"sale hazard $\eta(c)$ (per year)"); ax[0].set_title("(a) Sale hazard"); ax[0].legend(loc="upper left")86    ax[1].set_xlabel("carrying cost $c$"); ax[1].set_ylabel("expected listing duration (months)"); ax[1].set_title("(b) Expected time on market")87    save(fig, "fig03_urgency_sale_hazard")888990def fig04_discount_vs_arrivals():91    a = rd("exp03_arrival_shock.csv"); g = rd("exp03_gamma_sign_change.csv")92    fig, ax = plt.subplots(1, 3, figsize=(9.6, 2.9))93    for kind, ls in (("GE", "-"), ("PE", "--")):94        s = a[a.kind == kind]95        ax[0].plot(s.A_mult, 100 * s.D_urgent, ls=ls, color=PAL[0], label=f"most urgent type ({kind})")96        ax[0].plot(s.A_mult, 100 * s.D_mean, ls=ls, color=PAL[1], label=f"transaction-weighted ({kind})")97        ax[1].plot(s.A_mult, s.price_mean, ls=ls, color=PAL[0], label=f"mean price ({kind})")98    ax[0].set_xlabel("matching efficiency (multiple of baseline $A$)"); ax[0].set_ylabel("urgency discount $D$ (%)"); ax[0].set_title("(a) Urgency discount"); ax[0].legend()99    ax[1].set_xlabel("matching efficiency (multiple of baseline $A$)"); ax[1].set_ylabel("transaction-weighted mean price"); ax[1].set_title("(b) Price level"); ax[1].legend()100    ax[2].semilogx(g.Lam_s, 100 * g.Gamma, color=PAL[2]); ax[2].axhline(0, color=INK2, lw=0.8)101    ax[2].set_xlabel(r"effective arrival rate $\Lambda_s$ (log scale)"); ax[2].set_ylabel(r"market liquidity gap $\Gamma$ (%)"); ax[2].set_title("(c) Gap to Walrasian benchmark, patient seller")102    save(fig, "fig04_liquidity_discount_vs_arrivals")103104105def fig05_financing():106    d = rd("exp04_financing_phi.csv"); base = d[np.isclose(d.phi_m, 0.85)]107    fig, ax = plt.subplots(1, 3, figsize=(9.6, 2.9))108    ax[0].plot(base.c, 100 * base.Delta_b, color=PAL[0], label="delay-bargaining term $\\Delta_b(c)$")109    ax[0].plot(base.c, 100 * base.outside_term, color=PAL[3], label="outside-option term")110    ax[0].plot(base.c, 100 * base.sel_term, color=PAL[2], label="selection term")111    ax[0].plot(base.c, 100 * base.gap, color=INK, lw=1.2, ls="--", label="total mortgage$-$cash gap")112    ax[0].axhline(0, color=INK2, lw=0.8); ax[0].set_xlabel("carrying cost $c$"); ax[0].set_ylabel("% of value"); ax[0].set_title("(a) Decomposition by seller type"); ax[0].legend(fontsize=7)113    agg = d.groupby("phi_m").apply(lambda g: pd.Series({"gap": 100 * ((g.gap * g.f).sum() / g.f.sum()), "tom": (g.E_tau_listed_months * g.f).sum(), "theta": g.theta.iloc[0]}), include_groups=False).reset_index()114    ax[1].plot(agg.phi_m, agg.gap, marker="o", ms=4, color=PAL[0]); ax[1].set_xlabel(r"approval probability $\phi$"); ax[1].set_ylabel("mean mortgage$-$cash gap (%)"); ax[1].set_title("(b) Gap vs approval probability")115    nu = rd("exp04_financing_nu.csv"); aggn = nu[nu.nu < 1e3].groupby("nu").apply(lambda g: pd.Series({"gap": 100 * ((g.gap * g.f).sum() / g.f.sum()), "tom": (g.E_tau_listed_months * g.f).sum()}), include_groups=False).reset_index()116    ax[2].plot(12 / aggn.nu, aggn.gap, marker="o", ms=4, color=PAL[1]); ax[2].set_xlabel(r"expected approval delay $12/\nu$ (months)"); ax[2].set_ylabel("mean mortgage$-$cash gap (%)"); ax[2].set_title("(c) Gap vs financing delay")117    save(fig, "fig05_financing_counterfactual")118119120def fig06_volume_price():121    a = rd("exp03_arrival_shock.csv"); s = a[a.kind == "GE"].sort_values("A_mult")122    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))123    sc = ax[0].scatter(s.Q, s.price_mean, c=s.A_mult, cmap="Blues", s=18, edgecolor=INK2, linewidth=0.3)124    ax[0].plot(s.Q, s.price_mean, color=INK2, lw=0.6)125    ax[0].set_xlabel("steady-state sales flow $Q$ (per year, inflow $s=1$)"); ax[0].set_ylabel("mean transaction price"); ax[0].set_title("(a) Volume and price across liquidity states")126    cb = fig.colorbar(sc, ax=ax[0], pad=0.02); cb.set_label("$A$ multiple")127    ax[1].plot(s.S, s.mean_tom_sold_months, color=PAL[1], marker="o", ms=3)128    ax[1].set_xlabel("inventory of listings $S$"); ax[1].set_ylabel("mean time on market of sold listings (months)"); ax[1].set_title("(b) Inventory and time on market")129    save(fig, "fig06_volume_vs_price")130131132def fig07_survival():133    d = rd("exp01_baseline_survival.csv"); t = rd("exp01_baseline_types.csv")134    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))135    cols = [c for c in d.columns if c.startswith("survival_c")]136    for i, c in enumerate(cols):137        idx = int(c.split("c")[-1]); ax[0].plot(d.tau_months, d[c], color=PAL[i + 1], lw=1.2, label=fr"type $c={t.c.iloc[idx]:.3f}$")138    ax[0].plot(d.tau_months, d.survival_population, color=INK, lw=2, label="population (mixture)")139    ax[0].set_xlabel("listing duration (months)"); ax[0].set_ylabel("survival probability"); ax[0].set_title("(a) Survival of listings"); ax[0].legend(); ax[0].set_xlim(0, 18)140    ax[1].plot(d.tau_months[1:-1], d.hazard_population[1:-1], color=INK); ax[1].set_xlim(0, 18)141    ax[1].set_xlabel("listing duration (months)"); ax[1].set_ylabel("population exit hazard (per year)"); ax[1].set_title("(b) Spurious negative duration dependence")142    save(fig, "fig07_tom_survival")143144145def fig08_price_distributions():146    b = rd("exp08_price_density_baseline.csv"); lo = rd("exp08_price_density_lowA.csv")147    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))148    types = [c for c in b.columns if c.startswith("type")]149    bottom = np.zeros(len(b)); cmap = plt.get_cmap("Blues")150    for i, c in enumerate(types):151        col = cmap(0.25 + 0.7 * i / max(len(types) - 1, 1))152        ax[0].fill_between(b.price, bottom, bottom + b[c], color=col, alpha=0.95, lw=0, label=("most patient" if i == 0 else "most urgent" if i == len(types) - 1 else None))153        bottom = bottom + b[c].values154    ax[0].plot(b.price, b.density, color=INK, lw=1); ax[0].set_xlabel("transaction price (units of mean valuation)"); ax[0].set_ylabel("density"); ax[0].set_title("(a) Mixture by seller type, baseline"); ax[0].legend(title="seller types (light = patient)", fontsize=7)155    ax[1].plot(b.price, b.density, color=PAL[0], label="baseline $A$"); ax[1].plot(lo.price, lo.density, color=PAL[1], label=r"$A\times0.6$ (illiquid)")156    ax[1].set_xlabel("transaction price"); ax[1].set_ylabel("density"); ax[1].set_title("(b) Liquidity and the price distribution"); ax[1].legend()157    save(fig, "fig08_seller_type_price_distributions")158159160def fig09_index_selection():161    d = rd("exp10_selection_decomposition.csv"); d = d[(d.kind == "GE") & d.A_mult.isin([0.7, 1.3]) & (d.c_max == 0.3)]162    h = rd("exp10_hazard_times_mrl.csv")163    fig, ax = plt.subplots(1, 3, figsize=(10.2, 3.0))164    Gs = list(dict.fromkeys(d.G)); x = np.arange(len(Gs)); w = 0.36165    for j, (mult, lab) in enumerate(((0.7, r"$A\times0.7$"), (1.3, r"$A\times1.3$"))):166        s_ = d[d.A_mult == mult].set_index("G").reindex(Gs)167        ax[0].bar(x + (j - 0.5) * w, 100 * s_.within_log, w * 0.92, color=PAL[j], label=lab)168        ax[1].bar(x + (j - 0.5) * w, 1e4 * s_.composition_impact_log, w * 0.92, color=PAL[j], label=lab)169    for a_ in ax[:2]:170        a_.axhline(0, color=INK2, lw=0.8); a_.set_xticks(x); a_.set_xticklabels(Gs, fontsize=7.5); a_.legend(fontsize=7)171    ax[0].set_ylabel("within-type (liquidity) term, %"); ax[0].set_title("(a) Within-type price change on impact")172    ax[1].set_ylabel("composition (selection) term, basis points"); ax[1].set_title("(b) Composition term on impact")173    for i, (g, s_) in enumerate(h.groupby("G")):174        ax[2].plot(s_.v, s_.hG_times_M, color=PAL[i], label=g)175    ax[2].axhline(1, color=INK2, lw=0.8, ls=":"); ax[2].set_xlabel("valuation $v$"); ax[2].set_ylabel("$h_G(v)\\,M(v)$"); ax[2].set_title("(c) Sign condition: hazard × mean residual valuation"); ax[2].legend(fontsize=7); ax[2].set_xlim(0.7, 1.6)176    save(fig, "fig09_observed_vs_latent_index")177178179def fig10_impulse_responses():180    scen = {"perm_A_minus20": r"permanent $A\times0.8$", "temp_A_minus20_1y": r"temporary $A\times0.8$ (1 year)", "perm_A_plus20": r"permanent $A\times1.2$"}181    P0 = rd("exp01_baseline_moments.csv").iloc[0].price_mean182    fig, ax = plt.subplots(2, 3, figsize=(9.6, 5.2))183    for i, (k, lab) in enumerate(scen.items()):184        d = rd(f"exp09_transition_{k}.csv"); d = d[d.t <= 4]185        ax[0, 0].plot(d.t, d.theta, color=PAL[i], label=lab); ax[0, 1].plot(d.t, 100 * (d.P_obs / P0 - 1), color=PAL[i])186        ax[0, 2].plot(d.t, d.Q, color=PAL[i]); ax[1, 0].plot(d.t, d.S, color=PAL[i]); ax[1, 1].plot(d.t, d.mean_tom_sold_months, color=PAL[i])187        ax[1, 2].plot(d.t, 1e4 * np.log(d.P_obs / d.P_fix), color=PAL[i])188    for a_, (t_, y_) in zip(ax.ravel(), [("(a) tightness $\\theta_t$", "buyers per listing"), ("(b) observed price index", "% from initial steady state"), ("(c) sales flow $Q_t$", "per year"), ("(d) inventory $S_t$", "listings"), ("(e) mean time on market of sales", "months"), ("(f) composition term $\\ln(P_t/P^{fix}_t)$", "basis points")]):189        a_.set_title(t_); a_.set_ylabel(y_); a_.set_xlabel("years after shock")190    ax[0, 0].legend(fontsize=7)191    save(fig, "fig10_shock_impulse_responses")192193194def fig11_welfare():195    w = rd("exp05_welfare_surface.csv"); b = rd("exp05_bargaining_power.csv")196    fig, ax = plt.subplots(1, 2, figsize=(7.6, 3.0))197    s = w[w.sellers == "heterogeneous"].pivot(index="alpha", columns="beta", values="welfare_total")198    im = ax[0].imshow(s.values, origin="lower", aspect="auto", cmap="Blues", extent=[s.columns.min(), s.columns.max(), s.index.min(), s.index.max()])199    ax[0].plot(1 - s.index.values, s.index.values, color=PAL[1], ls="--", label=r"Hosios $\beta=1-\alpha$")200    best = s.idxmax(axis=1); ax[0].scatter(best.values, best.index, color=INK, s=14, zorder=5, label="welfare-maximising $\\beta$")201    ax[0].set_xlabel(r"seller bargaining share $\beta$"); ax[0].set_ylabel(r"matching elasticity $\alpha$"); ax[0].set_title("(a) Steady-state flow welfare (heterogeneous sellers)"); ax[0].legend(fontsize=7, loc="upper right")202    fig.colorbar(im, ax=ax[0], pad=0.02, label="welfare flow")203    for i, (lab, g) in enumerate(b.groupby("sellers")):204        ax[1].plot(g.beta, g.welfare_total, color=PAL[i], label=lab); 205    ax[1].axvline(0.5, color=PAL[1], ls="--", lw=1, label="Hosios ($\\alpha=0.5$)")206    ax[1].set_xlabel(r"seller bargaining share $\beta$"); ax[1].set_ylabel("welfare flow"); ax[1].set_title("(b) Welfare and bargaining power, $\\alpha=0.5$"); ax[1].legend(fontsize=7)207    save(fig, "fig11_welfare_friction_contours")208209210def fig12_tornado():211    d = rd("exp12_sensitivity_oat.csv")212    fig, ax = plt.subplots(1, 2, figsize=(7.6, 3.4))213    for a_, key, lab in ((ax[0], "D_urgent", "urgency discount of the most urgent type (pp)"), (ax[1], "mean_tom_sold_months", "mean time on market of sales (months)")):214        g = d.groupby("parameter").agg(lo=(f"d_{key}", "min"), hi=(f"d_{key}", "max")).assign(span=lambda x: x.hi - x.lo).sort_values("span")215        if key == "D_urgent": g[["lo", "hi"]] *= 100216        y = np.arange(len(g)); a_.barh(y, g.hi, color=PAL[0], height=0.6, label="upper parameter value"); a_.barh(y, g.lo, color=PAL[1], height=0.6, label="lower parameter value")217        a_.set_yticks(y); a_.set_yticklabels([f"${p}$" if p in ("r", "A", "k") else p.replace("kappa_b", r"$\kappa_b$").replace("delta", r"$\delta$").replace("alpha", r"$\alpha$").replace("beta", r"$\beta$").replace("phi", r"$\phi$").replace("w0", "$w_0$").replace("sigma_w", r"$\sigma_w$").replace("c_max", "$c_{max}$") for p in g.index])218        a_.axvline(0, color=INK2, lw=0.8); a_.set_xlabel(f"change in {lab}"); a_.set_title(r"$\pm$25% one-at-a-time ($\sigma_w$: 0.05–0.20; $c_{max}$: 0.2–0.45)", fontsize=8)219    ax[0].legend(fontsize=7, loc="lower right")220    save(fig, "fig12_sensitivity_tornado")221222223def fig13_convergence():224    t = rd("exp12_convergence_types.csv"); dt = rd("exp12_convergence_dt.csv")225    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))226    ref = t[t.n_types == t.n_types.max()].iloc[0]227    for i, key in enumerate(("theta", "price_mean", "mean_tom_sold", "D_mean")):228        ax[0].semilogx(t.n_types, np.abs(t[key] - ref[key]) / abs(ref[key]) + 1e-16, marker="o", ms=3, color=PAL[i], label=key.replace("_", " "))229    ax[0].set_yscale("log"); ax[0].set_xlabel("number of seller types in the discretisation of $F$"); ax[0].set_ylabel("relative deviation from finest grid"); ax[0].set_title("(a) Convergence in the seller-type grid"); ax[0].legend(fontsize=7)230    ref2 = dt[dt.dt == dt.dt.min()].iloc[0]231    for i, key in enumerate(("P_obs_1y", "Q_1y", "S_1y", "theta_impact")):232        ax[1].loglog(dt.dt, np.abs(dt[key] - ref2[key]) / abs(ref2[key]) + 1e-16, marker="o", ms=3, color=PAL[i], label=key.replace("_", " "))233    ax[1].set_xlabel("time step $\\Delta t$ (years)"); ax[1].set_ylabel("relative deviation from finest step"); ax[1].set_title("(b) Convergence of the transition solver"); ax[1].legend(fontsize=7)234    save(fig, "fig13_numerical_convergence")235236237def fig14_posted_vs_nash():238    n = rd("exp11_types_nash.csv"); p_ = rd("exp11_types_posted.csv")239    fig, ax = plt.subplots(1, 2, figsize=(7.2, 2.9))240    ax[0].plot(n.c, n.pbar, color=PAL[0], marker="o", ms=3, label="Nash bargaining (expected price)"); ax[0].plot(p_.c, p_.pbar, color=PAL[1], marker="s", ms=3, label="posted price")241    ax[0].set_xlabel("carrying cost $c$"); ax[0].set_ylabel("price"); ax[0].set_title("(a) Price by seller type"); ax[0].legend(fontsize=7)242    ax[1].plot(n.c, n.eta, color=PAL[0], marker="o", ms=3); ax[1].plot(p_.c, p_.eta, color=PAL[1], marker="s", ms=3)243    ax[1].set_xlabel("carrying cost $c$"); ax[1].set_ylabel("sale hazard (per year)"); ax[1].set_title("(b) Sale hazard by seller type")244    save(fig, "fig14_posted_vs_nash")245246247ALL = [fig01_equilibrium, fig02_urgency_prices, fig03_urgency_hazard, fig04_discount_vs_arrivals, fig05_financing, fig06_volume_price,248       fig07_survival, fig08_price_distributions, fig09_index_selection, fig10_impulse_responses, fig11_welfare, fig12_tornado,249       fig13_convergence, fig14_posted_vs_nash]250251if __name__ == "__main__":252    import sys253    failed = []254    for f in ALL:255        try:256            f()257        except Exception as ex:258            failed.append((f.__name__, repr(ex))); print("FAILED", f.__name__, repr(ex))259    if failed:260        sys.exit(1)261