#!/usr/bin/env python # -*- coding: utf-8 -*- """P...

創建於:2026年11月1日

使用 GPT-5.6 Thinking 回答 Chat01

問題

effect.png

#!/usr/bin/env python

-- coding: utf-8 --

"""Publication-quality plot for the client-scaling (n=3) PFLAD run."""

import csv
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

HERE = Path(file).resolve().parent
CSV_PATH = HERE / "pflad_scaling_01_n3_results.csv"
OUT_PDF = HERE / "pflad_scaling_01_n3_results.pdf"

plt.rcParams.update({
"font.size": 11,
"font.family": "serif",
"font.serif": ["Times New Roman", "DejaVu Serif"],
"axes.labelsize": 12,
"axes.titlesize": 12,
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"legend.fontsize": 10,
"lines.linewidth": 1.8,
"axes.linewidth": 0.8,
"figure.dpi": 300,
"savefig.dpi": 300,
"savefig.bbox": "tight",
"savefig.pad_inches": 0.05,
"pdf.fonttype": 42,
"ps.fonttype": 42,
})

rounds, loss, auroc, auprc = [], [], [], []
with open(CSV_PATH, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
rounds.append(int(row["Round"]))
loss.append(float(row["Train_Loss"]))
auroc.append(float(row["AUROC"]))
auprc.append(float(row["AUPRC"]))

COLOR_LOSS = "#377eb8"
COLOR_AUROC = "#e41a1c"
COLOR_AUPRC = "#4daf4a"

fig, axes = plt.subplots(1, 3, figsize=(10.5, 3.2))

panels = [
(axes[0], loss, "Training Loss", COLOR_LOSS, "o", "(a)"),
(axes[1], auroc, "AUROC", COLOR_AUROC, "s", "(b)"),
(axes[2], auprc, "AUPRC", COLOR_AUPRC, "^", "(c)"),
]

for ax, values, ylabel, color, marker, label in panels:
ax.plot(
rounds, values,
color=color, marker=marker, markersize=4.5,
markerfacecolor="white", markeredgewidth=1.2,
markevery=1, alpha=0.95, zorder=3,
)
ax.set_xlabel("Communication Round")
ax.set_ylabel(ylabel)
ax.set_title(label, fontweight="bold", loc="left", fontsize=11)
ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.4, zorder=0)
ax.xaxis.set_major_locator(mticker.MultipleLocator(20))
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.margins(x=0.02)

fig.suptitle(
"PFLAD Client-Scaling Experiment (N = 3 clients, 1 selected/round)",
fontsize=12, fontweight="bold", y=1.03,
)

fig.tight_layout()
fig.savefig(OUT_PDF)
fig.savefig(OUT_PDF.with_suffix(".png"))
print(f"Saved: {OUT_PDF}")
print(f"Saved: {OUT_PDF.with_suffix('.png')}")#!/usr/bin/env python

-- coding: utf-8 --

"""Publication-quality figure for the client-scaling experiment (effect.png).

Plots PFLAD AUROC/AUPRC vs. number of clients and annotates the gap
(delta) of each configuration relative to the N=3 baseline.
"""

import csv
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker

HERE = Path(file).resolve().parent
CSV_PATH = HERE / "client_scaling_summary.csv"
OUT_PDF = HERE / "effect.pdf"
OUT_PNG = HERE / "effect.png"

plt.rcParams.update({
"font.size": 11,
"font.family": "serif",
"font.serif": ["Times New Roman", "DejaVu Serif"],
"axes.labelsize": 12,
"axes.titlesize": 12,
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"legend.fontsize": 9.5,
"lines.linewidth": 1.8,
"axes.linewidth": 0.8,
"figure.dpi": 300,
"savefig.dpi": 300,
"savefig.bbox": "tight",
"savefig.pad_inches": 0.05,
"pdf.fonttype": 42,
"ps.fonttype": 42,
})

rows = []
with open(CSV_PATH, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
rows.append({
"num_clients": int(row["num_clients"]),
"num_selected": int(row["num_selected_clients"]),
"ratio": float(row["participation_ratio"]),
"auroc": float(row["final_pflad_auroc"]),
"auprc": float(row["final_pflad_auprc"]),
})
rows.sort(key=lambda r: r["num_clients"])

baseline = rows[0]
num_clients = [r["num_clients"] for r in rows]
auroc = [r["auroc"] for r in rows]
auprc = [r["auprc"] for r in rows]
gap_auroc = [r["auroc"] - baseline["auroc"] for r in rows]
gap_auprc = [r["auprc"] - baseline["auprc"] for r in rows]

xtick_labels = [f"{r['num_clients']}\n({r['ratio']*100:.0f}% sel.)" for r in rows]

COLOR_AUROC = "#e41a1c"
COLOR_AUPRC = "#4daf4a"
COLOR_GAP_POS = "#2166ac"
COLOR_GAP_NEG = "#b2182b"
COLOR_BASELINE = "#4d4d4d"

fig, axes = plt.subplots(1, 2, figsize=(9.5, 3.8))

panels = [
(axes[0], auroc, gap_auroc, "AUROC", COLOR_AUROC, "s", "(a)"),
(axes[1], auprc, gap_auprc, "AUPRC", COLOR_AUPRC, "^", "(b)"),
]

for ax, values, gaps, ylabel, color, marker, label in panels:
ax.plot(
num_clients, values,
color=color, marker=marker, markersize=6,
markerfacecolor="white", markeredgewidth=1.4,
alpha=0.95, zorder=3, label=ylabel,
)
ax.axhline(
baseline["auroc"] if ylabel == "AUROC" else baseline["auprc"],
color=COLOR_BASELINE, linestyle="--", linewidth=1.0, alpha=0.6,
zorder=1, label=f"N={baseline['num_clients']} baseline",
)

text
y_span = max(values) - min(values) if max(values) != min(values) else 0.01 for x, y, g in zip(num_clients, values, gaps): if x == baseline["num_clients"]: continue gap_color = COLOR_GAP_POS if g >= 0 else COLOR_GAP_NEG sign = "+" if g >= 0 else "" ax.annotate( f"{sign}{g:.4f}", xy=(x, y), xytext=(0, 10 if y >= baseline["auroc" if ylabel == "AUROC" else "auprc"] else -16), textcoords="offset points", ha="center", fontsize=8.5, color=gap_color, fontweight="bold", zorder=4, ) ax.set_xlabel("Number of Clients (N)") ax.set_ylabel(ylabel) ax.set_title(label, fontweight="bold", loc="left", fontsize=11) ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.4, zorder=0) ax.xaxis.set_major_locator(mticker.FixedLocator(num_clients)) ax.set_xticklabels(xtick_labels) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.margins(x=0.15, y=0.3) ax.legend(loc="upper right", frameon=False)

fig.suptitle(
"Effect of Number of Clients on PFLAD Performance "
f"(gap relative to N={baseline['num_clients']} baseline)",
fontsize=12, fontweight="bold", y=1.02,
)

fig.tight_layout()
fig.savefig(OUT_PDF)
fig.savefig(OUT_PNG)
print(f"Saved: {OUT_PDF}")
print(f"Saved: {OUT_PNG}") 修改代码,这次我们为了可视化client_scaling_20261031_224126文件夹下6个*.csv单个pdf图片并汇总统计处理成类似effect.png风格的pdf图片

思考

设计汇总图表

我会把代码改成自动读取文件夹中的六个 CSV,分别生成单个 PDF,并额外生成一张类似 effect.png 风格的汇总 PDF,包含 AUROC、AUPRC 和训练损失曲线。

Designed summary charts

回答

我会把这两段脚本合并成一个批处理版本:自动扫描 client_scaling_20261031_224126 下 6 个 CSV,为每个运行生成 3-panel 单独 PDF,同时提取最终 AUROC/AUPRC,生成一张类似你给的 effect.png 风格的汇总 PDF/PNG。代码会尽量兼容文件名里类似 n3、n6 这样的 client 数标记。

思考

完善脚本设计

I’ll make the script scan all six CSV files, generate one PDF per file, then aggregate repeated client settings with mean and standard deviation. The summary PDF will match the reference style with metric curves, baselines, gaps, labels, and clean formatting.

Worked for 19s

Python

python
from pathlib import Path import py_compile code = r'''#!/usr/bin/env python # -*- coding: utf-8 -*- """ Batch visualization for client_scaling_20261031_224126. What this script does --------------------- 1. Scan the 6 CSV files under client_scaling_20261031_224126/. 2. Generate one publication-quality PDF for every CSV: Training Loss / AUROC / AUPRC vs. Communication Round 3. Extract run-level statistics and write: figures/client_scaling_summary.csv 4. Aggregate results by number of clients and generate an effect-style summary: figures/effect.pdf figures/effect.png Required metric columns ----------------------- Round, Train_Loss, AUROC, AUPRC Number of clients is inferred in this order: 1. CSV column such as: num_clients / Num_Clients / N 2. MANUAL_N_CLIENTS mapping below 3. filename pattern such as: n3, n6, n12, clients_24, clients24 Selected clients is inferred similarly. If unavailable, it defaults to 1. """ import csv import math import re from collections import defaultdict from pathlib import Path from statistics import mean, stdev import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import matplotlib.ticker as mticker # --------------------------------------------------------------------- # Paths / experiment settings # --------------------------------------------------------------------- HERE = Path(__file__).resolve().parent DATA_DIR = HERE / "client_scaling_20261031_224126" OUT_DIR = DATA_DIR / "figures" INDIVIDUAL_DIR = OUT_DIR / "individual" SUMMARY_CSV = OUT_DIR / "client_scaling_summary.csv" EFFECT_PDF = OUT_DIR / "effect.pdf" EFFECT_PNG = OUT_DIR / "effect.png" EXPECTED_CSV_COUNT = 6 # Match the behavior of your previous effect.py: # "final" -> use the final communication round # "tail_mean" -> average the last TAIL_K communication rounds SUMMARY_MODE = "final" TAIL_K = 5 # Preferred baseline. If N=3 is not present, the smallest N is used. BASELINE_N = 3 # If filenames do not contain n3/n6/... and the CSV has no metadata column, # fill these dictionaries, for example: # MANUAL_N_CLIENTS = { # "pflad_scaling_01_results.csv": 3, # "pflad_scaling_02_results.csv": 6, # } MANUAL_N_CLIENTS = {} # Optional override. Otherwise selected clients defaults to 1. MANUAL_SELECTED_CLIENTS = {} # --------------------------------------------------------------------- # Plot style # --------------------------------------------------------------------- plt.rcParams.update({ "font.size": 11, "font.family": "serif", "font.serif": ["Times New Roman", "DejaVu Serif"], "axes.labelsize": 12, "axes.titlesize": 12, "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.fontsize": 9.5, "lines.linewidth": 1.8, "axes.linewidth": 0.8, "figure.dpi": 300, "savefig.dpi": 300, "savefig.bbox": "tight", "savefig.pad_inches": 0.05, "pdf.fonttype": 42, "ps.fonttype": 42, }) COLOR_LOSS = "#377eb8" COLOR_AUROC = "#e41a1c" COLOR_AUPRC = "#4daf4a" COLOR_GAP_POS = "#2166ac" COLOR_GAP_NEG = "#b2182b" COLOR_BASELINE = "#4d4d4d" COLOR_GAP_BOX = "#f4b6b6" # --------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------- def _find_key(fieldnames, candidates): """Case-insensitive column lookup.""" if not fieldnames: return None lookup = {name.strip().lower(): name for name in fieldnames} for candidate in candidates: key = lookup.get(candidate.lower()) if key is not None: return key return None def _as_int(value): if value is None: return None text = str(value).strip() if not text: return None return int(float(text)) def infer_num_clients(csv_path, first_row, fieldnames): """Infer N from CSV metadata, manual mapping, or filename.""" key = _find_key( fieldnames, ["num_clients", "number_of_clients", "clients", "n_clients", "N"], ) if key is not None: value = _as_int(first_row.get(key)) if value is not None: return value if csv_path.name in MANUAL_N_CLIENTS: return int(MANUAL_N_CLIENTS[csv_path.name]) stem = csv_path.stem.lower() patterns = [ r"(?:^|[_-])n[_-]?(\d+)(?:[_-]|$)", r"(?:^|[_-])clients?[_-]?(\d+)(?:[_-]|$)", r"(?:^|[_-])num[_-]?clients?[_-]?(\d+)(?:[_-]|$)", ] for pattern in patterns: match = re.search(pattern, stem) if match: return int(match.group(1)) raise ValueError( f"Cannot infer number of clients from {csv_path.name}. " "Add num_clients to the CSV, include n3/n6/... in the filename, " "or fill MANUAL_N_CLIENTS." ) def infer_num_selected(csv_path, first_row, fieldnames): """Infer selected clients; default to 1 when not encoded.""" key = _find_key( fieldnames, [ "num_selected_clients", "selected_clients", "num_selected", "clients_per_round", "k", ], ) if key is not None: value = _as_int(first_row.get(key)) if value is not None: return value if csv_path.name in MANUAL_SELECTED_CLIENTS: return int(MANUAL_SELECTED_CLIENTS[csv_path.name]) stem = csv_path.stem.lower() patterns = [ r"(?:^|[_-])selected[_-]?(\d+)(?:[_-]|$)", r"(?:^|[_-])sel[_-]?(\d+)(?:[_-]|$)", r"(?:^|[_-])k[_-]?(\d+)(?:[_-]|$)", r"(?:^|[_-])m[_-]?(\d+)(?:[_-]|$)", ] for pattern in patterns: match = re.search(pattern, stem) if match: return int(match.group(1)) return 1 def read_run(csv_path): """Read one run and normalize the required columns.""" with csv_path.open(newline="", encoding="utf-8-sig") as f: reader = csv.DictReader(f) fieldnames = reader.fieldnames or [] round_key = _find_key(fieldnames, ["Round", "communication_round", "round"]) loss_key = _find_key(fieldnames, ["Train_Loss", "train_loss", "loss"]) auroc_key = _find_key(fieldnames, ["AUROC", "auroc"]) auprc_key = _find_key(fieldnames, ["AUPRC", "auprc"]) required = { "Round": round_key, "Train_Loss": loss_key, "AUROC": auroc_key, "AUPRC": auprc_key, } missing = [name for name, key in required.items() if key is None] if missing: raise ValueError( f"{csv_path.name}: missing required column(s): {', '.join(missing)}. " f"Available columns: {fieldnames}" ) raw_rows = list(reader) if not raw_rows: raise ValueError(f"{csv_path.name}: CSV is empty.") first_row = raw_rows[0] num_clients = infer_num_clients(csv_path, first_row, fieldnames) num_selected = infer_num_selected(csv_path, first_row, fieldnames) rows = [] for row in raw_rows: try: rows.append({ "round": int(float(row[round_key])), "loss": float(row[loss_key]), "auroc": float(row[auroc_key]), "auprc": float(row[auprc_key]), }) except (TypeError, ValueError): # Skip malformed / incomplete rows instead of crashing on one row. continue if not rows: raise ValueError(f"{csv_path.name}: no valid metric rows found.") rows.sort(key=lambda r: r["round"]) return { "path": csv_path, "name": csv_path.name, "stem": csv_path.stem, "num_clients": num_clients, "num_selected": num_selected, "participation_ratio": num_selected / num_clients, "rows": rows, } def summarize_values(values): """Return final value, tail mean, and best value.""" tail = values[-min(TAIL_K, len(values)):] return { "final": values[-1], "tail_mean": mean(tail), "best_max": max(values), } def summarize_run(run): rows = run["rows"] losses = [r["loss"] for r in rows] aurocs = [r["auroc"] for r in rows] auprcs = [r["auprc"] for r in rows] loss_stats = summarize_values(losses) auroc_stats = summarize_values(aurocs) auprc_stats = summarize_values(auprcs) return { "file": run["name"], "num_clients": run["num_clients"], "num_selected_clients": run["num_selected"], "participation_ratio": run["participation_ratio"], "num_rounds": len(rows), "last_round": rows[-1]["round"], "final_train_loss": loss_stats["final"], "final_auroc": auroc_stats["final"], "final_auprc": auprc_stats["final"], f"mean_last_{TAIL_K}_train_loss": loss_stats["tail_mean"], f"mean_last_{TAIL_K}_auroc": auroc_stats["tail_mean"], f"mean_last_{TAIL_K}_auprc": auprc_stats["tail_mean"], "best_auroc": auroc_stats["best_max"], "best_auprc": auprc_stats["best_max"], } def choose_metric(summary_row, metric): if SUMMARY_MODE == "final": return summary_row[f"final_{metric}"] if SUMMARY_MODE == "tail_mean": return summary_row[f"mean_last_{TAIL_K}_{metric}"] raise ValueError("SUMMARY_MODE must be 'final' or 'tail_mean'.") # --------------------------------------------------------------------- # Per-CSV figure # --------------------------------------------------------------------- def plot_individual_run(run): rows = run["rows"] rounds = [r["round"] for r in rows] loss = [r["loss"] for r in rows] auroc = [r["auroc"] for r in rows] auprc = [r["auprc"] for r in rows] fig, axes = plt.subplots(1, 3, figsize=(10.5, 3.2)) panels = [ (axes[0], loss, "Training Loss", COLOR_LOSS, "o", "(a)"), (axes[1], auroc, "AUROC", COLOR_AUROC, "s", "(b)"), (axes[2], auprc, "AUPRC", COLOR_AUPRC, "^", "(c)"), ] for ax, values, ylabel, color, marker, label in panels: ax.plot( rounds, values, color=color, marker=marker, markersize=4.5, markerfacecolor="white", markeredgewidth=1.2, markevery=1, alpha=0.95, zorder=3, ) ax.set_xlabel("Communication Round") ax.set_ylabel(ylabel) ax.set_title(label, fontweight="bold", loc="left", fontsize=11) ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.4, zorder=0) ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins=6, integer=True)) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.margins(x=0.02) fig.suptitle( "PFLAD Client-Scaling Experiment " f"(N = {run['num_clients']} clients, " f"{run['num_selected']} selected/round, " f"{run['participation_ratio'] * 100:.1f}% participation)", fontsize=12, fontweight="bold", y=1.03, ) fig.tight_layout() out_pdf = INDIVIDUAL_DIR / f"{run['stem']}.pdf" fig.savefig(out_pdf) plt.close(fig) return out_pdf # --------------------------------------------------------------------- # Run-level summary CSV # --------------------------------------------------------------------- def write_summary_csv(summary_rows): fieldnames = [ "file", "num_clients", "num_selected_clients", "participation_ratio", "num_rounds", "last_round", "final_train_loss", "final_auroc", "final_auprc", f"mean_last_{TAIL_K}_train_loss", f"mean_last_{TAIL_K}_auroc", f"mean_last_{TAIL_K}_auprc", "best_auroc", "best_auprc", ] with SUMMARY_CSV.open("w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(summary_rows) # --------------------------------------------------------------------- # Aggregate by N # --------------------------------------------------------------------- def aggregate_by_num_clients(summary_rows): groups = defaultdict(list) for row in summary_rows: groups[row["num_clients"]].append(row) aggregated = [] for n in sorted(groups): group = groups[n] selected_values = [r["num_selected_clients"] for r in group] ratio_values = [r["participation_ratio"] for r in group] auroc_values = [choose_metric(r, "auroc") for r in group] auprc_values = [choose_metric(r, "auprc") for r in group] def safe_std(values): return stdev(values) if len(values) > 1 else 0.0 aggregated.append({ "num_clients": n, "num_selected": int(round(mean(selected_values))), "participation_ratio": mean(ratio_values), "n_runs": len(group), "auroc_mean": mean(auroc_values), "auroc_std": safe_std(auroc_values), "auprc_mean": mean(auprc_values), "auprc_std": safe_std(auprc_values), }) return aggregated # --------------------------------------------------------------------- # effect.png-style summary figure # --------------------------------------------------------------------- def plot_effect(aggregated): if not aggregated: raise ValueError("No aggregated rows to plot.") baseline = next( (row for row in aggregated if row["num_clients"] == BASELINE_N), aggregated[0], ) x = [row["num_clients"] for row in aggregated] xtick_labels = [ f"{row['num_clients']}\n({row['participation_ratio'] * 100:.0f}% sel.)" for row in aggregated ] fig, axes = plt.subplots(1, 2, figsize=(9.5, 3.8)) panels = [ ( axes[0], "auroc_mean", "auroc_std", "AUROC", COLOR_AUROC, "s", "(a)", ), ( axes[1], "auprc_mean", "auprc_std", "AUPRC", COLOR_AUPRC, "^", "(b)", ), ] for ax, mean_key, std_key, ylabel, color, marker, panel_label in panels: values = [row[mean_key] for row in aggregated] errors = [row[std_key] for row in aggregated] baseline_value = baseline[mean_key] has_error = any(err > 0 for err in errors) if has_error: ax.errorbar( x, values, yerr=errors, color=color, marker=marker, markersize=6, markerfacecolor="white", markeredgewidth=1.4, capsize=3, alpha=0.95, zorder=3, label=ylabel, ) else: ax.plot( x, values, color=color, marker=marker, markersize=6, markerfacecolor="white", markeredgewidth=1.4, alpha=0.95, zorder=3, label=ylabel, ) ax.axhline( baseline_value, color=COLOR_BASELINE, linestyle="--", linewidth=1.0, alpha=0.65, zorder=1, label=f"N={baseline['num_clients']} baseline", ) # Gap annotations relative to the baseline. value_span = max(values) - min(values) pad = max(value_span * 0.08, 0.002) for row, value in zip(aggregated, values): n = row["num_clients"] if n == baseline["num_clients"]: # Show the baseline value itself. ax.annotate( f"{value:.4f}", xy=(n, value), xytext=(0, 12), textcoords="offset points", ha="center", fontsize=8.5, color=color, fontweight="bold", zorder=5, ) continue gap = value - baseline_value gap_color = COLOR_GAP_POS if gap >= 0 else COLOR_GAP_NEG sign = "+" if gap >= 0 else "" offset_y = 13 if gap >= 0 else -20 ax.annotate( f"Gap: {sign}{gap:.4f}", xy=(n, value), xytext=(0, offset_y), textcoords="offset points", ha="center", fontsize=8.2, color="black", bbox=dict( boxstyle="round,pad=0.25", facecolor=COLOR_GAP_BOX, edgecolor="#888888", linewidth=0.7, alpha=0.9, ), arrowprops=dict( arrowstyle="->", color="#666666", linewidth=0.7, shrinkA=3, shrinkB=4, ), zorder=5, ) ax.set_xlabel("Number of Clients (N)") ax.set_ylabel(ylabel) ax.set_title(panel_label, fontweight="bold", loc="left", fontsize=11) ax.grid(True, linestyle="--", linewidth=0.5, alpha=0.4, zorder=0) ax.xaxis.set_major_locator(mticker.FixedLocator(x)) ax.set_xticklabels(xtick_labels) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.margins(x=0.12, y=0.25) ax.legend(loc="best", frameon=False) # Prevent an overly tight y-axis when all values are nearly identical. ymin, ymax = ax.get_ylim() if math.isclose(ymin, ymax): ax.set_ylim(ymin - pad, ymax + pad) metric_label = ( "final round" if SUMMARY_MODE == "final" else f"mean of last {TAIL_K} rounds" ) fig.suptitle( "Effect of Number of Clients on PFLAD Performance " f"({metric_label}; gap relative to N={baseline['num_clients']})", fontsize=12, fontweight="bold", y=1.02, ) fig.tight_layout() fig.savefig(EFFECT_PDF) fig.savefig(EFFECT_PNG) plt.close(fig) # --------------------------------------------------------------------- # Main # --------------------------------------------------------------------- def main(): if not DATA_DIR.exists(): raise FileNotFoundError( f"Data directory does not exist: {DATA_DIR}\n" "Place this script next to client_scaling_20261031_224126/ " "or change DATA_DIR." ) OUT_DIR.mkdir(parents=True, exist_ok=True) INDIVIDUAL_DIR.mkdir(parents=True, exist_ok=True) csv_paths = sorted( p for p in DATA_DIR.glob("*.csv") if p.name != SUMMARY_CSV.name ) if not csv_paths: raise FileNotFoundError(f"No CSV files found under: {DATA_DIR}") if len(csv_paths) != EXPECTED_CSV_COUNT: print( f"[Warning] Expected {EXPECTED_CSV_COUNT} CSV files, " f"but found {len(csv_paths)}." ) runs = [] summary_rows = [] print(f"Found {len(csv_paths)} CSV file(s).") for csv_path in csv_paths: run = read_run(csv_path) runs.append(run) out_pdf = plot_individual_run(run) summary = summarize_run(run) summary_rows.append(summary) print( f"[OK] {csv_path.name}: " f"N={run['num_clients']}, selected={run['num_selected']}, " f"final AUROC={summary['final_auroc']:.6f}, " f"final AUPRC={summary['final_auprc']:.6f}" ) print(f" saved: {out_pdf}") summary_rows.sort(key=lambda r: (r["num_clients"], r["file"])) write_summary_csv(summary_rows) aggregated = aggregate_by_num_clients(summary_rows) plot_effect(aggregated) print() print(f"Saved summary CSV: {SUMMARY_CSV}") print(f"Saved effect PDF : {EFFECT_PDF}") print(f"Saved effect PNG : {EFFECT_PNG}") if __name__ == "__main__": main() ''' path = Path("/mnt/data/plot_client_scaling_batch.py") path.write_text(code, encoding="utf-8") py_compile.compile(str(path), doraise=True) print(f"Created and syntax-checked: {path}")

回答

已经把两段代码合并成一个批处理脚本,并做了语法检查:

下载 plot_client_scaling_batch.py

它会自动处理:

  • 扫描 client_scaling_20261031_224126/ 下的 6 个 *.csv
  • 每个 CSV 单独生成一个 3-panel PDF:Training Loss / AUROC / AUPRC
  • 自动从文件名中的 n3、n6、n12 等识别 client 数
  • 默认每轮选择 1 个 client,也支持从 CSV/文件名读取 selected clients
  • 汇总每个实验的 final / last-5 mean / best AUROC、AUPRC 等统计量
  • 生成 client_scaling_summary.csv
  • 按 client 数排序,生成类似你给的 effect.png 风格的汇总图,包括 baseline 虚线和相对于 N=3 的 Gap 标注
  • 如果相同 N 有多个实验,会自动计算 mean/std,并在汇总图上画 error bar

预期目录结构是:

text
your_project/ ├── plot_client_scaling_batch.py └── client_scaling_20261031_224126/ ├── pflad_scaling_01_n3_results.csv ├── pflad_scaling_02_n6_results.csv ├── ... └── *.csv

直接运行:

bash
python plot_client_scaling_batch.py

输出会放到:

text
client_scaling_20261031_224126/ └── figures/ ├── individual/ │ ├── xxx_n3_results.pdf │ ├── xxx_n6_results.pdf │ └── ... # 6 个单独 PDF ├── client_scaling_summary.csv ├── effect.pdf # 最终论文汇总图 └── effect.png

当前汇总图默认使用最后一轮的 AUROC/AUPRC,和你之前的 effect.py 保持一致。如果想降低最后一轮波动的影响,只需要把:

python
SUMMARY_MODE = "final"

改为:

python
SUMMARY_MODE = "tail_mean" TAIL_K = 5

就会改成最后 5 轮均值。

目前你只上传了 effect.png,没有上传 client_scaling_20261031_224126 里的 6 个 CSV,所以我已经验证脚本语法无误,但还不能对真实数据实际生成最终 7 张 PDF。

分享這個問答