WP46 exhibits: representative-spec heatmaps, trimmed Pareto axis with materiality band
2 changed files +21 −5
modified
scripts/build_paper.py
+13 −3
@@ -112,9 +112,19 @@ def main() -> None: | ||
| 112 | 112 | if cc.exists(): |
| 113 | 113 | TB.crosscheck_table(json.load(open(cc)), tdir / "tab_crosscheck.tex") |
| 114 | 114 | # --- figures |
| 115 | − F.ratio_heatmap(per_symbol, 1, tierA, F.FIG / "fig_heatmap_h1_tierA.pdf", "Tier A, h = 1") | |
| 116 | − F.ratio_heatmap(per_symbol, 22, tierA, F.FIG / "fig_heatmap_h22_tierA.pdf", "Tier A, h = 22") | |
| 117 | − F.ratio_heatmap(per_symbol, 1, tierB + [s for s in specs_all if s.startswith("garch11__normal")], F.FIG / "fig_heatmap_h1_tierB.pdf", "Tier B, h = 1") | |
| 115 | + # representative specifications (Student-t where available) ordered by family: readable heatmaps (mandate §5-6) | |
| 116 | + rep0 = {} | |
| 117 | + for sp in specs_all: | |
| 118 | + mid, dist, _ = sp.split("__"); pref = {"t": 0, "normal": 1, "empirical": 2, "ged": 3, "skewt": 4}[dist] | |
| 119 | + if mid not in rep0 or pref < rep0[mid][0]: | |
| 120 | + rep0[mid] = (pref, sp) | |
| 121 | + fam_order = ["baseline", "classical", "asymmetric", "power", "fractional", "component", "regime-switching", "score-driven", "realized"] | |
| 122 | + repA = sorted([v[1] for v in rep0.values() if F.fam(v[1]) != "realized" and v[1].split("__")[0] not in F.EXT_PARENT], key=lambda s: (fam_order.index(F.fam(s)), s)) | |
| 123 | + repB = sorted([v[1] for v in rep0.values() if F.fam(v[1]) == "realized" and v[1].split("__")[0] not in F.EXT_PARENT], key=lambda s: s) | |
| 124 | + F.ratio_heatmap(per_symbol, 1, repA, F.FIG / "fig_heatmap_h1_tierA.pdf", "Tier A (representative specifications), h = 1") | |
| 125 | + F.ratio_heatmap(per_symbol, 22, repA, F.FIG / "fig_heatmap_h22_tierA.pdf", "Tier A (representative specifications), h = 22") | |
| 126 | + F.ratio_heatmap(per_symbol, 1, repB + [s for s in specs_all if s.startswith("garch11__normal")], F.FIG / "fig_heatmap_h1_tierB.pdf", "Tier B (+ GARCH(1,1)-N reference), h = 1") | |
| 127 | + F.ratio_heatmap(per_symbol, 1, tierA, F.FIG / "fig_heatmap_h1_tierA_full.pdf", "Tier A, all innovation laws, h = 1", vmin=0.7, vmax=1.3) | |
| 118 | 128 | F.mcs_matrix(per_symbol, 1, "A", F.FIG / "fig_mcs_h1_tierA.pdf", "90% MCS, Tier A, h = 1") |
| 119 | 129 | F.mcs_matrix(per_symbol, 22, "A", F.FIG / "fig_mcs_h22_tierA.pdf", "90% MCS, Tier A, h = 22") |
| 120 | 130 | F.mcs_matrix(per_symbol, 1, "B", F.FIG / "fig_mcs_h1_tierB.pdf", "90% MCS, Tier B (+ GARCH(1,1)-N), h = 1") |
modified
src/wp46/reporting/figures.py
+8 −2
@@ -172,10 +172,15 @@ def horizon_profile(pooled: pd.DataFrame, specs: list[str], path: Path, cls: str | ||
| 172 | 172 | # ----------------------------------------------------------------------------- 6. Pareto: accuracy vs cost / failure |
| 173 | 173 | def pareto(pooled_h1: pd.DataFrame, accounting: pd.DataFrame, path: Path, xcol: str = "fit_time_median", xlabel: str = "Median refit time (s, log scale)") -> Path: |
| 174 | 174 | d = pooled_h1.merge(accounting.drop(columns=[c for c in ("family",) if c in accounting]), on="spec_id", how="inner") |
| 175 | + d = d[np.isfinite(d.mean_ratio)] | |
| 176 | + ymax = min(float(np.nanpercentile(d.mean_ratio, 95)) * 1.05, 2.5) # degenerate outliers never dictate the axis (mandate §6) | |
| 177 | + n_clip = int((d.mean_ratio > ymax).sum()); d = d.assign(mean_ratio_plot=np.minimum(d.mean_ratio, ymax)) | |
| 175 | 178 | fig, ax = plt.subplots(figsize=(ps.W2, 3.2)) |
| 176 | 179 | for f in sorted(d.family.unique()): |
| 177 | 180 | s = d[d.family == f] |
| 178 | − ax.scatter(s[xcol], s.mean_ratio, marker=ps.FAMILY_MARKER.get(f, "o"), color=ps.FAMILY_COLOR.get(f, ps.INK3), s=22, label=ps.FAMILY_LABEL.get(f, f), zorder=3, edgecolor="white", linewidth=0.4) | |
| 181 | + ax.scatter(s[xcol], s.mean_ratio_plot, marker=ps.FAMILY_MARKER.get(f, "o"), color=ps.FAMILY_COLOR.get(f, ps.INK3), s=22, label=ps.FAMILY_LABEL.get(f, f), zorder=3, edgecolor="white", linewidth=0.4) | |
| 182 | + if n_clip: | |
| 183 | + ax.text(0.01, 0.98, f"{n_clip} specification(s) above {ymax:.2f} shown at the top edge", transform=ax.transAxes, fontsize=6, va="top", color=ps.INK2) | |
| 179 | 184 | # Pareto frontier (min ratio for cost <= x) |
| 180 | 185 | dd = d.sort_values(xcol); best = np.minimum.accumulate(dd.mean_ratio.values) |
| 181 | 186 | ax.step(dd[xcol], best, where="post", color=ps.INK, lw=0.8, ls="--", label="Pareto frontier") |
@@ -183,7 +188,8 @@ def pareto(pooled_h1: pd.DataFrame, accounting: pd.DataFrame, path: Path, xcol: | ||
| 183 | 188 | ax.annotate(spec_label(r.spec_id), (r[xcol], r.mean_ratio), xytext=(3, 3), textcoords="offset points", fontsize=5.5, color=ps.INK2) |
| 184 | 189 | if "time" in xcol: |
| 185 | 190 | ax.set_xscale("log") |
| 186 | − ax.axhline(1, color=ps.INK3, lw=0.7); ax.set_xlabel(xlabel); ax.set_ylabel("Pooled QLIKE ratio to GARCH(1,1)-N, h = 1") | |
| 191 | + ax.axhline(1, color=ps.INK3, lw=0.7); ax.axhspan(0.98, 1.02, color="#f2f2f0", zorder=0) # ±2 % materiality band | |
| 192 | + ax.set_xlabel(xlabel); ax.set_ylabel("Pooled QLIKE ratio to GARCH(1,1)-N, h = 1"); ax.set_ylim(min(0.8, float(d.mean_ratio_plot.min()) * 0.98), ymax * 1.02) | |
| 187 | 193 | ax.legend(ncol=3, fontsize=6, loc="upper right") |
| 188 | 194 | ps.save(fig, path); _audit(path.name, ["results/tables/pooled_*.csv", "results/tables/accounting_*.csv"], len(d)) |
| 189 | 195 | return path |
| 190 | 196 | |