SPB Git forge

spb/wp20_uqo

Public
1commits 1branches 0releases
2.8 MBsize
maindefault branch
yesterdaylast push
TeX 59% Python 40.8%
20.7 KB · 351 lines python
Raw Blame History
1"""Publication figures (vector PDF + PNG preview) from saved results only. Palette: validated default palette of the2dataviz reference (categorical fixed order; sequential one-hue blue ramp for ordered levels). Thin marks, direct labels,3no dual axes, recessive grid."""4from __future__ import annotations56import json7from pathlib import Path89import matplotlib10matplotlib.use("Agg")11import matplotlib.pyplot as plt  # noqa: E40212import numpy as np  # noqa: E40213import pandas as pd  # noqa: E4021415ROOT = Path(__file__).resolve().parents[2]16P = ROOT / "data" / "processed"17T = ROOT / "results" / "tables"18F = ROOT / "results" / "figures"19F.mkdir(parents=True, exist_ok=True)2021CAT = {"haiku": "#2a78d6", "sonnet": "#eb6834", "opus": "#1baf7a", "text": "#eda100", "visual": "#2a78d6", "gray": "#9a9892"}22SEQ = {"B0": "#9ec5f4", "B1": "#6da7ec", "B2": "#3987e5", "B3": "#1c5cab", "B4": "#0d366b"}23SCENE_COL = {"exterior_front": "#2a78d6", "exterior_rear_or_side": "#6da7ec", "exterior_aerial_or_street": "#9ec5f4",24             "living_or_dining": "#eb6834", "kitchen": "#1baf7a", "bedroom": "#eda100", "bathroom": "#e87ba4",25             "basement": "#008300", "yard_pool_or_deck": "#6250d6", "garage": "#e34948", "other": "#c3c2b7",26             "floor_plan_or_document": "#9a9892", "mechanical_or_utility": "#52514e", "unusable": "#0b0b0b"}27plt.rcParams.update({"font.family": "serif", "font.size": 9, "axes.spines.top": False, "axes.spines.right": False,28                     "axes.grid": True, "grid.color": "#e6e5e1", "grid.linewidth": 0.5, "axes.axisbelow": True,29                     "legend.frameon": False, "figure.dpi": 150, "pdf.fonttype": 42})30LEVEL_LAB = {"B0": "B0 roll only", "B1": "B1 + text", "B2": "B2 + images", "B3": "B3 multimodal", "B4": "B4 + admin recon."}313233def save(fig, name):34    fig.tight_layout()35    fig.savefig(F / f"{name}.pdf"); fig.savefig(F / f"{name}.png", dpi=170)36    plt.close(fig)373839def tiers():40    return [t for t in ["haiku", "sonnet", "opus"] if (P / f"images_{t}.parquet").exists()]414243# ---------------------------------------------------------------- F2 sample composition44def fig_sample():45    frame = pd.read_parquet(P / "frame.parquet"); smp = pd.read_parquet(P / "sample.parquet")46    fig, axes = plt.subplots(1, 3, figsize=(7.2, 2.4))47    for ax, col, title in zip(axes, ["cma", "age_band", "price_tier"], ["Metropolitan area", "Building age (years)", "Asking-price quartile"]):48        f = frame[col].astype(str).value_counts(normalize=True); s = smp[col].astype(str).value_counts(normalize=True)49        cats = list(f.sort_values(ascending=False).index) if col == "cma" else sorted(f.index)50        x = np.arange(len(cats)); w = 0.3851        ax.bar(x - w / 2, [f.get(c, 0) * 100 for c in cats], w, color=CAT["gray"], label="frame (n = %d)" % len(frame))52        ax.bar(x + w / 2, [s.get(c, 0) * 100 for c in cats], w, color=CAT["haiku"], label="sample (n = %d)" % len(smp))53        ax.set_xticks(x); ax.set_xticklabels(cats, rotation=30, ha="right"); ax.set_title(title, fontsize=9); ax.set_ylabel("% of listings")54    axes[0].legend(loc="upper right", fontsize=7)55    save(fig, "f02_sample_composition")565758# ---------------------------------------------------------------- F3 scene by position (MNAR evidence)59def fig_scene_position(tier="haiku"):60    img = pd.read_parquet(P / f"images_{tier}.parquet"); img = img[img.ok]61    ct = pd.crosstab(img.image_index, img.scene, normalize="index")62    order = [c for c in SCENE_COL if c in ct.columns] + [c for c in ct.columns if c not in SCENE_COL]63    ct = ct[order]64    fig, ax = plt.subplots(figsize=(7.2, 3.0))65    bottom = np.zeros(len(ct))66    for c in ct.columns:67        ax.bar(ct.index + 1, ct[c] * 100, bottom=bottom, color=SCENE_COL.get(c, "#c3c2b7"), width=0.8, label=c.replace("_", " "), linewidth=0.5, edgecolor="white")68        bottom += ct[c].values * 10069    ax.set_xlabel("Position of the photograph in the listing (seller order)"); ax.set_ylabel("% of photographs")70    ax.set_xticks(ct.index + 1); ax.set_ylim(0, 100)71    ax.legend(ncol=3, fontsize=6.5, loc="upper center", bbox_to_anchor=(0.5, -0.22))72    save(fig, "f03_scene_by_position")737475# ---------------------------------------------------------------- F4 scene presence vs K76def fig_scene_vs_k():77    df = pd.read_csv(T / "exp3_image_count.csv")78    fig, ax = plt.subplots(figsize=(4.6, 2.8))79    for tier in df.tier.unique():80        d = df[df.tier == tier]81        for col, lab, ls in [("share_with_kitchen", "kitchen", "-"), ("share_with_bathroom", "bathroom", "--"), ("share_with_basement", "basement", ":")]:82            ax.plot(d.K, d[col] * 100, ls, color=CAT[tier], lw=1.6, marker="o", ms=3.5, label=f"{tier}: {lab}" if tier == df.tier.unique()[0] or True else None)83    ax.set_xlabel("Images analysed per listing (K, seller order)"); ax.set_ylabel("% of listings with ≥ 1 photo of the room")84    ax.set_ylim(0, 100); ax.legend(fontsize=6.5, ncol=2)85    save(fig, "f04_scene_presence_vs_k")868788# ---------------------------------------------------------------- F5 agreement dot plot89def fig_agreement():90    df = pd.read_csv(T / "exp1_agreement.csv")91    df = df[df.channel == "visual"]92    attrs = [a for a in df.attribute.unique() if "kitchen" not in a]93    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2), sharey=True)94    y = np.arange(len(attrs))95    for ax, metric, title in zip(axes, ["coverage", "accuracy"], ["Coverage (informative outputs)", "Agreement, informative cases"]):96        for tier in df.tier.unique():97            d = df[df.tier == tier].set_index("attribute").reindex(attrs)98            ax.scatter(d[metric], y, color=CAT[tier], s=22, label=tier, zorder=3)99        ax.set_xlim(0, 1.02); ax.set_title(title, fontsize=8.5); ax.set_xlabel("share")100    axes[0].set_yticks(y); axes[0].set_yticklabels([a.replace(" vs ", "\nvs ").replace("∈", "in") for a in attrs], fontsize=6.5)101    axes[1].legend(fontsize=7, loc="lower left")102    save(fig, "f05_agreement")103104105# ---------------------------------------------------------------- F6 confusion heatmaps106def fig_confusion():107    tier = json.load(open(ROOT / "results" / "raw" / "exp_agreement.json"))["exp1_confusion_tier"]108    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))109    for ax, name, title in zip(axes, ["roof", "storeys"], ["Roof covering: broker field (rows) vs image (cols)", "Storeys: roll (rows) vs image (cols)"]):110        ct = pd.read_csv(T / f"exp1_confusion_{name}_{tier}.csv", index_col=0)111        ct = ct.loc[:, [c for c in ct.columns if c != "not_applicable"]]112        norm = ct.div(ct.sum(axis=1), axis=0)113        im = ax.imshow(norm.values, cmap="Blues", vmin=0, vmax=1, aspect="auto")114        ax.set_xticks(range(ct.shape[1])); ax.set_xticklabels([c.replace("_", "\n") for c in ct.columns], fontsize=6.5)115        ax.set_yticks(range(ct.shape[0])); ax.set_yticklabels(ct.index, fontsize=7)116        for i in range(ct.shape[0]):117            for j in range(ct.shape[1]):118                ax.text(j, i, int(ct.values[i, j]), ha="center", va="center", fontsize=6.5, color="white" if norm.values[i, j] > 0.6 else "#0b0b0b")119        ax.set_title(title, fontsize=8); ax.grid(False)120    save(fig, "f06_confusion")121122123# ---------------------------------------------------------------- F7 cross-model agreement124def fig_cross_model():125    f = T / "exp4_cross_model.csv"126    if not f.exists():127        return128    df = pd.read_csv(f)129    cat = df[df.alpha_nominal.notna()].copy()130    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0), gridspec_kw={"width_ratios": [1.3, 1]})131    ax = axes[0]132    y = np.arange(len(cat))133    ax.barh(y, cat.alpha_nominal, color=CAT["haiku"], height=0.6)134    ax.set_yticks(y); ax.set_yticklabels([a.replace("_", " ") for a in cat.attribute], fontsize=7)135    ax.axvline(0.67, color="#52514e", lw=0.8, ls="--"); ax.axvline(0.8, color="#52514e", lw=0.8, ls=":")136    ax.set_xlabel("Krippendorff's α (nominal) across extractor tiers"); ax.set_xlim(0, 1)137    ax = axes[1]138    ts = tiers()139    if len(ts) >= 2:140        a = pd.read_parquet(P / f"props_{ts[0]}_K10.parquet").set_index("uid"); b = pd.read_parquet(P / f"props_{ts[1]}_K10.parquet").set_index("uid")141        c = a[["condition_composite"]].join(b[["condition_composite"]], lsuffix="_a", rsuffix="_b").dropna()142        ax.scatter(c.iloc[:, 0] + np.random.default_rng(1).normal(0, 0.03, len(c)), c.iloc[:, 1] + np.random.default_rng(2).normal(0, 0.03, len(c)),143                   s=5, alpha=0.35, color=CAT["sonnet"], edgecolor="none")144        ax.plot([1, 5], [1, 5], color="#52514e", lw=0.8)145        ax.set_xlabel(f"composite condition, {ts[0]}"); ax.set_ylabel(f"composite condition, {ts[1]}")146        ax.set_title(f"r = {np.corrcoef(c.iloc[:, 0], c.iloc[:, 1])[0, 1]:.2f}, n = {len(c)}", fontsize=8)147    save(fig, "f07_cross_model")148149150# ---------------------------------------------------------------- F8 calibration151def fig_calibration():152    df = pd.read_csv(T / "exp12_calibration.csv"); sel = pd.read_csv(T / "exp12_selective.csv")153    fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9))154    ax = axes[0]155    for tier in df.tier.unique():156        d = df[df.tier == tier]157        ax.plot(d.conf_mean, d.roof_acc, "-o", color=CAT[tier], ms=4, lw=1.4, label=f"{tier}: roof covering")158        ax.plot(d.conf_mean, d.clad_acc, "--s", color=CAT[tier], ms=3.5, lw=1.2, label=f"{tier}: cladding")159    ax.plot([0.5, 1], [0.5, 1], color="#52514e", lw=0.8)160    ax.set_xlabel("stated confidence (per image, bin mean)"); ax.set_ylabel("agreement with broker field"); ax.set_xlim(0.5, 1); ax.set_ylim(0.5, 1.02)161    ax.legend(fontsize=6.5)162    ax = axes[1]163    for tier in sel.tier.unique():164        d = sel[sel.tier == tier]165        ax.plot(d.kept_share * 100, d.roof_accuracy, "-o", color=CAT[tier], ms=4, lw=1.4, label=tier)166        for _, r in d.iterrows():167            ax.annotate(f"≥{r.threshold:.1f}", (r.kept_share * 100, r.roof_accuracy), fontsize=6, xytext=(2, 3), textcoords="offset points")168    ax.set_xlabel("% of exterior images retained (confidence threshold)"); ax.set_ylabel("roof agreement among retained")169    ax.legend(fontsize=7)170    save(fig, "f08_calibration")171172173# ---------------------------------------------------------------- F9 decomposition by level and age band174def fig_decomposition():175    df = pd.read_csv(T / "exp5_levels_summary.csv"); main = "sonnet" if "sonnet" in df.tier.values else df.tier.iloc[0]176    d = df[(df.tier == main) & (df.anchor == "B")].set_index("level").reindex(["B0", "B1", "B2", "B3", "B4"])177    dec = pd.read_csv(T / "exp5_decomposition.csv"); dec = dec[(dec.tier == main) & (dec.level == "B3") & (dec.group == "age_band")]178    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))179    ax = axes[0]180    x = np.arange(len(d))181    L = d["V_median"] * d["L_share_median"]; I = d["V_median"] - L182    ax.bar(x, L / 1000, color="#9ec5f4", label="land (roll value, indexed)")183    ax.bar(x, I / 1000, bottom=L / 1000, color=[SEQ[l] for l in d.index], label="depreciated improvements")184    for i, (l, v) in enumerate(zip(d.index, d["V_median"])):185        ax.text(i, v / 1000 + 8, f"${v/1000:,.0f}k", ha="center", fontsize=7)186    ax.set_xticks(x); ax.set_xticklabels([LEVEL_LAB[l] for l in d.index], fontsize=6.5, rotation=20, ha="right")187    ax.set_ylabel("median cost indication (k$)"); ax.legend(fontsize=6.5, loc="lower right")188    ax = axes[1]189    dec = dec.set_index("value").reindex(["0-10", "11-25", "26-50", "51-75", "76+"])190    x = np.arange(len(dec)); w = 0.26191    ax.bar(x - w, dec["L"] / 1000, w, color="#9ec5f4", label="land")192    ax.bar(x, dec["RCN"] / 1000, w, color="#1c5cab", label="RCN (incl. soft, incentive)")193    ax.bar(x + w, dec["improvements"] / 1000, w, color="#eb6834", label="depreciated improvements")194    ax.set_xticks(x); ax.set_xticklabels(dec.index); ax.set_xlabel("building age (years)"); ax.set_ylabel("median (k$), B3")195    ax.legend(fontsize=6.5)196    save(fig, "f09_decomposition")197198199# ---------------------------------------------------------------- F10 ratio distributions by level200def fig_ratios():201    main = "sonnet" if (P / "cost_sonnet_B3.parquet").exists() else "haiku"202    fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9), sharey=False)203    for ax, col, title, xlim in zip(axes, ["ratio_price", "ratio_assessor"],204                                    ["Cost indication / asking price", "Depreciated improvements / assessor building value"], [(0, 2), (0, 3)]):205        data, labels = [], []206        for lvl in ["B0", "B1", "B2", "B3", "B4"]:207            f = P / f"cost_{main}_{lvl}.parquet"208            if f.exists():209                d = pd.read_parquet(f)[col].replace([np.inf, -np.inf], np.nan).dropna()210                data.append(d.clip(*xlim)); labels.append(LEVEL_LAB[lvl])211        parts = ax.violinplot(data, showmedians=True, showextrema=False, widths=0.85)212        for b, lvl in zip(parts["bodies"], ["B0", "B1", "B2", "B3", "B4"][:len(data)]):213            b.set_facecolor(SEQ[lvl]); b.set_alpha(0.85); b.set_edgecolor("white")214        parts["cmedians"].set_color("#0b0b0b"); parts["cmedians"].set_linewidth(1)215        ax.axhline(1.0, color="#52514e", lw=0.8, ls="--")216        ax.set_xticks(range(1, len(labels) + 1)); ax.set_xticklabels(labels, fontsize=6.5, rotation=20, ha="right"); ax.set_ylim(*xlim)217        ax.set_title(title, fontsize=8)218    save(fig, "f10_ratio_distributions")219220221# ---------------------------------------------------------------- F11 sensitivity tornado222def fig_sensitivity():223    df = pd.read_csv(T / "exp7_8_sensitivity.csv")224    base = df[df.scenario.str.startswith("baseline")].iloc[0]225    d = df[~df.scenario.str.startswith("baseline") & ~df.scenario.str.startswith("no depreciation")].copy()226    d["delta"] = (d["V_median"] / base["V_median"] - 1) * 100227    d = d.reindex(d.delta.abs().sort_values().index)228    fig, ax = plt.subplots(figsize=(6.8, 4.2))229    y = np.arange(len(d))230    ax.barh(y, d.delta, color=np.where(d.delta >= 0, "#2a78d6", "#eb6834"), height=0.62)231    ax.set_yticks(y); ax.set_yticklabels(d.scenario, fontsize=7)232    ax.axvline(0, color="#0b0b0b", lw=0.8)233    for yi, v in zip(y, d.delta):234        inside = v < -15235        ax.text(v + (0.6 if v >= 0 else (1.0 if inside else -0.6)), yi, f"{v:+.1f}%", va="center",236                ha="left" if (v >= 0 or inside) else "right", fontsize=6.5, color="white" if inside else "#0b0b0b")237    ax.set_xlabel("change in median cost indication vs baseline (B3, anchor B), %")238    ax.set_xlim(min(d.delta.min() * 1.05, -10), max(d.delta.max() * 1.6, 10))239    save(fig, "f11_sensitivity")240241242# ---------------------------------------------------------------- F12 effect of visual grade and condition243def fig_grade_condition():244    main = "sonnet" if (P / "exp6_effects_sonnet.parquet").exists() else "haiku"245    eff = pd.read_parquet(P / f"exp6_effects_{main}.parquet")246    fig, axes = plt.subplots(1, 2, figsize=(7.2, 2.9))247    ax = axes[0]248    order = ["basic", "standard", "upgraded", "luxury"]249    data = [eff.loc[eff.grade == g, "delta_impr_pct"].clip(-80, 150) for g in order]250    bp = ax.boxplot(data, widths=0.55, showfliers=False, patch_artist=True, medianprops={"color": "#0b0b0b"})251    for patch, c in zip(bp["boxes"], ["#9ec5f4", "#6da7ec", "#3987e5", "#1c5cab"]):252        patch.set_facecolor(c); patch.set_edgecolor("white")253    ax.set_xticks(range(1, 5)); ax.set_xticklabels([f"{g}\n(n={int((eff.grade == g).sum())})" for g in order], fontsize=7)254    ax.axhline(0, color="#52514e", lw=0.8, ls="--"); ax.set_ylabel("Δ depreciated improvements, B2 vs B0 (%)"); ax.set_title("by visual finish grade", fontsize=8)255    ax = axes[1]256    ax.scatter(eff.condition + np.random.default_rng(3).normal(0, 0.03, len(eff)), eff.delta_D_pts, s=6, alpha=0.4, color="#eb6834", edgecolor="none")257    ax.set_xlabel("composite visual condition (1–5)"); ax.set_ylabel("Δ depreciation, B2 vs B0 (percentage points)")258    ax.axhline(0, color="#52514e", lw=0.8, ls="--")259    save(fig, "f12_grade_condition_effects")260261262# ---------------------------------------------------------------- F13 Monte Carlo intervals263def fig_mc():264    main = "sonnet" if (P / "exp14_mc_sonnet.parquet").exists() else "haiku"265    mc = pd.read_parquet(P / f"exp14_mc_{main}.parquet")266    fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.0))267    ax = axes[0]268    ax.hist(mc.rel_width.clip(0, 1) * 100, bins=40, color="#2a78d6", edgecolor="white", linewidth=0.4)269    ax.axvline(mc.rel_width.median() * 100, color="#0b0b0b", lw=1); ax.set_xlabel("relative width of the 10–90 % interval (% of median)"); ax.set_ylabel("listings")270    ax = axes[1]271    rng = np.random.default_rng(4); idx = rng.choice(mc.index, size=min(250, len(mc)), replace=False)272    s = mc.loc[idx].sort_values("V_mc_median")273    x = np.arange(len(s))274    ax.fill_between(x, s.V_p10 / 1e3, s.V_p90 / 1e3, color="#9ec5f4", alpha=0.8, label="10–90 % interval")275    ax.plot(x, s.V_mc_median / 1e3, color="#1c5cab", lw=1, label="median indication")276    ax.scatter(x, s.price / 1e3, s=5, color="#eb6834", label="asking price", zorder=3)277    ax.set_yscale("log"); ax.set_xlabel("listings (random subset of 250, sorted by indication)"); ax.set_ylabel("k$ (log scale)")278    ax.legend(fontsize=6.5, loc="upper left")279    save(fig, "f13_monte_carlo")280281282# ---------------------------------------------------------------- F14 subgroup heatmap283def fig_subgroups():284    main = "sonnet" if (P / "cost_sonnet_B3.parquet").exists() else "haiku"285    d = pd.read_parquet(P / f"cost_{main}_B3.parquet")286    d["ape"] = (d.ratio_price - 1).abs() * 100287    piv = d.pivot_table(index="age_band", columns="price_tier", values="ape", aggfunc="median", observed=True).reindex(["0-10", "11-25", "26-50", "51-75", "76+"])288    cnt = d.pivot_table(index="age_band", columns="price_tier", values="ape", aggfunc="size", observed=True).reindex(piv.index)289    fig, ax = plt.subplots(figsize=(4.6, 2.9))290    im = ax.imshow(piv.values, cmap="Blues", aspect="auto")291    ax.set_xticks(range(piv.shape[1])); ax.set_xticklabels(piv.columns); ax.set_yticks(range(piv.shape[0])); ax.set_yticklabels(piv.index)292    for i in range(piv.shape[0]):293        for j in range(piv.shape[1]):294            ax.text(j, i, f"{piv.values[i, j]:.0f}%\n(n={int(cnt.values[i, j])})", ha="center", va="center", fontsize=6.5,295                    color="white" if piv.values[i, j] > np.nanpercentile(piv.values, 60) else "#0b0b0b")296    ax.set_xlabel("asking-price quartile"); ax.set_ylabel("building age (years)"); ax.set_title("median |cost indication / price − 1|, B3", fontsize=8); ax.grid(False)297    save(fig, "f14_subgroups")298299300# ---------------------------------------------------------------- F15 recorded sales301def fig_sales():302    main = "sonnet"303    sales = pd.read_parquet(P / "frame_sales_after_listing.parquet").sort_values("date").drop_duplicates("uid", keep="last").set_index("uid")304    pts = []305    for lvfile in [P / f"levels_{main}_sold.parquet", P / f"levels_{main}.parquet"]:306        if not lvfile.exists():307            continue308        import sys309        sys.path.insert(0, str(ROOT))310        from src.cost_model import cost311        L = pd.read_parquet(lvfile); d = L[L.level == "B3"].set_index("uid"); d = d[d.index.isin(sales.index)]312        if len(d):313            out = cost.cost_indication(d); pts.append(pd.DataFrame({"V": out.V_cost, "sale": sales.loc[d.index, "amount"], "ask": d.price}))314    if not pts:315        return316    s = pd.concat(pts)317    fig, ax = plt.subplots(figsize=(3.8, 3.4))318    ax.scatter(s.sale / 1e3, s.V / 1e3, s=10, color="#2a78d6", alpha=0.7, edgecolor="none", label="cost indication (B3)")319    ax.scatter(s.sale / 1e3, s.ask / 1e3, s=10, color="#eb6834", alpha=0.6, marker="x", label="asking price")320    lim = [s.sale.min() / 1e3 * 0.8, s.sale.max() / 1e3 * 1.1]321    ax.plot(lim, lim, color="#52514e", lw=0.8); ax.set_xscale("log"); ax.set_yscale("log")322    ax.set_xlabel("recorded sale price after listing (k$, log)"); ax.set_ylabel("k$ (log)"); ax.legend(fontsize=6.5)323    ax.set_title(f"n = {len(s)} units sold after listing", fontsize=8)324    save(fig, "f15_recorded_sales")325326327# ---------------------------------------------------------------- F16 image count sensitivity328def fig_k_sensitivity():329    df = pd.read_csv(T / "exp3_image_count.csv")330    fig, ax = plt.subplots(figsize=(4.6, 2.8))331    for tier in df.tier.unique():332        d = df[df.tier == tier]333        ax.plot(d.K, d.grade_available * 100, "-o", color=CAT[tier], ms=4, lw=1.5, label=f"{tier}: finish grade available")334        ax.plot(d.K, d.grade_stable_vs_full * 100, "--s", color=CAT[tier], ms=3.5, lw=1.2, label=f"{tier}: grade equal to full-K grade")335    ax.set_xlabel("images analysed per listing (K)"); ax.set_ylabel("% of listings"); ax.set_ylim(0, 102); ax.legend(fontsize=6.5)336    save(fig, "f16_image_count")337338339def all_figures():340    for fn in [fig_sample, fig_scene_position, fig_scene_vs_k, fig_agreement, fig_confusion, fig_cross_model, fig_calibration,341               fig_decomposition, fig_ratios, fig_sensitivity, fig_grade_condition, fig_mc, fig_subgroups, fig_sales, fig_k_sensitivity]:342        try:343            fn()344            print("ok ", fn.__name__)345        except Exception as e:  # noqa: BLE001346            print("FAIL", fn.__name__, repr(e))347348349if __name__ == "__main__":350    all_figures()351