Spaces:
Running
Running
Download app/training/generate_paper_figures.py from Rthur2003/crowncode-backend: direct link, hf CLI and curl.
- Browser
- Download file 25 kB
-
https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/training/generate_paper_figures.py
- Command line
-
hf download hf://spaces/Rthur2003/crowncode-backend/app/training/generate_paper_figures.py
-
curl -L -o generate_paper_figures.py https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/training/generate_paper_figures.py
25 kB
| """ | |
| Generate publication-ready figures for the AURIS academic paper. | |
| All text in English, 300 DPI, Times New Roman. | |
| Produces: | |
| - paper_roc_curves.png | |
| - paper_confusion_matrix_lightgbm.png | |
| - paper_model_comparison.png | |
| - paper_feature_importance.png | |
| - paper_feature_distribution.png | |
| - paper_ml_vs_dl.png | |
| - paper_calibration.png | |
| - paper_precision_recall.png | |
| - paper_score_distribution.png | |
| - paper_shap_summary.png (if shap available) | |
| - paper_pipeline_diagram.png | |
| - paper_fold_std_table.png | |
| Usage: | |
| python -m app.training.generate_paper_figures | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| import io | |
| from pathlib import Path | |
| import numpy as np | |
| try: | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.patches as mpatches | |
| import matplotlib.patheffects as pe | |
| from matplotlib.gridspec import GridSpec | |
| except ImportError: | |
| print("matplotlib required") | |
| sys.exit(1) | |
| try: | |
| import seaborn as sns | |
| HAS_SEABORN = True | |
| except ImportError: | |
| HAS_SEABORN = False | |
| from sklearn.metrics import ( | |
| roc_curve, auc, precision_recall_curve, | |
| confusion_matrix, average_precision_score, | |
| ) | |
| from sklearn.calibration import calibration_curve | |
| # ── Paths ────────────────────────────────────────────────────────────── | |
| BASE = Path(__file__).resolve().parents[2] | |
| MODELS = BASE / "models" | |
| OUT = BASE.parent / "docs" / "academic" / "figures" | |
| OUT.mkdir(parents=True, exist_ok=True) | |
| # ── Style ────────────────────────────────────────────────────────────── | |
| plt.rcParams.update({ | |
| "font.family": "serif", | |
| "font.serif": ["Times New Roman", "DejaVu Serif"], | |
| "font.size": 11, | |
| "axes.titlesize": 13, | |
| "axes.labelsize": 11, | |
| "xtick.labelsize": 10, | |
| "ytick.labelsize": 10, | |
| "legend.fontsize": 9, | |
| "figure.dpi": 150, | |
| "savefig.dpi": 300, | |
| "savefig.bbox": "tight", | |
| "savefig.pad_inches": 0.15, | |
| "axes.grid": True, | |
| "grid.alpha": 0.3, | |
| "axes.spines.top": False, | |
| "axes.spines.right": False, | |
| }) | |
| # Colorblind-safe palette | |
| COLORS = { | |
| "Logistic Regression": "#4363d8", | |
| "Random Forest": "#3cb44b", | |
| "Gradient Boosting": "#e6194b", | |
| "SVM (RBF)": "#f58231", | |
| "MLP Neural Network": "#911eb4", | |
| "XGBoost": "#42d4f4", | |
| "LightGBM": "#f032e6", | |
| "Deep MLP (512-256-128-64)": "#e6194b", | |
| "1D-CNN": "#f58231", | |
| "Residual MLP (3 blocks)": "#3cb44b", | |
| "Attention MLP": "#4363d8", | |
| } | |
| GOLD = "#C99347" | |
| BG = "#faf8f4" | |
| def _c(name: str) -> str: | |
| return COLORS.get(name, "#555555") | |
| def _save(fig, name: str) -> None: | |
| fig.savefig(OUT / f"{name}.png") | |
| fig.savefig(OUT / f"{name}.pdf") | |
| plt.close(fig) | |
| print(f" OK {name}.png") | |
| # ── Load data ────────────────────────────────────────────────────────── | |
| def _load_results(): | |
| with open(MODELS / "training_results.json") as f: | |
| ml = json.load(f) | |
| with open(MODELS / "deep_learning_results.json") as f: | |
| dl = json.load(f) | |
| # Merge: DL entries don't have y_true/y_prob from CV, so keep separate | |
| return ml, dl | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 1. ROC Curves — all models | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_roc_curves(ml: dict, dl: dict) -> None: | |
| fig, ax = plt.subplots(figsize=(7, 6)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| # ML models (have y_true / y_prob from CV) | |
| for name, data in ml.items(): | |
| if name.startswith("_") or not isinstance(data, dict): | |
| continue | |
| if "y_true" not in data or "y_prob" not in data: | |
| continue | |
| fpr, tpr, _ = roc_curve(data["y_true"], data["y_prob"]) | |
| roc_auc = auc(fpr, tpr) | |
| lw = 2.5 if name == ml.get("_best_model") else 1.5 | |
| ax.plot(fpr, tpr, color=_c(name), lw=lw, | |
| label=f"{name} (AUC={roc_auc:.3f})") | |
| # DL models — use reported AUC value as annotation | |
| for name, data in dl.items(): | |
| if not isinstance(data, dict): | |
| continue | |
| auc_val = data.get("roc_auc", 0) | |
| ax.annotate(f"{name}: AUC={auc_val:.3f}", | |
| xy=(0.55, 0.05 + list(dl.keys()).index(name) * 0.055), | |
| xycoords="axes fraction", fontsize=8, color=_c(name)) | |
| ax.plot([0, 1], [0, 1], "k--", lw=1, alpha=0.4, label="Random (AUC=0.500)") | |
| ax.set_xlabel("False Positive Rate") | |
| ax.set_ylabel("True Positive Rate") | |
| ax.set_title("ROC Curves — All Models (5-Fold CV)") | |
| ax.legend(loc="lower right", fontsize=8) | |
| ax.set_xlim(0, 1); ax.set_ylim(0, 1.02) | |
| _save(fig, "paper_roc_curves") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 2. Confusion Matrix — LightGBM (best ML model) | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_confusion_matrix_lightgbm(ml: dict) -> None: | |
| name = "LightGBM" | |
| data = ml[name] | |
| # Load pre-computed CV predictions | |
| y_true = np.load(MODELS / "lgbm_cv_ytrue.npy") | |
| y_prob = np.load(MODELS / "lgbm_cv_probs.npy") | |
| threshold = data.get("optimal_threshold", 0.5) | |
| y_pred = (y_prob >= threshold).astype(int) | |
| cm = confusion_matrix(y_true, y_pred) | |
| acc = (cm[0, 0] + cm[1, 1]) / cm.sum() | |
| f1 = data.get("f1", 0) | |
| roc = data.get("roc_auc", 0) | |
| fig, ax = plt.subplots(figsize=(5.5, 5)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| cmap = matplotlib.colors.LinearSegmentedColormap.from_list( | |
| "auris", ["#faf8f4", GOLD]) | |
| im = ax.imshow(cm, cmap=cmap) | |
| plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) | |
| labels = ["Human", "AI"] | |
| for i in range(2): | |
| for j in range(2): | |
| pct = cm[i, j] / cm.sum() * 100 | |
| ax.text(j, i, f"{cm[i, j]}\n({pct:.1f}%)", | |
| ha="center", va="center", fontsize=13, fontweight="bold", | |
| color="white" if cm[i, j] > cm.max() * 0.5 else "#333") | |
| ax.set_xticks([0, 1]); ax.set_yticks([0, 1]) | |
| ax.set_xticklabels(labels); ax.set_yticklabels(labels) | |
| ax.set_xlabel("Predicted Label"); ax.set_ylabel("Actual Label") | |
| ax.set_title(f"Confusion Matrix — LightGBM\n" | |
| f"Accuracy: {acc:.1%} | F1: {f1:.3f} | AUC: {roc:.4f}") | |
| _save(fig, "paper_confusion_matrix_lightgbm") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 3. Model Comparison Bar Chart — all 11 models | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_model_comparison(ml: dict, dl: dict) -> None: | |
| models, accs, f1s, aucs_list = [], [], [], [] | |
| for name, data in ml.items(): | |
| if name.startswith("_") or not isinstance(data, dict): | |
| continue | |
| models.append(name) | |
| accs.append(data.get("accuracy", 0)) | |
| f1s.append(data.get("f1", 0)) | |
| aucs_list.append(data.get("roc_auc", 0)) | |
| for name, data in dl.items(): | |
| if not isinstance(data, dict): | |
| continue | |
| models.append(name) | |
| accs.append(data.get("accuracy", 0)) | |
| f1s.append(data.get("f1", 0)) | |
| aucs_list.append(data.get("roc_auc", 0)) | |
| # Sort by AUC descending | |
| order = sorted(range(len(models)), key=lambda i: aucs_list[i], reverse=True) | |
| models = [models[i] for i in order] | |
| accs = [accs[i] for i in order] | |
| f1s = [f1s[i] for i in order] | |
| aucs_list = [aucs_list[i] for i in order] | |
| x = np.arange(len(models)) | |
| w = 0.25 | |
| fig, ax = plt.subplots(figsize=(13, 5)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| b1 = ax.bar(x - w, accs, w, label="Accuracy", color="#C99347", alpha=0.85) | |
| b2 = ax.bar(x, f1s, w, label="F1 Score", color="#6b8f7a", alpha=0.85) | |
| b3 = ax.bar(x + w, aucs_list, w, label="ROC-AUC", color="#a64b3c", alpha=0.85) | |
| ax.set_xticks(x) | |
| ax.set_xticklabels(models, rotation=35, ha="right", fontsize=9) | |
| ax.set_ylim(0.60, 1.0) | |
| ax.set_ylabel("Score") | |
| ax.set_title("Model Performance Comparison — 11 Models (5-Fold CV, 47 Features, 5,195 Samples)") | |
| ax.legend(loc="lower left") | |
| # Annotate AUC on top of each AUC bar | |
| for bar, val in zip(b3, aucs_list): | |
| ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.003, | |
| f"{val:.3f}", ha="center", va="bottom", fontsize=7.5, fontweight="bold") | |
| _save(fig, "paper_model_comparison") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 4. Fold Std Table Figure | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_fold_std_table(ml: dict, dl: dict) -> None: | |
| rows = [] | |
| for name, data in ml.items(): | |
| if name.startswith("_") or not isinstance(data, dict): | |
| continue | |
| auc_val = data.get("roc_auc", 0) | |
| # ML has no fold_aucs stored — show mean only with dash | |
| rows.append((name, "ML", auc_val, None, None)) | |
| for name, data in dl.items(): | |
| if not isinstance(data, dict): | |
| continue | |
| fold_aucs = data.get("fold_aucs", []) | |
| if fold_aucs: | |
| mean = np.mean(fold_aucs) | |
| std = np.std(fold_aucs) | |
| rows.append((name, "DL", mean, std, fold_aucs)) | |
| else: | |
| rows.append((name, "DL", data.get("roc_auc", 0), None, None)) | |
| rows.sort(key=lambda r: r[2], reverse=True) | |
| fig, ax = plt.subplots(figsize=(10, 0.45 * len(rows) + 1.5)) | |
| fig.patch.set_facecolor(BG) | |
| ax.axis("off") | |
| col_labels = ["Model", "Type", "Mean AUC", "Std AUC", "Fold AUCs"] | |
| table_data = [] | |
| for name, mtype, mean, std, folds in rows: | |
| std_str = f"±{std:.4f}" if std is not None else "—" | |
| folds_str = ", ".join(f"{v:.4f}" for v in folds) if folds else "—" | |
| table_data.append([name, mtype, f"{mean:.4f}", std_str, folds_str]) | |
| tbl = ax.table( | |
| cellText=table_data, | |
| colLabels=col_labels, | |
| loc="center", | |
| cellLoc="center", | |
| ) | |
| tbl.auto_set_font_size(False) | |
| tbl.set_fontsize(9) | |
| tbl.scale(1, 1.5) | |
| # Header style | |
| for j in range(len(col_labels)): | |
| tbl[(0, j)].set_facecolor(GOLD) | |
| tbl[(0, j)].set_text_props(color="white", fontweight="bold") | |
| # Alternate rows | |
| for i in range(1, len(rows) + 1): | |
| color = "#f5f0e8" if i % 2 == 0 else BG | |
| for j in range(len(col_labels)): | |
| tbl[(i, j)].set_facecolor(color) | |
| ax.set_title("Table 1. Cross-Validation AUC Results (5-Fold) — Mean ± Std", | |
| fontsize=12, fontweight="bold", pad=15) | |
| _save(fig, "paper_fold_std_table") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 5. ML vs DL Comparison — English version | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_ml_vs_dl(ml: dict, dl: dict) -> None: | |
| ml_names, ml_accs, ml_aucs, ml_f1s = [], [], [], [] | |
| dl_names, dl_accs, dl_aucs, dl_f1s = [], [], [], [] | |
| for name, data in ml.items(): | |
| if name.startswith("_") or not isinstance(data, dict): | |
| continue | |
| ml_names.append(name); ml_accs.append(data.get("accuracy", 0)) | |
| ml_aucs.append(data.get("roc_auc", 0)); ml_f1s.append(data.get("f1", 0)) | |
| for name, data in dl.items(): | |
| if not isinstance(data, dict): | |
| continue | |
| dl_names.append(name); dl_accs.append(data.get("accuracy", 0)) | |
| dl_aucs.append(data.get("roc_auc", 0)); dl_f1s.append(data.get("f1", 0)) | |
| fig, axes = plt.subplots(1, 3, figsize=(14, 5)) | |
| fig.patch.set_facecolor(BG) | |
| fig.suptitle( | |
| "ML vs DL Model Comparison — 47 Features, 5,195 Samples, 5-Fold CV", | |
| fontsize=13, fontweight="bold" | |
| ) | |
| for ax, ml_vals, dl_vals, title in zip( | |
| axes, | |
| [ml_accs, ml_aucs, ml_f1s], | |
| [dl_accs, dl_aucs, dl_f1s], | |
| ["Accuracy", "ROC-AUC", "F1 Score"], | |
| ): | |
| ax.set_facecolor(BG) | |
| ml_y = range(len(ml_names)) | |
| dl_y = range(len(dl_names)) | |
| offset = len(ml_names) + 1 | |
| bars_ml = ax.barh(list(ml_y), ml_vals, color="#C99347", alpha=0.85) | |
| bars_dl = ax.barh([i + offset for i in dl_y], dl_vals, color="#6b4a1e", alpha=0.85) | |
| ax.set_yticks( | |
| list(ml_y) + [i + offset for i in dl_y] | |
| ) | |
| ax.set_yticklabels(ml_names + dl_names, fontsize=8) | |
| # Extend the right limit so the value labels printed past each bar | |
| # are not clipped at the axis edge. | |
| ax.set_xlim(0.65, 1.06) | |
| ax.set_xlabel(title) | |
| # ML / DL labels | |
| ax.axhline(len(ml_names) - 0.5, color="#aaa", lw=1, ls="--") | |
| ax.text(0.66, len(ml_names) / 2 - 0.3, "ML", fontsize=9, | |
| color="#C99347", fontweight="bold") | |
| ax.text(0.66, offset + len(dl_names) / 2 - 0.3, "DL", fontsize=9, | |
| color="#6b4a1e", fontweight="bold") | |
| for bar, val in zip(list(bars_ml) + list(bars_dl), | |
| ml_vals + dl_vals): | |
| ax.text(bar.get_width() + 0.002, bar.get_y() + bar.get_height() / 2, | |
| f"{val:.3f}", va="center", fontsize=7.5) | |
| plt.tight_layout() | |
| _save(fig, "paper_ml_vs_dl") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 6. Pipeline Diagram | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_pipeline_diagram() -> None: | |
| fig, ax = plt.subplots(figsize=(14, 4)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| ax.axis("off") | |
| ax.set_xlim(0, 14) | |
| ax.set_ylim(0, 4) | |
| # Define stages | |
| stages = [ | |
| (0.6, "Audio Input\n(WAV / MP3 / FLAC)", "#4363d8"), | |
| (2.6, "Feature\nExtraction\n(librosa)", "#3cb44b"), | |
| (4.6, "47 Audio\nFeatures", "#f58231"), | |
| (6.6, "Standard\nScaler", "#911eb4"), | |
| (8.6, "ML / DL\nEnsemble\n(11 Models)", "#C99347"), | |
| (10.6, "Probability\nFusion\n(Youden Thresh.)", "#e6194b"), | |
| (12.6, "Decision\nAI / Human", "#1a1a1a"), | |
| ] | |
| box_w = 1.6 | |
| box_h = 1.8 | |
| box_y = 1.1 | |
| for x, label, color in stages: | |
| rect = mpatches.FancyBboxPatch( | |
| (x - box_w / 2, box_y), box_w, box_h, | |
| boxstyle="round,pad=0.1", | |
| facecolor=color, edgecolor="white", | |
| linewidth=2, alpha=0.88, | |
| ) | |
| ax.add_patch(rect) | |
| ax.text(x, box_y + box_h / 2, label, | |
| ha="center", va="center", | |
| fontsize=9, color="white", fontweight="bold", | |
| wrap=True) | |
| # Arrows | |
| for i in range(len(stages) - 1): | |
| x1 = stages[i][0] + box_w / 2 | |
| x2 = stages[i+1][0] - box_w / 2 | |
| y = box_y + box_h / 2 | |
| ax.annotate("", xy=(x2, y), xytext=(x1, y), | |
| arrowprops=dict(arrowstyle="->", color="#555", lw=2)) | |
| # Sub-labels under boxes | |
| sub = [ | |
| (0.6, "File upload /\nmicrophone"), | |
| (2.6, "Spectral · Temporal\nVocal · Rhythmic"), | |
| (4.6, "Normalized\nvector"), | |
| (6.6, "Zero mean\nUnit variance"), | |
| (8.6, "7 ML + 4 DL\nmodels"), | |
| (10.6, "Optimal\nthreshold"), | |
| (12.6, "P(AI) score\n0–100%"), | |
| ] | |
| for x, txt in sub: | |
| ax.text(x, box_y - 0.35, txt, | |
| ha="center", va="top", fontsize=7.5, color="#555", | |
| style="italic") | |
| ax.set_title( | |
| "Figure 1. AURIS System Pipeline — End-to-End AI Music Detection", | |
| fontsize=12, fontweight="bold", pad=10 | |
| ) | |
| _save(fig, "paper_pipeline_diagram") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 7. Feature Importance — English | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_feature_importance(ml: dict) -> None: | |
| imp = ml.get("_feature_importance", {}) | |
| if not imp: | |
| print(" ⚠ No feature importance data") | |
| return | |
| items = sorted(imp.items(), key=lambda x: x[1], reverse=True)[:20] | |
| names = [k for k, _ in items] | |
| vals = [v for _, v in items] | |
| colors_bar = [ | |
| "#6b4a1e" if v > 0.08 else | |
| "#C99347" if v > 0.04 else | |
| "#e6c97a" | |
| for v in vals | |
| ] | |
| fig, ax = plt.subplots(figsize=(8, 7)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| bars = ax.barh(names[::-1], vals[::-1], color=colors_bar[::-1], edgecolor="white") | |
| for bar, val in zip(bars, vals[::-1]): | |
| ax.text(bar.get_width() + 0.001, bar.get_y() + bar.get_height() / 2, | |
| f"{val:.4f}", va="center", fontsize=8) | |
| ax.set_xlabel("Normalized Importance") | |
| ax.set_title("Top 20 Feature Importances — LightGBM (Best Model)") | |
| ax.set_xlim(0, max(vals) * 1.18) | |
| _save(fig, "paper_feature_importance") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 8. Score Distribution — English | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_score_distribution(ml: dict) -> None: | |
| best = ml.get("_best_model", "LightGBM") | |
| data = ml.get(best, {}) | |
| y_prob_path = MODELS / "lgbm_cv_probs.npy" | |
| y_true_path = MODELS / "lgbm_cv_ytrue.npy" | |
| if not y_prob_path.exists(): | |
| print(" SKIP No score distribution data") | |
| return | |
| y_true = np.load(y_true_path) | |
| y_prob = np.load(y_prob_path) | |
| threshold = data.get("optimal_threshold", 0.5) | |
| fig, ax = plt.subplots(figsize=(7, 4.5)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| ax.hist(y_prob[y_true == 0], bins=40, alpha=0.65, | |
| color="#3cb44b", label=f"Human (n={int((y_true==0).sum())})", density=False) | |
| ax.hist(y_prob[y_true == 1], bins=40, alpha=0.65, | |
| color="#e6194b", label=f"AI (n={int((y_true==1).sum())})", density=False) | |
| ax.axvline(threshold, color="#555", ls="--", lw=1.5, | |
| label=f"Decision threshold ({threshold:.2f})") | |
| ax.set_xlabel("Predicted Probability P(AI)") | |
| ax.set_ylabel("Count") | |
| ax.set_title(f"Predicted Probability Distribution — {best}") | |
| ax.legend() | |
| _save(fig, "paper_score_distribution") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 9. Calibration — English | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_calibration(ml: dict) -> None: | |
| best = ml.get("_best_model", "LightGBM") | |
| data = ml.get(best, {}) | |
| y_prob_path = MODELS / "lgbm_cv_probs.npy" | |
| y_true_path = MODELS / "lgbm_cv_ytrue.npy" | |
| if not y_prob_path.exists(): | |
| print(" SKIP No calibration data") | |
| return | |
| y_true = np.load(y_true_path) | |
| y_prob = np.load(y_prob_path) | |
| from sklearn.metrics import brier_score_loss | |
| brier = brier_score_loss(y_true, y_prob) | |
| prob_true, prob_pred = calibration_curve(y_true, y_prob, n_bins=10) | |
| fig, ax = plt.subplots(figsize=(5.5, 5)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| ax.plot(prob_pred, prob_true, "o-", color=GOLD, lw=2, ms=6, label=best) | |
| ax.fill_between(prob_pred, prob_true, prob_pred, | |
| alpha=0.12, color=GOLD) | |
| ax.plot([0, 1], [0, 1], "k--", lw=1, alpha=0.5, label="Perfect calibration") | |
| ax.text(0.05, 0.88, | |
| f"Brier Score = {brier:.4f}\nN = {len(y_true)} (5-Fold CV)", | |
| transform=ax.transAxes, fontsize=9, | |
| bbox=dict(boxstyle="round,pad=0.4", facecolor="white", alpha=0.7)) | |
| ax.set_xlabel("Mean Predicted Probability") | |
| ax.set_ylabel("Fraction of Positives") | |
| ax.set_title(f"Calibration Curve — {best}") | |
| ax.legend() | |
| _save(fig, "paper_calibration") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # 10. Precision-Recall — English | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def plot_precision_recall(ml: dict) -> None: | |
| best = ml.get("_best_model", "LightGBM") | |
| y_prob_path = MODELS / "lgbm_cv_probs.npy" | |
| y_true_path = MODELS / "lgbm_cv_ytrue.npy" | |
| if not y_prob_path.exists(): | |
| print(" SKIP No precision-recall data") | |
| return | |
| y_true = np.load(y_true_path) | |
| y_prob = np.load(y_prob_path) | |
| prec, rec, _ = precision_recall_curve(y_true, y_prob) | |
| ap = average_precision_score(y_true, y_prob) | |
| baseline = y_true.mean() | |
| fig, ax = plt.subplots(figsize=(5.5, 5)) | |
| fig.patch.set_facecolor(BG) | |
| ax.set_facecolor(BG) | |
| ax.plot(rec, prec, color=GOLD, lw=2, label=f"{best} (AP={ap:.4f})") | |
| ax.fill_between(rec, prec, alpha=0.12, color=GOLD) | |
| ax.axhline(baseline, color="#888", ls="--", lw=1, | |
| label=f"Baseline = {baseline:.3f}") | |
| ax.set_xlabel("Recall") | |
| ax.set_ylabel("Precision") | |
| ax.set_title(f"Precision-Recall Curve — {best}") | |
| ax.legend() | |
| _save(fig, "paper_precision_recall") | |
| # ══════════════════════════════════════════════════════════════════════ | |
| # MAIN | |
| # ══════════════════════════════════════════════════════════════════════ | |
| def main() -> None: | |
| print(f"Output dir: {OUT}\n") | |
| ml, dl = _load_results() | |
| print("Generating figures...") | |
| plot_roc_curves(ml, dl) | |
| plot_confusion_matrix_lightgbm(ml) | |
| plot_model_comparison(ml, dl) | |
| plot_fold_std_table(ml, dl) | |
| plot_ml_vs_dl(ml, dl) | |
| plot_pipeline_diagram() | |
| plot_feature_importance(ml) | |
| plot_score_distribution(ml) | |
| plot_calibration(ml) | |
| plot_precision_recall(ml) | |
| print(f"\nDone. All figures saved to {OUT}") | |
| if __name__ == "__main__": | |
| main() | |