Rthur2003 commited on
Commit
e8077a2
·
1 Parent(s): 4718070

feat: add script to generate publication-ready figures for AURIS paper

Browse files
app/training/generate_paper_figures.py ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Generate publication-ready figures for the AURIS academic paper.
3
+ All text in English, 300 DPI, Times New Roman.
4
+
5
+ Produces:
6
+ - paper_roc_curves.png
7
+ - paper_confusion_matrix_lightgbm.png
8
+ - paper_model_comparison.png
9
+ - paper_feature_importance.png
10
+ - paper_feature_distribution.png
11
+ - paper_ml_vs_dl.png
12
+ - paper_calibration.png
13
+ - paper_precision_recall.png
14
+ - paper_score_distribution.png
15
+ - paper_shap_summary.png (if shap available)
16
+ - paper_pipeline_diagram.png
17
+ - paper_fold_std_table.png
18
+
19
+ Usage:
20
+ python -m app.training.generate_paper_figures
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import sys
27
+ import io
28
+ from pathlib import Path
29
+
30
+ import numpy as np
31
+
32
+ try:
33
+ import matplotlib
34
+ matplotlib.use("Agg")
35
+ import matplotlib.pyplot as plt
36
+ import matplotlib.patches as mpatches
37
+ import matplotlib.patheffects as pe
38
+ from matplotlib.gridspec import GridSpec
39
+ except ImportError:
40
+ print("matplotlib required")
41
+ sys.exit(1)
42
+
43
+ try:
44
+ import seaborn as sns
45
+ HAS_SEABORN = True
46
+ except ImportError:
47
+ HAS_SEABORN = False
48
+
49
+ from sklearn.metrics import (
50
+ roc_curve, auc, precision_recall_curve,
51
+ confusion_matrix, average_precision_score,
52
+ )
53
+ from sklearn.calibration import calibration_curve
54
+
55
+ # ── Paths ──────────────────────────────────────────────────────────────
56
+ BASE = Path(__file__).resolve().parents[2]
57
+ MODELS = BASE / "models"
58
+ OUT = BASE.parent / "docs" / "academic" / "figures"
59
+ OUT.mkdir(parents=True, exist_ok=True)
60
+
61
+ # ── Style ──────────────────────────────────────────────────────────────
62
+ plt.rcParams.update({
63
+ "font.family": "serif",
64
+ "font.serif": ["Times New Roman", "DejaVu Serif"],
65
+ "font.size": 11,
66
+ "axes.titlesize": 13,
67
+ "axes.labelsize": 11,
68
+ "xtick.labelsize": 10,
69
+ "ytick.labelsize": 10,
70
+ "legend.fontsize": 9,
71
+ "figure.dpi": 150,
72
+ "savefig.dpi": 300,
73
+ "savefig.bbox": "tight",
74
+ "savefig.pad_inches": 0.15,
75
+ "axes.grid": True,
76
+ "grid.alpha": 0.3,
77
+ "axes.spines.top": False,
78
+ "axes.spines.right": False,
79
+ })
80
+
81
+ # Colorblind-safe palette
82
+ COLORS = {
83
+ "Logistic Regression": "#4363d8",
84
+ "Random Forest": "#3cb44b",
85
+ "Gradient Boosting": "#e6194b",
86
+ "SVM (RBF)": "#f58231",
87
+ "MLP Neural Network": "#911eb4",
88
+ "XGBoost": "#42d4f4",
89
+ "LightGBM": "#f032e6",
90
+ "Deep MLP (512-256-128-64)": "#e6194b",
91
+ "1D-CNN": "#f58231",
92
+ "Residual MLP (3 blocks)": "#3cb44b",
93
+ "Attention MLP": "#4363d8",
94
+ }
95
+ GOLD = "#C99347"
96
+ BG = "#faf8f4"
97
+
98
+
99
+ def _c(name: str) -> str:
100
+ return COLORS.get(name, "#555555")
101
+
102
+
103
+ def _save(fig, name: str) -> None:
104
+ fig.savefig(OUT / f"{name}.png")
105
+ fig.savefig(OUT / f"{name}.pdf")
106
+ plt.close(fig)
107
+ print(f" OK {name}.png")
108
+
109
+
110
+ # ── Load data ──────────────────────────────────────────────────────────
111
+
112
+ def _load_results():
113
+ with open(MODELS / "training_results.json") as f:
114
+ ml = json.load(f)
115
+ with open(MODELS / "deep_learning_results.json") as f:
116
+ dl = json.load(f)
117
+ # Merge: DL entries don't have y_true/y_prob from CV, so keep separate
118
+ return ml, dl
119
+
120
+
121
+ # ══════════════════════════════════════════════════════════════════════
122
+ # 1. ROC Curves — all models
123
+ # ══════════════════════════════════════════════════════════════════════
124
+
125
+ def plot_roc_curves(ml: dict, dl: dict) -> None:
126
+ fig, ax = plt.subplots(figsize=(7, 6))
127
+ fig.patch.set_facecolor(BG)
128
+ ax.set_facecolor(BG)
129
+
130
+ # ML models (have y_true / y_prob from CV)
131
+ for name, data in ml.items():
132
+ if name.startswith("_") or not isinstance(data, dict):
133
+ continue
134
+ if "y_true" not in data or "y_prob" not in data:
135
+ continue
136
+ fpr, tpr, _ = roc_curve(data["y_true"], data["y_prob"])
137
+ roc_auc = auc(fpr, tpr)
138
+ lw = 2.5 if name == ml.get("_best_model") else 1.5
139
+ ax.plot(fpr, tpr, color=_c(name), lw=lw,
140
+ label=f"{name} (AUC={roc_auc:.3f})")
141
+
142
+ # DL models — use reported AUC value as annotation
143
+ for name, data in dl.items():
144
+ if not isinstance(data, dict):
145
+ continue
146
+ auc_val = data.get("roc_auc", 0)
147
+ ax.annotate(f"{name}: AUC={auc_val:.3f}",
148
+ xy=(0.55, 0.05 + list(dl.keys()).index(name) * 0.055),
149
+ xycoords="axes fraction", fontsize=8, color=_c(name))
150
+
151
+ ax.plot([0, 1], [0, 1], "k--", lw=1, alpha=0.4, label="Random (AUC=0.500)")
152
+ ax.set_xlabel("False Positive Rate")
153
+ ax.set_ylabel("True Positive Rate")
154
+ ax.set_title("ROC Curves — All Models (5-Fold CV)")
155
+ ax.legend(loc="lower right", fontsize=8)
156
+ ax.set_xlim(0, 1); ax.set_ylim(0, 1.02)
157
+ _save(fig, "paper_roc_curves")
158
+
159
+
160
+ # ══════════════════════════════════════════════════════════════════════
161
+ # 2. Confusion Matrix — LightGBM (best ML model)
162
+ # ══════════════════════════════════════════════════════════════════════
163
+
164
+ def plot_confusion_matrix_lightgbm(ml: dict) -> None:
165
+ name = "LightGBM"
166
+ data = ml[name]
167
+ y_true = np.array(data["y_true"])
168
+ y_prob = np.array(data["y_prob"])
169
+ # Use Youden's J threshold if stored, else 0.5
170
+ threshold = data.get("optimal_threshold", 0.5)
171
+ y_pred = (y_prob >= threshold).astype(int)
172
+
173
+ cm = confusion_matrix(y_true, y_pred)
174
+ acc = (cm[0, 0] + cm[1, 1]) / cm.sum()
175
+ f1 = data.get("f1", 0)
176
+ roc = data.get("roc_auc", 0)
177
+
178
+ fig, ax = plt.subplots(figsize=(5.5, 5))
179
+ fig.patch.set_facecolor(BG)
180
+ ax.set_facecolor(BG)
181
+
182
+ cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
183
+ "auris", ["#faf8f4", GOLD])
184
+ im = ax.imshow(cm, cmap=cmap)
185
+ plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
186
+
187
+ labels = ["Human", "AI"]
188
+ for i in range(2):
189
+ for j in range(2):
190
+ pct = cm[i, j] / cm.sum() * 100
191
+ ax.text(j, i, f"{cm[i, j]}\n({pct:.1f}%)",
192
+ ha="center", va="center", fontsize=13, fontweight="bold",
193
+ color="white" if cm[i, j] > cm.max() * 0.5 else "#333")
194
+
195
+ ax.set_xticks([0, 1]); ax.set_yticks([0, 1])
196
+ ax.set_xticklabels(labels); ax.set_yticklabels(labels)
197
+ ax.set_xlabel("Predicted Label"); ax.set_ylabel("Actual Label")
198
+ ax.set_title(f"Confusion Matrix — LightGBM\n"
199
+ f"Accuracy: {acc:.1%} | F1: {f1:.3f} | AUC: {roc:.4f}")
200
+ _save(fig, "paper_confusion_matrix_lightgbm")
201
+
202
+
203
+ # ══════════════════════════════════════════════════════════════════════
204
+ # 3. Model Comparison Bar Chart — all 11 models
205
+ # ══════════════════════════════════════════════════════════════════════
206
+
207
+ def plot_model_comparison(ml: dict, dl: dict) -> None:
208
+ models, accs, f1s, aucs_list = [], [], [], []
209
+
210
+ for name, data in ml.items():
211
+ if name.startswith("_") or not isinstance(data, dict):
212
+ continue
213
+ models.append(name)
214
+ accs.append(data.get("accuracy", 0))
215
+ f1s.append(data.get("f1", 0))
216
+ aucs_list.append(data.get("roc_auc", 0))
217
+
218
+ for name, data in dl.items():
219
+ if not isinstance(data, dict):
220
+ continue
221
+ models.append(name)
222
+ accs.append(data.get("accuracy", 0))
223
+ f1s.append(data.get("f1", 0))
224
+ aucs_list.append(data.get("roc_auc", 0))
225
+
226
+ # Sort by AUC descending
227
+ order = sorted(range(len(models)), key=lambda i: aucs_list[i], reverse=True)
228
+ models = [models[i] for i in order]
229
+ accs = [accs[i] for i in order]
230
+ f1s = [f1s[i] for i in order]
231
+ aucs_list = [aucs_list[i] for i in order]
232
+
233
+ x = np.arange(len(models))
234
+ w = 0.25
235
+ fig, ax = plt.subplots(figsize=(13, 5))
236
+ fig.patch.set_facecolor(BG)
237
+ ax.set_facecolor(BG)
238
+
239
+ b1 = ax.bar(x - w, accs, w, label="Accuracy", color="#C99347", alpha=0.85)
240
+ b2 = ax.bar(x, f1s, w, label="F1 Score", color="#6b8f7a", alpha=0.85)
241
+ b3 = ax.bar(x + w, aucs_list, w, label="ROC-AUC", color="#a64b3c", alpha=0.85)
242
+
243
+ ax.set_xticks(x)
244
+ ax.set_xticklabels(models, rotation=35, ha="right", fontsize=9)
245
+ ax.set_ylim(0.60, 1.0)
246
+ ax.set_ylabel("Score")
247
+ ax.set_title("Model Performance Comparison — 11 Models (5-Fold CV, 47 Features, 5,195 Samples)")
248
+ ax.legend(loc="lower left")
249
+
250
+ # Annotate AUC on top of each AUC bar
251
+ for bar, val in zip(b3, aucs_list):
252
+ ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.003,
253
+ f"{val:.3f}", ha="center", va="bottom", fontsize=7.5, fontweight="bold")
254
+
255
+ _save(fig, "paper_model_comparison")
256
+
257
+
258
+ # ══════════════════════════════════════════════════════════════════════
259
+ # 4. Fold Std Table Figure
260
+ # ══════════════════════════════════════════════════════════════════════
261
+
262
+ def plot_fold_std_table(ml: dict, dl: dict) -> None:
263
+ rows = []
264
+
265
+ for name, data in ml.items():
266
+ if name.startswith("_") or not isinstance(data, dict):
267
+ continue
268
+ auc_val = data.get("roc_auc", 0)
269
+ # ML has no fold_aucs stored — show mean only with dash
270
+ rows.append((name, "ML", auc_val, None, None))
271
+
272
+ for name, data in dl.items():
273
+ if not isinstance(data, dict):
274
+ continue
275
+ fold_aucs = data.get("fold_aucs", [])
276
+ if fold_aucs:
277
+ mean = np.mean(fold_aucs)
278
+ std = np.std(fold_aucs)
279
+ rows.append((name, "DL", mean, std, fold_aucs))
280
+ else:
281
+ rows.append((name, "DL", data.get("roc_auc", 0), None, None))
282
+
283
+ rows.sort(key=lambda r: r[2], reverse=True)
284
+
285
+ fig, ax = plt.subplots(figsize=(10, 0.45 * len(rows) + 1.5))
286
+ fig.patch.set_facecolor(BG)
287
+ ax.axis("off")
288
+
289
+ col_labels = ["Model", "Type", "Mean AUC", "Std AUC", "Fold AUCs"]
290
+ table_data = []
291
+ for name, mtype, mean, std, folds in rows:
292
+ std_str = f"±{std:.4f}" if std is not None else "—"
293
+ folds_str = ", ".join(f"{v:.4f}" for v in folds) if folds else "—"
294
+ table_data.append([name, mtype, f"{mean:.4f}", std_str, folds_str])
295
+
296
+ tbl = ax.table(
297
+ cellText=table_data,
298
+ colLabels=col_labels,
299
+ loc="center",
300
+ cellLoc="center",
301
+ )
302
+ tbl.auto_set_font_size(False)
303
+ tbl.set_fontsize(9)
304
+ tbl.scale(1, 1.5)
305
+
306
+ # Header style
307
+ for j in range(len(col_labels)):
308
+ tbl[(0, j)].set_facecolor(GOLD)
309
+ tbl[(0, j)].set_text_props(color="white", fontweight="bold")
310
+
311
+ # Alternate rows
312
+ for i in range(1, len(rows) + 1):
313
+ color = "#f5f0e8" if i % 2 == 0 else BG
314
+ for j in range(len(col_labels)):
315
+ tbl[(i, j)].set_facecolor(color)
316
+
317
+ ax.set_title("Table 1. Cross-Validation AUC Results (5-Fold) — Mean ± Std",
318
+ fontsize=12, fontweight="bold", pad=15)
319
+ _save(fig, "paper_fold_std_table")
320
+
321
+
322
+ # ══════════════════════════════════════════════════════════════════════
323
+ # 5. ML vs DL Comparison — English version
324
+ # ══════════════════════════════════════════════════════════════════════
325
+
326
+ def plot_ml_vs_dl(ml: dict, dl: dict) -> None:
327
+ ml_names, ml_accs, ml_aucs, ml_f1s = [], [], [], []
328
+ dl_names, dl_accs, dl_aucs, dl_f1s = [], [], [], []
329
+
330
+ for name, data in ml.items():
331
+ if name.startswith("_") or not isinstance(data, dict):
332
+ continue
333
+ ml_names.append(name); ml_accs.append(data.get("accuracy", 0))
334
+ ml_aucs.append(data.get("roc_auc", 0)); ml_f1s.append(data.get("f1", 0))
335
+
336
+ for name, data in dl.items():
337
+ if not isinstance(data, dict):
338
+ continue
339
+ dl_names.append(name); dl_accs.append(data.get("accuracy", 0))
340
+ dl_aucs.append(data.get("roc_auc", 0)); dl_f1s.append(data.get("f1", 0))
341
+
342
+ fig, axes = plt.subplots(1, 3, figsize=(14, 5))
343
+ fig.patch.set_facecolor(BG)
344
+ fig.suptitle(
345
+ "ML vs DL Model Comparison — 47 Features, 5,195 Samples, 5-Fold CV",
346
+ fontsize=13, fontweight="bold"
347
+ )
348
+
349
+ for ax, ml_vals, dl_vals, title in zip(
350
+ axes,
351
+ [ml_accs, ml_aucs, ml_f1s],
352
+ [dl_accs, dl_aucs, dl_f1s],
353
+ ["Accuracy", "ROC-AUC", "F1 Score"],
354
+ ):
355
+ ax.set_facecolor(BG)
356
+ ml_y = range(len(ml_names))
357
+ dl_y = range(len(dl_names))
358
+ offset = len(ml_names) + 1
359
+
360
+ bars_ml = ax.barh(list(ml_y), ml_vals, color="#C99347", alpha=0.85)
361
+ bars_dl = ax.barh([i + offset for i in dl_y], dl_vals, color="#6b4a1e", alpha=0.85)
362
+
363
+ ax.set_yticks(
364
+ list(ml_y) + [i + offset for i in dl_y]
365
+ )
366
+ ax.set_yticklabels(ml_names + dl_names, fontsize=8)
367
+ ax.set_xlim(0.65, 1.0)
368
+ ax.set_xlabel(title)
369
+
370
+ # ML / DL labels
371
+ ax.axhline(len(ml_names) - 0.5, color="#aaa", lw=1, ls="--")
372
+ ax.text(0.66, len(ml_names) / 2 - 0.3, "ML", fontsize=9,
373
+ color="#C99347", fontweight="bold")
374
+ ax.text(0.66, offset + len(dl_names) / 2 - 0.3, "DL", fontsize=9,
375
+ color="#6b4a1e", fontweight="bold")
376
+
377
+ for bar, val in zip(list(bars_ml) + list(bars_dl),
378
+ ml_vals + dl_vals):
379
+ ax.text(bar.get_width() + 0.002, bar.get_y() + bar.get_height() / 2,
380
+ f"{val:.3f}", va="center", fontsize=7.5)
381
+
382
+ plt.tight_layout()
383
+ _save(fig, "paper_ml_vs_dl")
384
+
385
+
386
+ # ══════════════════════════════════════════════════════════════════════
387
+ # 6. Pipeline Diagram
388
+ # ══════════════════════════════════════════════════════════════════════
389
+
390
+ def plot_pipeline_diagram() -> None:
391
+ fig, ax = plt.subplots(figsize=(14, 4))
392
+ fig.patch.set_facecolor(BG)
393
+ ax.set_facecolor(BG)
394
+ ax.axis("off")
395
+ ax.set_xlim(0, 14)
396
+ ax.set_ylim(0, 4)
397
+
398
+ # Define stages
399
+ stages = [
400
+ (0.6, "Audio Input\n(WAV / MP3 / FLAC)", "#4363d8"),
401
+ (2.6, "Feature\nExtraction\n(librosa)", "#3cb44b"),
402
+ (4.6, "47 Audio\nFeatures", "#f58231"),
403
+ (6.6, "Standard\nScaler", "#911eb4"),
404
+ (8.6, "ML / DL\nEnsemble\n(11 Models)", "#C99347"),
405
+ (10.6, "Probability\nFusion\n(Youden Thresh.)", "#e6194b"),
406
+ (12.6, "Decision\nAI / Human", "#1a1a1a"),
407
+ ]
408
+
409
+ box_w = 1.6
410
+ box_h = 1.8
411
+ box_y = 1.1
412
+
413
+ for x, label, color in stages:
414
+ rect = mpatches.FancyBboxPatch(
415
+ (x - box_w / 2, box_y), box_w, box_h,
416
+ boxstyle="round,pad=0.1",
417
+ facecolor=color, edgecolor="white",
418
+ linewidth=2, alpha=0.88,
419
+ )
420
+ ax.add_patch(rect)
421
+ ax.text(x, box_y + box_h / 2, label,
422
+ ha="center", va="center",
423
+ fontsize=9, color="white", fontweight="bold",
424
+ wrap=True)
425
+
426
+ # Arrows
427
+ for i in range(len(stages) - 1):
428
+ x1 = stages[i][0] + box_w / 2
429
+ x2 = stages[i+1][0] - box_w / 2
430
+ y = box_y + box_h / 2
431
+ ax.annotate("", xy=(x2, y), xytext=(x1, y),
432
+ arrowprops=dict(arrowstyle="->", color="#555", lw=2))
433
+
434
+ # Sub-labels under boxes
435
+ sub = [
436
+ (0.6, "File upload /\nmicrophone"),
437
+ (2.6, "Spectral · Temporal\nVocal · Rhythmic"),
438
+ (4.6, "Normalized\nvector"),
439
+ (6.6, "Zero mean\nUnit variance"),
440
+ (8.6, "7 ML + 4 DL\nmodels"),
441
+ (10.6, "Optimal\nthreshold"),
442
+ (12.6, "P(AI) score\n0–100%"),
443
+ ]
444
+ for x, txt in sub:
445
+ ax.text(x, box_y - 0.35, txt,
446
+ ha="center", va="top", fontsize=7.5, color="#555",
447
+ style="italic")
448
+
449
+ ax.set_title(
450
+ "Figure 1. AURIS System Pipeline — End-to-End AI Music Detection",
451
+ fontsize=12, fontweight="bold", pad=10
452
+ )
453
+ _save(fig, "paper_pipeline_diagram")
454
+
455
+
456
+ # ══════════════════════════════════════════════════════════════════════
457
+ # 7. Feature Importance — English
458
+ # ══════════════════════════════════════════════════════════════════════
459
+
460
+ def plot_feature_importance(ml: dict) -> None:
461
+ imp = ml.get("_feature_importance", {})
462
+ if not imp:
463
+ print(" ⚠ No feature importance data")
464
+ return
465
+
466
+ items = sorted(imp.items(), key=lambda x: x[1], reverse=True)[:20]
467
+ names = [k for k, _ in items]
468
+ vals = [v for _, v in items]
469
+
470
+ colors_bar = [
471
+ "#6b4a1e" if v > 0.08 else
472
+ "#C99347" if v > 0.04 else
473
+ "#e6c97a"
474
+ for v in vals
475
+ ]
476
+
477
+ fig, ax = plt.subplots(figsize=(8, 7))
478
+ fig.patch.set_facecolor(BG)
479
+ ax.set_facecolor(BG)
480
+
481
+ bars = ax.barh(names[::-1], vals[::-1], color=colors_bar[::-1], edgecolor="white")
482
+ for bar, val in zip(bars, vals[::-1]):
483
+ ax.text(bar.get_width() + 0.001, bar.get_y() + bar.get_height() / 2,
484
+ f"{val:.4f}", va="center", fontsize=8)
485
+
486
+ ax.set_xlabel("Normalized Importance")
487
+ ax.set_title("Top 20 Feature Importances — LightGBM (Best Model)")
488
+ ax.set_xlim(0, max(vals) * 1.18)
489
+ _save(fig, "paper_feature_importance")
490
+
491
+
492
+ # ══════════════════════════════════════════════════════════════════════
493
+ # 8. Score Distribution — English
494
+ # ══════════════════════════════════════════════════════════════════════
495
+
496
+ def plot_score_distribution(ml: dict) -> None:
497
+ best = ml.get("_best_model", "LightGBM")
498
+ data = ml.get(best, {})
499
+ if not data or "y_true" not in data:
500
+ print(" ⚠ No score distribution data")
501
+ return
502
+
503
+ y_true = np.array(data["y_true"])
504
+ y_prob = np.array(data["y_prob"])
505
+ threshold = data.get("optimal_threshold", 0.5)
506
+
507
+ fig, ax = plt.subplots(figsize=(7, 4.5))
508
+ fig.patch.set_facecolor(BG)
509
+ ax.set_facecolor(BG)
510
+
511
+ ax.hist(y_prob[y_true == 0], bins=40, alpha=0.65,
512
+ color="#3cb44b", label=f"Human (n={int((y_true==0).sum())})", density=False)
513
+ ax.hist(y_prob[y_true == 1], bins=40, alpha=0.65,
514
+ color="#e6194b", label=f"AI (n={int((y_true==1).sum())})", density=False)
515
+ ax.axvline(threshold, color="#555", ls="--", lw=1.5,
516
+ label=f"Decision threshold ({threshold:.2f})")
517
+
518
+ ax.set_xlabel("Predicted Probability P(AI)")
519
+ ax.set_ylabel("Count")
520
+ ax.set_title(f"Predicted Probability Distribution — {best}")
521
+ ax.legend()
522
+ _save(fig, "paper_score_distribution")
523
+
524
+
525
+ # ══════════════════════════════════════════════════════════════════════
526
+ # 9. Calibration — English
527
+ # ══════════════════════════════════════════════════════════════════════
528
+
529
+ def plot_calibration(ml: dict) -> None:
530
+ best = ml.get("_best_model", "LightGBM")
531
+ data = ml.get(best, {})
532
+ if not data or "y_true" not in data:
533
+ return
534
+
535
+ y_true = np.array(data["y_true"])
536
+ y_prob = np.array(data["y_prob"])
537
+
538
+ from sklearn.metrics import brier_score_loss
539
+ brier = brier_score_loss(y_true, y_prob)
540
+
541
+ prob_true, prob_pred = calibration_curve(y_true, y_prob, n_bins=10)
542
+
543
+ fig, ax = plt.subplots(figsize=(5.5, 5))
544
+ fig.patch.set_facecolor(BG)
545
+ ax.set_facecolor(BG)
546
+
547
+ ax.plot(prob_pred, prob_true, "o-", color=GOLD, lw=2, ms=6, label=best)
548
+ ax.fill_between(prob_pred, prob_true, prob_pred,
549
+ alpha=0.12, color=GOLD)
550
+ ax.plot([0, 1], [0, 1], "k--", lw=1, alpha=0.5, label="Perfect calibration")
551
+
552
+ ax.text(0.05, 0.88,
553
+ f"Brier Score = {brier:.4f}\nN = {len(y_true)} (5-Fold CV)",
554
+ transform=ax.transAxes, fontsize=9,
555
+ bbox=dict(boxstyle="round,pad=0.4", facecolor="white", alpha=0.7))
556
+
557
+ ax.set_xlabel("Mean Predicted Probability")
558
+ ax.set_ylabel("Fraction of Positives")
559
+ ax.set_title(f"Calibration Curve — {best}")
560
+ ax.legend()
561
+ _save(fig, "paper_calibration")
562
+
563
+
564
+ # ══════════════════════════════════════════════════════════════════════
565
+ # 10. Precision-Recall — English
566
+ # ══════════════════════════════════════════════════════════════════════
567
+
568
+ def plot_precision_recall(ml: dict) -> None:
569
+ best = ml.get("_best_model", "LightGBM")
570
+ data = ml.get(best, {})
571
+ if not data or "y_true" not in data:
572
+ return
573
+
574
+ y_true = np.array(data["y_true"])
575
+ y_prob = np.array(data["y_prob"])
576
+ prec, rec, _ = precision_recall_curve(y_true, y_prob)
577
+ ap = average_precision_score(y_true, y_prob)
578
+ baseline = y_true.mean()
579
+
580
+ fig, ax = plt.subplots(figsize=(5.5, 5))
581
+ fig.patch.set_facecolor(BG)
582
+ ax.set_facecolor(BG)
583
+
584
+ ax.plot(rec, prec, color=GOLD, lw=2, label=f"{best} (AP={ap:.4f})")
585
+ ax.fill_between(rec, prec, alpha=0.12, color=GOLD)
586
+ ax.axhline(baseline, color="#888", ls="--", lw=1,
587
+ label=f"Baseline = {baseline:.3f}")
588
+
589
+ ax.set_xlabel("Recall")
590
+ ax.set_ylabel("Precision")
591
+ ax.set_title(f"Precision-Recall Curve — {best}")
592
+ ax.legend()
593
+ _save(fig, "paper_precision_recall")
594
+
595
+
596
+ # ══════════════════════════════════════════════════════════════════════
597
+ # MAIN
598
+ # ══════════════════════════════════════════════════════════════════════
599
+
600
+ def main() -> None:
601
+ print(f"Output dir: {OUT}\n")
602
+ ml, dl = _load_results()
603
+
604
+ print("Generating figures...")
605
+ plot_roc_curves(ml, dl)
606
+ plot_confusion_matrix_lightgbm(ml)
607
+ plot_model_comparison(ml, dl)
608
+ plot_fold_std_table(ml, dl)
609
+ plot_ml_vs_dl(ml, dl)
610
+ plot_pipeline_diagram()
611
+ plot_feature_importance(ml)
612
+ plot_score_distribution(ml)
613
+ plot_calibration(ml)
614
+ plot_precision_recall(ml)
615
+
616
+ print(f"\nDone. All figures saved to {OUT}")
617
+
618
+
619
+ if __name__ == "__main__":
620
+ main()