TeX 59%
Python 40.8%
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