diff --git a/paper/src/chapters/04-results.tex b/paper/src/chapters/04-results.tex index 725dc7f..3a26d49 100644 --- a/paper/src/chapters/04-results.tex +++ b/paper/src/chapters/04-results.tex @@ -1,7 +1,7 @@ \section{Results} \begin{figure}[ht] \centering - \input{chapters/figures/supra.tex} + \input{chapters/figures/supra/supra.tex} \caption{Evolution of price distributions over experiment steps. The heatmap illustrates the density of price offerings. This is an early baseline simulation which demonstrates supra-competitive price-setting in deep learning agents such as SAC as can be clearly seen by the high density at the highest available price.} \label{fig:supra_heatmap} \end{figure} diff --git a/paper/src/chapters/figures/.gitignore b/paper/src/chapters/figures/.gitignore new file mode 100644 index 0000000..a467b0f --- /dev/null +++ b/paper/src/chapters/figures/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.pdf-view-restore diff --git a/paper/src/chapters/figures/results/generated/first_sweep_headline_summary.json b/paper/src/chapters/figures/results/generated/legacy/first_sweep_headline_summary.json similarity index 100% rename from paper/src/chapters/figures/results/generated/first_sweep_headline_summary.json rename to paper/src/chapters/figures/results/generated/legacy/first_sweep_headline_summary.json diff --git a/paper/src/chapters/figures/results/generated/first_sweep_tier_alpha_deltas.csv b/paper/src/chapters/figures/results/generated/legacy/first_sweep_tier_alpha_deltas.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/first_sweep_tier_alpha_deltas.csv rename to paper/src/chapters/figures/results/generated/legacy/first_sweep_tier_alpha_deltas.csv diff --git a/paper/src/chapters/figures/results/generated/first_sweep_tier_alpha_mode_summary.csv b/paper/src/chapters/figures/results/generated/legacy/first_sweep_tier_alpha_mode_summary.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/first_sweep_tier_alpha_mode_summary.csv rename to paper/src/chapters/figures/results/generated/legacy/first_sweep_tier_alpha_mode_summary.csv diff --git a/paper/src/chapters/figures/results/generated/first_sweep_tier_mode_summary.csv b/paper/src/chapters/figures/results/generated/legacy/first_sweep_tier_mode_summary.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/first_sweep_tier_mode_summary.csv rename to paper/src/chapters/figures/results/generated/legacy/first_sweep_tier_mode_summary.csv diff --git a/paper/src/chapters/figures/results/generated/first_sweep_top_configs.csv b/paper/src/chapters/figures/results/generated/legacy/first_sweep_top_configs.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/first_sweep_top_configs.csv rename to paper/src/chapters/figures/results/generated/legacy/first_sweep_top_configs.csv diff --git a/paper/src/chapters/figures/results/generated/plots/first_sweep_tier_revenue.pdf b/paper/src/chapters/figures/results/generated/legacy/plots/first_sweep_tier_revenue.pdf similarity index 99% rename from paper/src/chapters/figures/results/generated/plots/first_sweep_tier_revenue.pdf rename to paper/src/chapters/figures/results/generated/legacy/plots/first_sweep_tier_revenue.pdf index a019aee..a278726 100644 Binary files a/paper/src/chapters/figures/results/generated/plots/first_sweep_tier_revenue.pdf and b/paper/src/chapters/figures/results/generated/legacy/plots/first_sweep_tier_revenue.pdf differ diff --git a/paper/src/chapters/figures/results/generated/plots/ppo_alpha_curves.pdf b/paper/src/chapters/figures/results/generated/legacy/plots/ppo_alpha_curves.pdf similarity index 99% rename from paper/src/chapters/figures/results/generated/plots/ppo_alpha_curves.pdf rename to paper/src/chapters/figures/results/generated/legacy/plots/ppo_alpha_curves.pdf index d012154..5f97b19 100644 Binary files a/paper/src/chapters/figures/results/generated/plots/ppo_alpha_curves.pdf and b/paper/src/chapters/figures/results/generated/legacy/plots/ppo_alpha_curves.pdf differ diff --git a/paper/src/chapters/figures/results/generated/plots/ppo_delta_curves.pdf b/paper/src/chapters/figures/results/generated/legacy/plots/ppo_delta_curves.pdf similarity index 99% rename from paper/src/chapters/figures/results/generated/plots/ppo_delta_curves.pdf rename to paper/src/chapters/figures/results/generated/legacy/plots/ppo_delta_curves.pdf index b42b144..3f1be83 100644 Binary files a/paper/src/chapters/figures/results/generated/plots/ppo_delta_curves.pdf and b/paper/src/chapters/figures/results/generated/legacy/plots/ppo_delta_curves.pdf differ diff --git a/paper/src/chapters/figures/results/generated/plots/ppo_tradeoff_scatter.pdf b/paper/src/chapters/figures/results/generated/legacy/plots/ppo_tradeoff_scatter.pdf similarity index 99% rename from paper/src/chapters/figures/results/generated/plots/ppo_tradeoff_scatter.pdf rename to paper/src/chapters/figures/results/generated/legacy/plots/ppo_tradeoff_scatter.pdf index 34da3ac..49ecc66 100644 Binary files a/paper/src/chapters/figures/results/generated/plots/ppo_tradeoff_scatter.pdf and b/paper/src/chapters/figures/results/generated/legacy/plots/ppo_tradeoff_scatter.pdf differ diff --git a/paper/src/chapters/figures/results/generated/ppo_alpha_deltas.csv b/paper/src/chapters/figures/results/generated/legacy/ppo_alpha_deltas.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/ppo_alpha_deltas.csv rename to paper/src/chapters/figures/results/generated/legacy/ppo_alpha_deltas.csv diff --git a/paper/src/chapters/figures/results/generated/ppo_alpha_mode_summary.csv b/paper/src/chapters/figures/results/generated/legacy/ppo_alpha_mode_summary.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/ppo_alpha_mode_summary.csv rename to paper/src/chapters/figures/results/generated/legacy/ppo_alpha_mode_summary.csv diff --git a/paper/src/chapters/figures/results/generated/ppo_headline_summary.json b/paper/src/chapters/figures/results/generated/legacy/ppo_headline_summary.json similarity index 100% rename from paper/src/chapters/figures/results/generated/ppo_headline_summary.json rename to paper/src/chapters/figures/results/generated/legacy/ppo_headline_summary.json diff --git a/paper/src/chapters/figures/results/generated/ppo_overall_mode_summary.csv b/paper/src/chapters/figures/results/generated/legacy/ppo_overall_mode_summary.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/ppo_overall_mode_summary.csv rename to paper/src/chapters/figures/results/generated/legacy/ppo_overall_mode_summary.csv diff --git a/paper/src/chapters/figures/results/generated/ppo_pairwise_win_rates.csv b/paper/src/chapters/figures/results/generated/legacy/ppo_pairwise_win_rates.csv similarity index 100% rename from paper/src/chapters/figures/results/generated/ppo_pairwise_win_rates.csv rename to paper/src/chapters/figures/results/generated/legacy/ppo_pairwise_win_rates.csv diff --git a/paper/src/chapters/figures/results/first_sweep_tier_revenue.tex b/paper/src/chapters/figures/results/includes/legacy/first_sweep_tier_revenue.tex similarity index 54% rename from paper/src/chapters/figures/results/first_sweep_tier_revenue.tex rename to paper/src/chapters/figures/results/includes/legacy/first_sweep_tier_revenue.tex index f319a81..52a61b4 100644 --- a/paper/src/chapters/figures/results/first_sweep_tier_revenue.tex +++ b/paper/src/chapters/figures/results/includes/legacy/first_sweep_tier_revenue.tex @@ -1 +1 @@ -\includegraphics[width=0.99\linewidth]{chapters/figures/results/generated/plots/first_sweep_tier_revenue.pdf} +\includegraphics[width=0.99\linewidth]{chapters/figures/results/generated/legacy/plots/first_sweep_tier_revenue.pdf} diff --git a/paper/src/chapters/figures/results/ppo_alpha_curves.tex b/paper/src/chapters/figures/results/includes/legacy/ppo_alpha_curves.tex similarity index 58% rename from paper/src/chapters/figures/results/ppo_alpha_curves.tex rename to paper/src/chapters/figures/results/includes/legacy/ppo_alpha_curves.tex index 2496584..b4f6618 100644 --- a/paper/src/chapters/figures/results/ppo_alpha_curves.tex +++ b/paper/src/chapters/figures/results/includes/legacy/ppo_alpha_curves.tex @@ -1 +1 @@ -\includegraphics[width=0.98\linewidth]{chapters/figures/results/generated/plots/ppo_alpha_curves.pdf} +\includegraphics[width=0.98\linewidth]{chapters/figures/results/generated/legacy/plots/ppo_alpha_curves.pdf} diff --git a/paper/src/chapters/figures/results/ppo_delta_curves.tex b/paper/src/chapters/figures/results/includes/legacy/ppo_delta_curves.tex similarity index 58% rename from paper/src/chapters/figures/results/ppo_delta_curves.tex rename to paper/src/chapters/figures/results/includes/legacy/ppo_delta_curves.tex index 8c4eec0..2b37f92 100644 --- a/paper/src/chapters/figures/results/ppo_delta_curves.tex +++ b/paper/src/chapters/figures/results/includes/legacy/ppo_delta_curves.tex @@ -1 +1 @@ -\includegraphics[width=0.98\linewidth]{chapters/figures/results/generated/plots/ppo_delta_curves.pdf} +\includegraphics[width=0.98\linewidth]{chapters/figures/results/generated/legacy/plots/ppo_delta_curves.pdf} diff --git a/paper/src/chapters/figures/results/ppo_tradeoff_scatter.tex b/paper/src/chapters/figures/results/includes/legacy/ppo_tradeoff_scatter.tex similarity index 56% rename from paper/src/chapters/figures/results/ppo_tradeoff_scatter.tex rename to paper/src/chapters/figures/results/includes/legacy/ppo_tradeoff_scatter.tex index 0117970..7b795d1 100644 --- a/paper/src/chapters/figures/results/ppo_tradeoff_scatter.tex +++ b/paper/src/chapters/figures/results/includes/legacy/ppo_tradeoff_scatter.tex @@ -1 +1 @@ -\includegraphics[width=0.88\linewidth]{chapters/figures/results/generated/plots/ppo_tradeoff_scatter.pdf} +\includegraphics[width=0.88\linewidth]{chapters/figures/results/generated/legacy/plots/ppo_tradeoff_scatter.pdf} diff --git a/paper/src/chapters/figures/results/plot_results.py b/paper/src/chapters/figures/results/plot_results.py index 0b80926..0476948 100644 --- a/paper/src/chapters/figures/results/plot_results.py +++ b/paper/src/chapters/figures/results/plot_results.py @@ -16,7 +16,7 @@ from process_ppo_benchmark import run as run_ppo_benchmark def _output_dir() -> Path: - return Path(__file__).resolve().parent / "generated" + return Path(__file__).resolve().parent / "generated" / "legacy" def _plot_dir() -> Path: diff --git a/paper/src/chapters/figures/results/plot_wandb_export.py b/paper/src/chapters/figures/results/plot_wandb_export.py new file mode 100644 index 0000000..cda7faf --- /dev/null +++ b/paper/src/chapters/figures/results/plot_wandb_export.py @@ -0,0 +1,658 @@ +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Iterable + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.ticker import FuncFormatter +import numpy as np +import pandas as pd + + +def _load_tikzplotlib(): + def _patch_webcolors() -> None: + try: + import webcolors + + if hasattr(webcolors, "CSS3_HEX_TO_NAMES"): + return + css3 = getattr(webcolors, "CSS3", "css3") + webcolors.CSS3_HEX_TO_NAMES = { + webcolors.name_to_hex(name, spec=css3): name + for name in webcolors.names(spec=css3) + } + except Exception: + return + + _patch_webcolors() + + try: + from matplotlib.legend import Legend + + if not hasattr(Legend, "_ncol") and hasattr(Legend, "_ncols"): + Legend._ncol = property(lambda self: self._ncols) + except Exception: + pass + + try: + import tikzplotlib as module + + return module, None + except Exception: + pass + + try: + from matplotlib.backends import backend_pgf + + if not hasattr(backend_pgf, "common_texification") and hasattr( + backend_pgf, "_tex_escape" + ): + backend_pgf.common_texification = backend_pgf._tex_escape + + _patch_webcolors() + + import tikzplotlib as module + + return module, None + except Exception as exc: + return None, exc + + +TIKZPLOTLIB, TIKZPLOTLIB_IMPORT_ERROR = _load_tikzplotlib() + + +def _default_output_dir() -> Path: + return Path(__file__).resolve().parent / "generated" / "wandb" + + +def _default_plot_dir(output_dir: Path) -> Path: + return output_dir / "plots" + + +def _sanitize(key: str) -> str: + return key.replace("/", "_").replace("-", "_") + + +def _configure_style() -> None: + plt.rcParams.update( + { + "font.family": "serif", + "font.size": 10, + "axes.titlesize": 10, + "axes.labelsize": 9, + "legend.fontsize": 8, + "xtick.labelsize": 8, + "ytick.labelsize": 8, + "figure.dpi": 220, + "savefig.dpi": 320, + "axes.spines.top": False, + "axes.spines.right": False, + "axes.grid": True, + "grid.alpha": 0.22, + } + ) + + +def _fmt_thousands(value: float, _: int) -> str: + return f"{int(value):,}" + + +def _coerce_numeric(frame: pd.DataFrame, columns: Iterable[str]) -> None: + for column in columns: + if column in frame.columns: + frame[column] = pd.to_numeric(frame[column], errors="coerce") + + +def _extract_alpha(frame: pd.DataFrame) -> pd.Series: + if "study/alpha" in frame.columns: + return pd.to_numeric(frame["study/alpha"], errors="coerce") + if "alpha" in frame.columns: + return pd.to_numeric(frame["alpha"], errors="coerce") + return pd.Series(np.nan, index=frame.index, dtype=float) + + +def _extract_mode(frame: pd.DataFrame) -> pd.Series: + if "study/mode" in frame.columns: + mode = frame["study/mode"].astype(str).str.strip().str.lower() + mapping = { + "baseline": "baseline", + "no_robust": "baseline", + "defended": "defended", + "robust": "defended", + } + return mode.map(mapping).fillna("") + + if "study/no_robust" in frame.columns: + no_robust = pd.to_numeric(frame["study/no_robust"], errors="coerce").fillna(0.0) + return pd.Series( + np.where(no_robust > 0.5, "baseline", "defended"), + index=frame.index, + dtype="object", + ) + + if "no_robust" in frame.columns: + no_robust = ( + frame["no_robust"].astype(str).str.lower().isin({"1", "true", "yes"}) + ) + return pd.Series( + np.where(no_robust, "baseline", "defended"), + index=frame.index, + dtype="object", + ) + + return pd.Series("", index=frame.index, dtype="object") + + +def _prepare_frame(frame: pd.DataFrame, include_non_finished: bool) -> pd.DataFrame: + data = frame.copy() + if not include_non_finished and "State" in data.columns: + data = data[data["State"].astype(str).str.lower() == "finished"].copy() + + data["alpha"] = _extract_alpha(data) + data["mode"] = _extract_mode(data) + data = data[data["mode"].isin({"baseline", "defended"})] + data = data[data["alpha"].notna()] + + _coerce_numeric( + data, + [ + "eval/revenue_mean", + "eval/reward_mean", + "eval/coi_level_mean", + "eval/coi_leakage_mean", + "eval/volatility_mean", + "eval/revenue_std", + "eval/reward_std", + "eval/margin_mean", + "train/agent_prob", + "train/alpha_adv", + "lambda_coi", + "ambiguity_radius", + "n_products", + ], + ) + + return data.sort_values(["alpha", "mode"]).reset_index(drop=True) + + +def _summary_by_alpha_mode(frame: pd.DataFrame, metrics: list[str]) -> pd.DataFrame: + agg_spec: dict[str, tuple[str, str]] = {"runs": ("mode", "size")} + for metric in metrics: + safe = _sanitize(metric) + agg_spec[f"{safe}_mean"] = (metric, "mean") + agg_spec[f"{safe}_std"] = (metric, "std") + + return ( + frame.groupby(["alpha", "mode"], as_index=False) + .agg(**agg_spec) + .sort_values(["alpha", "mode"]) + .reset_index(drop=True) + ) + + +def _delta_by_alpha(summary: pd.DataFrame, metrics: list[str]) -> pd.DataFrame: + rows: list[dict[str, float]] = [] + for alpha, alpha_group in summary.groupby("alpha", sort=True): + defended = alpha_group[alpha_group["mode"] == "defended"] + baseline = alpha_group[alpha_group["mode"] == "baseline"] + if defended.empty or baseline.empty: + continue + + row: dict[str, float] = { + "alpha": float(alpha), + "runs_defended": float(defended["runs"].iloc[0]), + "runs_baseline": float(baseline["runs"].iloc[0]), + } + for metric in metrics: + safe = _sanitize(metric) + defended_value = float(defended[f"{safe}_mean"].iloc[0]) + baseline_value = float(baseline[f"{safe}_mean"].iloc[0]) + delta = defended_value - baseline_value + row[f"{safe}_defended"] = defended_value + row[f"{safe}_baseline"] = baseline_value + row[f"{safe}_delta"] = delta + row[f"{safe}_delta_pct"] = ( + np.nan if baseline_value == 0 else 100.0 * delta / baseline_value + ) + rows.append(row) + + return pd.DataFrame(rows) + + +def _summary_by_parameter( + frame: pd.DataFrame, parameter: str, metrics: list[str] +) -> pd.DataFrame: + defended = frame[frame["mode"] == "defended"].copy() + defended = defended[defended[parameter].notna()].copy() + agg_spec: dict[str, tuple[str, str]] = {"runs": ("mode", "size")} + for metric in metrics: + safe = _sanitize(metric) + agg_spec[f"{safe}_mean"] = (metric, "mean") + agg_spec[f"{safe}_std"] = (metric, "std") + + return ( + defended.groupby(["alpha", parameter], as_index=False) + .agg(**agg_spec) + .sort_values(["alpha", parameter]) + .reset_index(drop=True) + ) + + +def _save_table(frame: pd.DataFrame, path: Path) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + frame.to_csv(path, index=False) + return path + + +def _save_figure(fig: plt.Figure, pdf_path: Path, export_tikz: bool) -> list[Path]: + pdf_path.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(pdf_path, bbox_inches="tight") + written = [pdf_path] + + if export_tikz: + if TIKZPLOTLIB is None: + raise RuntimeError( + "tikzplotlib import failed. Install/upgrade tikzplotlib and matplotlib-compatible dependencies. " + f"Original error: {TIKZPLOTLIB_IMPORT_ERROR}" + ) + + try: + from matplotlib.legend import Legend + from matplotlib.lines import Line2D + + for legend in fig.findobj(Legend): + if not hasattr(legend, "_ncol") and hasattr(legend, "_ncols"): + setattr(legend, "_ncol", legend._ncols) + if not hasattr(legend, "legendHandles") and hasattr( + legend, "legend_handles" + ): + setattr(legend, "legendHandles", legend.legend_handles) + + for line in fig.findobj(Line2D): + if hasattr(line, "_us_dashSeq"): + continue + if not hasattr(line, "_dash_pattern"): + continue + dash_pattern = getattr(line, "_dash_pattern") + if not isinstance(dash_pattern, tuple) or len(dash_pattern) != 2: + continue + setattr(line, "_us_dashOffset", dash_pattern[0]) + setattr(line, "_us_dashSeq", dash_pattern[1]) + except Exception: + pass + + tikz_path = pdf_path.with_suffix(".tikz.tex") + TIKZPLOTLIB.save(str(tikz_path), figure=fig) + written.append(tikz_path) + + plt.close(fig) + return written + + +def _plot_alpha_curves( + alpha_mode: pd.DataFrame, out_dir: Path, export_tikz: bool +) -> list[Path]: + fig, axes = plt.subplots(2, 2, figsize=(9.3, 6.4), constrained_layout=True) + mode_colors = {"baseline": "#4C72B0", "defended": "#C44E52"} + mode_labels = {"baseline": "Baseline", "defended": "Defended"} + + panels = [ + ("eval_revenue_mean", "Mean Episode Revenue", "Revenue"), + ("eval_reward_mean", "Mean Episode Reward", "Reward"), + ("eval_coi_leakage_mean", "Mean COI Leakage", "COI Leakage"), + ("eval_volatility_mean", "Mean Price Volatility", "Volatility"), + ] + + for ax, (metric_prefix, title, ylabel) in zip(axes.flat, panels): + mean_col = f"{metric_prefix}_mean" + std_col = f"{metric_prefix}_std" + for mode in ("baseline", "defended"): + sub = alpha_mode[alpha_mode["mode"] == mode].sort_values("alpha") + if sub.empty: + continue + x = sub["alpha"].to_numpy(dtype=float) + y = sub[mean_col].to_numpy(dtype=float) + ax.plot( + x, + y, + marker="o", + linewidth=1.8, + markersize=4, + color=mode_colors[mode], + label=mode_labels[mode], + ) + sigma = sub[std_col].fillna(0.0).to_numpy(dtype=float) + ax.fill_between( + x, + y - sigma, + y + sigma, + color=mode_colors[mode], + alpha=0.14, + linewidth=0, + ) + + ax.set_title(title) + ax.set_xlabel(r"Contamination $\alpha$") + ax.set_ylabel(ylabel) + ax.set_xticks(sorted(alpha_mode["alpha"].unique())) + if metric_prefix in {"eval_revenue_mean", "eval_reward_mean"}: + ax.yaxis.set_major_formatter(FuncFormatter(_fmt_thousands)) + + handles, labels = axes.flat[0].get_legend_handles_labels() + fig.legend(handles, labels, ncol=2, loc="upper center", bbox_to_anchor=(0.5, 1.02)) + return _save_figure(fig, out_dir / "wandb_alpha_curves.pdf", export_tikz) + + +def _plot_delta_curves( + deltas: pd.DataFrame, out_dir: Path, export_tikz: bool +) -> list[Path]: + fig, axes = plt.subplots(2, 1, figsize=(8.6, 6.0), constrained_layout=True) + deltas = deltas.sort_values("alpha") + x = deltas["alpha"].to_numpy(dtype=float) + + top_metrics = [ + ("eval_revenue_mean_delta_pct", "Revenue", "#4C72B0"), + ("eval_reward_mean_delta_pct", "Reward", "#8172B3"), + ] + for col, label, color in top_metrics: + axes[0].plot( + x, + deltas[col].to_numpy(dtype=float), + marker="o", + linewidth=1.8, + markersize=4, + color=color, + label=label, + ) + axes[0].axhline(0.0, color="#444444", linewidth=1.0, linestyle="--") + axes[0].set_title("Defended Minus Baseline Delta by Contamination") + axes[0].set_ylabel("Delta (%)") + axes[0].set_xlabel(r"Contamination $\alpha$") + axes[0].set_xticks(x) + axes[0].legend(loc="lower left") + + bottom_metrics = [ + ("eval_coi_leakage_mean_delta_pct", "COI Leakage", "#55A868"), + ("eval_volatility_mean_delta_pct", "Volatility", "#DD8452"), + ] + for col, label, color in bottom_metrics: + axes[1].plot( + x, + deltas[col].to_numpy(dtype=float), + marker="o", + linewidth=1.8, + markersize=4, + color=color, + label=label, + ) + axes[1].axhline(0.0, color="#444444", linewidth=1.0, linestyle="--") + axes[1].set_ylabel("Delta (%)") + axes[1].set_xlabel(r"Contamination $\alpha$") + axes[1].set_xticks(x) + axes[1].legend(loc="lower left") + + return _save_figure(fig, out_dir / "wandb_delta_curves.pdf", export_tikz) + + +def _plot_tradeoff_scatter( + deltas: pd.DataFrame, out_dir: Path, export_tikz: bool +) -> list[Path]: + fig, ax = plt.subplots(figsize=(6.4, 5.2), constrained_layout=True) + data = deltas.sort_values("alpha") + x = data["eval_coi_leakage_mean_delta_pct"].to_numpy(dtype=float) + y = data["eval_revenue_mean_delta_pct"].to_numpy(dtype=float) + alphas = data["alpha"].to_numpy(dtype=float) + + scatter = ax.scatter( + x, + y, + c=alphas, + cmap="viridis", + s=72, + edgecolor="#222222", + linewidth=0.5, + ) + for x_i, y_i, alpha in zip(x, y, alphas): + ax.annotate( + rf"$\alpha={alpha:.2f}$", + (x_i, y_i), + textcoords="offset points", + xytext=(5, 4), + fontsize=8, + ) + + ax.axhline(0.0, color="#555555", linewidth=1.0, linestyle="--") + ax.axvline(0.0, color="#555555", linewidth=1.0, linestyle="--") + ax.set_xlabel("COI Leakage Delta (%)") + ax.set_ylabel("Revenue Delta (%)") + ax.set_title("Defended Tradeoff Frontier") + cbar = fig.colorbar(scatter, ax=ax) + cbar.set_label(r"Contamination $\alpha$") + + return _save_figure(fig, out_dir / "wandb_tradeoff_scatter.pdf", export_tikz) + + +def _plot_reward_robustness( + alpha_mode: pd.DataFrame, out_dir: Path, export_tikz: bool +) -> list[Path]: + fig, ax = plt.subplots(figsize=(7.6, 4.5), constrained_layout=True) + mode_colors = {"baseline": "#4C72B0", "defended": "#C44E52"} + mode_labels = {"baseline": "Baseline", "defended": "Defended"} + + for mode in ("baseline", "defended"): + sub = alpha_mode[alpha_mode["mode"] == mode].sort_values("alpha") + x = sub["alpha"].to_numpy(dtype=float) + y = sub["eval_reward_mean_std"].fillna(0.0).to_numpy(dtype=float) + ax.plot( + x, + y, + marker="o", + linewidth=1.8, + markersize=4, + color=mode_colors[mode], + label=mode_labels[mode], + ) + + ax.set_title("Reward Robustness Across Contamination") + ax.set_xlabel(r"Contamination $\alpha$") + ax.set_ylabel("Reward Std Across Runs") + ax.set_xticks(sorted(alpha_mode["alpha"].unique())) + ax.yaxis.set_major_formatter(FuncFormatter(_fmt_thousands)) + ax.legend(loc="upper left") + return _save_figure(fig, out_dir / "wandb_reward_robustness.pdf", export_tikz) + + +def _plot_parameter_sensitivity( + summary: pd.DataFrame, + parameter: str, + out_name: str, + out_dir: Path, + export_tikz: bool, +) -> list[Path]: + fig, axes = plt.subplots(1, 2, figsize=(10.0, 4.2), constrained_layout=True) + values = sorted(summary[parameter].dropna().unique()) + cmap = plt.get_cmap("viridis") + colors = [cmap(i) for i in np.linspace(0.1, 0.9, len(values))] + + panels = [ + ("eval_revenue_mean", "Revenue"), + ("eval_coi_leakage_mean", "COI Leakage"), + ] + for ax, (metric_prefix, ylabel) in zip(axes, panels): + mean_col = f"{metric_prefix}_mean" + std_col = f"{metric_prefix}_std" + for value, color in zip(values, colors): + sub = summary[summary[parameter] == value].sort_values("alpha") + if sub.empty: + continue + x = sub["alpha"].to_numpy(dtype=float) + y = sub[mean_col].to_numpy(dtype=float) + sigma = sub[std_col].fillna(0.0).to_numpy(dtype=float) + ax.plot( + x, + y, + marker="o", + linewidth=1.6, + markersize=3.6, + color=color, + label=f"{parameter}={value:.2f}", + ) + ax.fill_between( + x, y - sigma, y + sigma, color=color, alpha=0.10, linewidth=0 + ) + + ax.set_xlabel(r"Contamination $\alpha$") + ax.set_ylabel(ylabel) + ax.set_xticks(sorted(summary["alpha"].unique())) + if metric_prefix == "eval_revenue_mean": + ax.yaxis.set_major_formatter(FuncFormatter(_fmt_thousands)) + + axes[0].set_title(f"{parameter} Sensitivity (Defended)") + axes[1].set_title("Leakage Side-Effect") + handles, labels = axes[0].get_legend_handles_labels() + fig.legend( + handles, + labels, + ncol=max(1, len(values) // 2), + loc="upper center", + bbox_to_anchor=(0.5, 1.06), + ) + + return _save_figure(fig, out_dir / f"{out_name}.pdf", export_tikz) + + +def _plot_delta_summary( + deltas: pd.DataFrame, out_dir: Path, export_tikz: bool +) -> list[Path]: + data = deltas.sort_values("alpha") + x = np.arange(len(data)) + labels = [f"{alpha:.1f}" for alpha in data["alpha"].to_numpy(dtype=float)] + + fig, axes = plt.subplots(1, 3, figsize=(11.0, 3.8), constrained_layout=True) + panels = [ + ("eval_revenue_mean_delta_pct", "Revenue Delta (%)", "#4C72B0"), + ("eval_reward_mean_delta_pct", "Reward Delta (%)", "#8172B3"), + ("eval_coi_leakage_mean_delta_pct", "COI Leakage Delta (%)", "#55A868"), + ] + for ax, (column, title, color) in zip(axes, panels): + values = data[column].to_numpy(dtype=float) + ax.bar(x, values, color=color, alpha=0.85) + ax.axhline(0.0, color="#444444", linewidth=1.0, linestyle="--") + ax.set_xticks(x) + ax.set_xticklabels(labels) + ax.set_xlabel(r"$\alpha$") + ax.set_title(title) + + return _save_figure(fig, out_dir / "wandb_delta_summary.pdf", export_tikz) + + +def build_artifacts( + input_path: Path, + output_dir: Path, + plot_dir: Path, + include_non_finished: bool, + export_tikz: bool, +) -> list[Path]: + raw = pd.read_csv(input_path) + frame = _prepare_frame(raw, include_non_finished=include_non_finished) + + metrics = [ + metric + for metric in ( + "eval/revenue_mean", + "eval/reward_mean", + "eval/coi_level_mean", + "eval/coi_leakage_mean", + "eval/volatility_mean", + "eval/margin_mean", + "train/agent_prob", + "train/alpha_adv", + ) + if metric in frame.columns + ] + + alpha_mode = _summary_by_alpha_mode(frame, metrics) + deltas = _delta_by_alpha(alpha_mode, metrics) + lambda_summary = _summary_by_parameter(frame, "lambda_coi", metrics) + radius_summary = _summary_by_parameter(frame, "ambiguity_radius", metrics) + + output_dir.mkdir(parents=True, exist_ok=True) + plot_dir.mkdir(parents=True, exist_ok=True) + + written: list[Path] = [] + written.append(_save_table(alpha_mode, output_dir / "wandb_alpha_mode_summary.csv")) + written.append(_save_table(deltas, output_dir / "wandb_alpha_deltas.csv")) + written.append( + _save_table(lambda_summary, output_dir / "wandb_lambda_alpha_summary.csv") + ) + written.append( + _save_table(radius_summary, output_dir / "wandb_radius_alpha_summary.csv") + ) + + written.extend(_plot_alpha_curves(alpha_mode, plot_dir, export_tikz)) + written.extend(_plot_delta_curves(deltas, plot_dir, export_tikz)) + written.extend(_plot_tradeoff_scatter(deltas, plot_dir, export_tikz)) + written.extend(_plot_reward_robustness(alpha_mode, plot_dir, export_tikz)) + written.extend( + _plot_parameter_sensitivity( + summary=lambda_summary, + parameter="lambda_coi", + out_name="wandb_lambda_sensitivity", + out_dir=plot_dir, + export_tikz=export_tikz, + ) + ) + written.extend( + _plot_parameter_sensitivity( + summary=radius_summary, + parameter="ambiguity_radius", + out_name="wandb_radius_sensitivity", + out_dir=plot_dir, + export_tikz=export_tikz, + ) + ) + written.extend(_plot_delta_summary(deltas, plot_dir, export_tikz)) + return written + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Generate W&B sweep visualizations for PHANTOM results" + ) + parser.add_argument( + "--input", type=Path, required=True, help="Path to W&B export CSV" + ) + parser.add_argument("--output-dir", type=Path, default=_default_output_dir()) + parser.add_argument("--plot-dir", type=Path, default=None) + parser.add_argument("--include-non-finished", action="store_true") + parser.add_argument( + "--export-tikz", + action="store_true", + help="Export matplotlib figures to TikZ via tikzplotlib", + ) + args = parser.parse_args() + + _configure_style() + plot_dir = ( + args.plot_dir + if args.plot_dir is not None + else _default_plot_dir(args.output_dir) + ) + + outputs = build_artifacts( + input_path=args.input, + output_dir=args.output_dir, + plot_dir=plot_dir, + include_non_finished=bool(args.include_non_finished), + export_tikz=bool(args.export_tikz), + ) + for path in outputs: + print(path) + + +if __name__ == "__main__": + main() diff --git a/paper/src/chapters/figures/results/process_all_results.py b/paper/src/chapters/figures/results/process_all_results.py index 2dc2a4d..78ca65f 100644 --- a/paper/src/chapters/figures/results/process_all_results.py +++ b/paper/src/chapters/figures/results/process_all_results.py @@ -8,7 +8,7 @@ from process_ppo_benchmark import run as run_ppo_benchmark def _default_output_dir() -> Path: - return Path(__file__).resolve().parent / "generated" + return Path(__file__).resolve().parent / "generated" / "legacy" def main() -> None: diff --git a/paper/src/chapters/figures/results/process_first_sweep.py b/paper/src/chapters/figures/results/process_first_sweep.py index fd650b1..0e62525 100644 --- a/paper/src/chapters/figures/results/process_first_sweep.py +++ b/paper/src/chapters/figures/results/process_first_sweep.py @@ -18,7 +18,7 @@ def _default_input() -> Path: def _default_output_dir() -> Path: - return Path(__file__).resolve().parent / "generated" + return Path(__file__).resolve().parent / "generated" / "legacy" def _sanitize(key: str) -> str: diff --git a/paper/src/chapters/figures/results/process_ppo_benchmark.py b/paper/src/chapters/figures/results/process_ppo_benchmark.py index dbced6a..85f48b2 100644 --- a/paper/src/chapters/figures/results/process_ppo_benchmark.py +++ b/paper/src/chapters/figures/results/process_ppo_benchmark.py @@ -18,7 +18,7 @@ def _default_input() -> Path: def _default_output_dir() -> Path: - return Path(__file__).resolve().parent / "generated" + return Path(__file__).resolve().parent / "generated" / "legacy" def _sanitize(key: str) -> str: diff --git a/paper/src/chapters/figures/process_supra.py b/paper/src/chapters/figures/supra/process_supra.py similarity index 100% rename from paper/src/chapters/figures/process_supra.py rename to paper/src/chapters/figures/supra/process_supra.py diff --git a/paper/src/chapters/figures/supra.csv b/paper/src/chapters/figures/supra/supra.csv similarity index 100% rename from paper/src/chapters/figures/supra.csv rename to paper/src/chapters/figures/supra/supra.csv diff --git a/paper/src/chapters/figures/supra.tex b/paper/src/chapters/figures/supra/supra.tex similarity index 95% rename from paper/src/chapters/figures/supra.tex rename to paper/src/chapters/figures/supra/supra.tex index 290a2a1..9e815b7 100644 --- a/paper/src/chapters/figures/supra.tex +++ b/paper/src/chapters/figures/supra/supra.tex @@ -21,7 +21,7 @@ surf, shader=flat, mesh/check=false % Disable check to rely on empty lines - ] table [col sep=comma, x=step, y=price, z=density] {chapters/figures/supra_data.csv}; + ] table [col sep=comma, x=step, y=price, z=density] {chapters/figures/supra/supra_data.csv}; \end{axis} \end{tikzpicture} diff --git a/paper/src/chapters/figures/supra_data.csv b/paper/src/chapters/figures/supra/supra_data.csv similarity index 99% rename from paper/src/chapters/figures/supra_data.csv rename to paper/src/chapters/figures/supra/supra_data.csv index f005217..6216cac 100644 --- a/paper/src/chapters/figures/supra_data.csv +++ b/paper/src/chapters/figures/supra/supra_data.csv @@ -4038,4 +4038,3 @@ step,price,density 4000,146.51098761558535,0.0 4000,147.9065925693512,0.0 4000,149.30219752311706,10.0 - diff --git a/paper/src/mirrors/genpop/04-results.tex b/paper/src/mirrors/genpop/04-results.tex index 5950b35..af37e8c 100644 --- a/paper/src/mirrors/genpop/04-results.tex +++ b/paper/src/mirrors/genpop/04-results.tex @@ -1,7 +1,7 @@ \section{Results} \begin{figure}[ht] \centering - \input{chapters/figures/supra.tex} + \input{chapters/figures/supra/supra.tex} \caption{Evolution of price distributions over experiment steps. The heatmap illustrates the density of price offerings. This is an early baseline simulation which demonstrates supra-competitive price-setting in deep learning agents such as SAC as can be clearly seen by the high density at the highest available price.} \label{fig:supra_heatmap} \end{figure}