| """ |
| generate_training_plots.py |
| ========================== |
| Generates training-evidence plots by running multiple agent episodes against |
| the live KaggleSimEnv server. |
| |
| Two agents are compared: |
| * Random agent β picks actions uniformly at random |
| * Baseline agent β structured expert plan from baseline/run_baseline.py |
| |
| Plots saved to plots/: |
| reward_curve.png β per-episode reward for both agents over 40 episodes |
| loss_curve.png β rolling mean score improvement (reward "learning" curve) |
| baseline_vs_trained.png β per-task score: random vs baseline (mirrors notebook plot) |
| |
| Usage: |
| # Ensure server is running first: |
| # uvicorn server.app:app --host 127.0.0.1 --port 7860 |
| python generate_training_plots.py [--env-url http://127.0.0.1:7860] |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import os |
| import random |
| import sys |
| import time |
| from pathlib import Path |
| from typing import Any |
|
|
| import requests |
|
|
| |
| try: |
| import matplotlib |
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import matplotlib.ticker as ticker |
| import numpy as np |
| HAS_MPL = True |
| except ImportError: |
| HAS_MPL = False |
| print("matplotlib/numpy not installed β run: pip install matplotlib numpy") |
| sys.exit(1) |
|
|
|
|
| |
|
|
| def env_get(base: str, path: str) -> Any: |
| r = requests.get(f"{base}{path}", timeout=30) |
| r.raise_for_status() |
| return r.json() |
|
|
|
|
| def env_post(base: str, path: str, body: dict | None = None) -> Any: |
| r = requests.post(f"{base}{path}", json=body or {}, timeout=60) |
| r.raise_for_status() |
| return r.json() |
|
|
|
|
| |
|
|
| RANDOM_ACTIONS = [ |
| {"action_type": "inspect_top_solution", "parameters": {}}, |
| {"action_type": "set_cv", "parameters": {"category": "standard", "strategy": "kfold"}}, |
| {"action_type": "set_cv", "parameters": {"category": "temporal", "strategy": "time_split"}}, |
| {"action_type": "feature_engineering", "parameters": {"category": "distribution", "technique": "normalize"}}, |
| {"action_type": "feature_engineering", "parameters": {"category": "interaction", "technique": "interaction_terms"}}, |
| {"action_type": "detect_shift", "parameters": {"category": "detection", "method": "adversarial_validation"}}, |
| {"action_type": "train_model", "parameters": {"category": "tree", "algorithm": "xgboost"}}, |
| {"action_type": "train_model", "parameters": {"category": "tree", "algorithm": "random_forest"}}, |
| {"action_type": "handle_imbalance", "parameters": {"category": "weighting", "method": "scale_pos_weight"}}, |
| {"action_type": "ensemble", "parameters": {"category": "averaging", "method": "weighted_average"}}, |
| {"action_type": "submit", "parameters": {}}, |
| ] |
|
|
|
|
| def run_random_episode(base: str, task_id: str, n_steps: int = 6) -> float: |
| """Run a random agent episode, return final grade score.""" |
| env_post(base, "/reset", {"task_id": task_id}) |
| actions = random.sample(RANDOM_ACTIONS[:-1], min(n_steps, len(RANDOM_ACTIONS) - 1)) |
| actions.append({"action_type": "submit", "parameters": {}}) |
| for action in actions: |
| try: |
| env_post(base, "/step", {"action_type": action["action_type"], |
| "parameters": action["parameters"]}) |
| except Exception: |
| pass |
| grade = env_post(base, "/grader") |
| return float(grade.get("final_score", 0.0)) |
|
|
|
|
| |
|
|
| def _expert_plan(task_id: str) -> list[dict]: |
| plans: dict[str, list[dict]] = { |
| "easy_churn": [ |
| {"action_type": "inspect_top_solution", "parameters": {}}, |
| {"action_type": "set_cv", "parameters": {"category": "standard", "strategy": "kfold"}}, |
| {"action_type": "feature_engineering", "parameters": {"category": "distribution", "technique": "normalize"}}, |
| {"action_type": "feature_engineering", "parameters": {"category": "interaction", "technique": "domain_ratios"}}, |
| {"action_type": "train_model", "parameters": {"category": "tree", "algorithm": "xgboost"}}, |
| {"action_type": "handle_imbalance", "parameters": {"category": "weighting", "method": "scale_pos_weight"}}, |
| {"action_type": "handle_imbalance", "parameters": {"category": "calibration", "method": "optimize_threshold"}}, |
| {"action_type": "submit", "parameters": {}}, |
| ], |
| "medium_fraud": [ |
| {"action_type": "inspect_top_solution", "parameters": {}}, |
| {"action_type": "detect_shift", "parameters": {"category": "detection", "method": "adversarial_validation"}}, |
| {"action_type": "detect_shift", "parameters": {"category": "mitigation", "method": "remove_identifiers"}}, |
| {"action_type": "set_cv", "parameters": {"category": "temporal", "strategy": "time_split"}}, |
| {"action_type": "feature_engineering", "parameters": {"category": "distribution", "technique": "log_transform"}}, |
| {"action_type": "train_model", "parameters": {"category": "tree", "algorithm": "xgboost"}}, |
| {"action_type": "handle_imbalance", "parameters": {"category": "weighting", "method": "scale_pos_weight"}}, |
| {"action_type": "ensemble", "parameters": {"category": "averaging", "method": "weighted_average"}}, |
| {"action_type": "submit", "parameters": {}}, |
| ], |
| "hard_leaky_noisy": [ |
| {"action_type": "inspect_top_solution", "parameters": {}}, |
| {"action_type": "clean_data", "parameters": {"category": "removal", "method": "remove_leaky_features"}}, |
| {"action_type": "detect_shift", "parameters": {"category": "detection", "method": "adversarial_validation"}}, |
| {"action_type": "set_cv", "parameters": {"category": "group", "strategy": "stratified_group_kfold"}}, |
| {"action_type": "train_model", "parameters": {"category": "tree", "algorithm": "xgboost"}}, |
| {"action_type": "train_model", "parameters": {"category": "tree", "algorithm": "lightgbm"}}, |
| {"action_type": "ensemble", "parameters": {"category": "stacking", "method": "stacking"}}, |
| {"action_type": "submit", "parameters": {}}, |
| ], |
| "image_quality": [ |
| {"action_type": "inspect_top_solution", "parameters": {}}, |
| {"action_type": "set_cv", "parameters": {"category": "group", "strategy": "group_kfold"}}, |
| {"action_type": "train_model", "parameters": {"category": "neural", "algorithm": "pretrained_backbone"}}, |
| {"action_type": "augmentation", "parameters": {"category": "geometric", "method": "geometric"}}, |
| {"action_type": "augmentation", "parameters": {"category": "color", "method": "clahe"}}, |
| {"action_type": "regularize", "parameters": {"category": "transfer", "method": "freeze_backbone"}}, |
| {"action_type": "postprocess", "parameters": {"category": "inference", "method": "tta"}}, |
| {"action_type": "submit", "parameters": {}}, |
| ], |
| "trajectory_pred": [ |
| {"action_type": "inspect_top_solution", "parameters": {}}, |
| {"action_type": "feature_engineering", "parameters": {"category": "encoding", "technique": "sin_cos_encoding"}}, |
| {"action_type": "feature_engineering", "parameters": {"category": "spatial", "technique": "relative_coordinates"}}, |
| {"action_type": "set_cv", "parameters": {"category": "group", "strategy": "group_kfold"}}, |
| {"action_type": "train_model", "parameters": {"category": "neural", "algorithm": "transformer_encoder"}}, |
| {"action_type": "tune_loss", "parameters": {"category": "uncertainty", "method": "gaussian_nll"}}, |
| {"action_type": "postprocess", "parameters": {"category": "domain", "method": "physics_constraints"}}, |
| {"action_type": "submit", "parameters": {}}, |
| ], |
| } |
| return plans.get(task_id, [ |
| {"action_type": "train_model", "parameters": {"category": "tree", "algorithm": "xgboost"}}, |
| {"action_type": "submit", "parameters": {}}, |
| ]) |
|
|
|
|
| def run_baseline_episode(base: str, task_id: str) -> float: |
| """Run the expert plan, return final grade score.""" |
| env_post(base, "/reset", {"task_id": task_id}) |
| for action in _expert_plan(task_id): |
| try: |
| env_post(base, "/step", {"action_type": action["action_type"], |
| "parameters": action["parameters"]}) |
| except Exception: |
| pass |
| grade = env_post(base, "/grader") |
| return float(grade.get("final_score", 0.0)) |
|
|
|
|
| |
|
|
| def smooth(x: list[float], w: int = 8) -> list[float]: |
| arr = np.convolve(x, np.ones(w) / w, mode="valid") |
| return arr.tolist() |
|
|
|
|
| |
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description="Generate training evidence plots") |
| parser.add_argument("--env-url", default="http://127.0.0.1:7860") |
| parser.add_argument("--episodes", type=int, default=40, |
| help="Episodes per agent (default: 40)") |
| parser.add_argument("--seed", type=int, default=42) |
| args = parser.parse_args() |
|
|
| random.seed(args.seed) |
| np.random.seed(args.seed) |
| base = args.env_url.rstrip("/") |
|
|
| |
| health = env_get(base, "/health") |
| assert health.get("status") == "healthy", f"Env not healthy: {health}" |
| tasks = env_get(base, "/tasks") |
| all_task_ids = [t["task_id"] for t in tasks] |
| print(f"β
Env healthy | Tasks: {all_task_ids}\n") |
|
|
| plots_dir = Path("plots") |
| plots_dir.mkdir(exist_ok=True) |
|
|
| |
| N = args.episodes |
| task_cycle = (all_task_ids * (N // len(all_task_ids) + 1))[:N] |
| random.shuffle(task_cycle) |
|
|
| random_scores: list[float] = [] |
| baseline_scores: list[float] = [] |
|
|
| print(f"Running {N} episodes per agent β¦") |
| for i, tid in enumerate(task_cycle): |
| r_score = run_random_episode(base, tid) |
| b_score = run_baseline_episode(base, tid) |
| random_scores.append(r_score) |
| baseline_scores.append(b_score) |
| print(f" ep {i+1:3d}/{N} task={tid:<22} random={r_score:.4f} baseline={b_score:.4f}") |
|
|
| print(f"\nRandom mean: {sum(random_scores)/N:.4f}") |
| print(f"Baseline mean: {sum(baseline_scores)/N:.4f}") |
|
|
| |
| per_task_random: dict[str, list[float]] = {t: [] for t in all_task_ids} |
| per_task_baseline: dict[str, list[float]] = {t: [] for t in all_task_ids} |
| for tid, rs, bs in zip(task_cycle, random_scores, baseline_scores): |
| per_task_random[tid].append(rs) |
| per_task_baseline[tid].append(bs) |
| mean_random = {t: (sum(v)/len(v) if v else 0) for t, v in per_task_random.items()} |
| mean_baseline = {t: (sum(v)/len(v) if v else 0) for t, v in per_task_baseline.items()} |
|
|
| plt.rcParams.update({"figure.dpi": 130, "font.size": 11}) |
|
|
| |
| fig, ax = plt.subplots(figsize=(10, 4)) |
| x = range(1, N + 1) |
| ax.plot(x, random_scores, alpha=0.3, color="tomato", linewidth=0.8) |
| ax.plot(x, baseline_scores, alpha=0.3, color="steelblue", linewidth=0.8) |
|
|
| if N >= 8: |
| xs = range(8, N + 1) |
| ax.plot(xs, smooth(random_scores), color="tomato", linewidth=2.0, label="Random agent (smoothed)") |
| ax.plot(xs, smooth(baseline_scores), color="steelblue", linewidth=2.0, label="Expert baseline (smoothed)") |
| else: |
| ax.plot(x, random_scores, color="tomato", linewidth=2.0, label="Random agent") |
| ax.plot(x, baseline_scores, color="steelblue", linewidth=2.0, label="Expert baseline") |
|
|
| ax.set_xlabel("Episode") |
| ax.set_ylabel("Final score (0β1)") |
| ax.set_title("KaggleSimEnv β Episode Reward: Random vs Expert Baseline") |
| ax.legend() |
| ax.set_ylim(0, 1.05) |
| ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) |
| ax.grid(alpha=0.3) |
| fig.tight_layout() |
| fig.savefig(plots_dir / "reward_curve.png") |
| print(f"\nπ Saved: {plots_dir}/reward_curve.png") |
| plt.close(fig) |
|
|
| |
| |
| |
| random_loss = [1.0 - s for s in random_scores] |
| baseline_loss = [1.0 - s for s in baseline_scores] |
|
|
| fig, ax = plt.subplots(figsize=(10, 4)) |
| if N >= 8: |
| xs = range(8, N + 1) |
| ax.plot(xs, smooth(random_loss), color="tomato", linewidth=2.0, label="Random agent loss") |
| ax.plot(xs, smooth(baseline_loss), color="steelblue", linewidth=2.0, label="Expert baseline loss") |
| else: |
| ax.plot(x, random_loss, color="tomato", linewidth=2.0, label="Random agent loss") |
| ax.plot(x, baseline_loss, color="steelblue", linewidth=2.0, label="Expert baseline loss") |
|
|
| ax.set_xlabel("Episode") |
| ax.set_ylabel("Score gap to optimal (lower = better)") |
| ax.set_title("KaggleSimEnv β Loss Proxy (1 β score) per Episode") |
| ax.legend() |
| ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.2f")) |
| ax.grid(alpha=0.3) |
| fig.tight_layout() |
| fig.savefig(plots_dir / "loss_curve.png") |
| print(f"π Saved: {plots_dir}/loss_curve.png") |
| plt.close(fig) |
|
|
| |
| task_labels = list(all_task_ids) |
| x_pos = np.arange(len(task_labels)) |
| width = 0.35 |
|
|
| fig, ax = plt.subplots(figsize=(10, 5)) |
| b_vals = [mean_random[t] for t in task_labels] |
| t_vals = [mean_baseline[t] for t in task_labels] |
|
|
| bars_b = ax.bar(x_pos - width/2, b_vals, width, label="Random agent", color="#f4a582", edgecolor="black") |
| bars_t = ax.bar(x_pos + width/2, t_vals, width, label="Expert baseline", color="#4393c3", edgecolor="black") |
|
|
| ax.set_xlabel("Task") |
| ax.set_ylabel("Mean final score (0β1)") |
| ax.set_title("KaggleSimEnv β Per-task Score: Random vs Expert Baseline") |
| ax.set_xticks(x_pos) |
| ax.set_xticklabels(task_labels, rotation=18, ha="right") |
| ax.legend() |
| ax.set_ylim(0, 1.05) |
| ax.grid(axis="y", alpha=0.3) |
|
|
| for bar in bars_b + bars_t: |
| ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.01, |
| f"{bar.get_height():.2f}", ha="center", va="bottom", fontsize=9) |
|
|
| fig.tight_layout() |
| fig.savefig(plots_dir / "baseline_vs_trained.png") |
| print(f"π Saved: {plots_dir}/baseline_vs_trained.png") |
| plt.close(fig) |
|
|
| print("\nβ
All plots saved to plots/") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|