Spaces:
Sleeping
Sleeping
Download plot_results.py from blackcow63/spreadsheetlabeller: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/spaces/blackcow63/spreadsheetlabeller/resolve/main/plot_results.py
- Command line
-
hf download hf://spaces/blackcow63/spreadsheetlabeller/plot_results.py
-
curl -L -o plot_results.py https://huggingface.co/spaces/blackcow63/spreadsheetlabeller/resolve/main/plot_results.py
14.5 kB
| """Generate NeurIPS-style figures and LaTeX tables from experiment results. | |
| Reads from ``data/experiments/all_results.json`` (output of train_compare.py) | |
| and ``data/rag_eval_results.json`` (output of eval_rag.py). | |
| Figures (PDF, NeurIPS column widths): | |
| 1. Architecture comparison (grouped bar chart) | |
| 2. Learning curves | |
| 3. Ablation waterfall | |
| 4. Feature group ablation | |
| 5. RAG retrieval recall | |
| 6. Confusion matrix heatmap | |
| LaTeX tables: | |
| main_results.tex, ablation.tex, feature_ablation.tex, dataset_stats.tex | |
| Usage: | |
| python plot_results.py # generate all outputs | |
| python plot_results.py --output-dir figs # custom output directory | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from collections import defaultdict | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| import config | |
| from label_utils import CELL_LABELS | |
| # Matplotlib setup for NeurIPS style | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from matplotlib.colors import LinearSegmentedColormap | |
| SINGLE_COL = 3.25 | |
| DOUBLE_COL = 6.75 | |
| FONT_SIZE = 8 | |
| COLORS = ["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B3", "#937860"] | |
| plt.rcParams.update({ | |
| "font.size": FONT_SIZE, | |
| "font.family": "serif", | |
| "axes.titlesize": FONT_SIZE + 1, | |
| "axes.labelsize": FONT_SIZE, | |
| "xtick.labelsize": FONT_SIZE - 1, | |
| "ytick.labelsize": FONT_SIZE - 1, | |
| "legend.fontsize": FONT_SIZE - 1, | |
| "figure.dpi": 300, | |
| "savefig.bbox": "tight", | |
| "savefig.pad_inches": 0.02, | |
| }) | |
| def _load_results() -> dict: | |
| path = config.EXPERIMENTS_DIR / "all_results.json" | |
| if not path.exists(): | |
| print(f"No results file at {path}. Run train_compare.py first.") | |
| return {} | |
| with open(path) as f: | |
| return json.load(f) | |
| def _load_rag_results() -> dict: | |
| path = config.DATA_DIR / "rag_eval_results.json" | |
| if not path.exists(): | |
| return {} | |
| with open(path) as f: | |
| return json.load(f) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Figure 1: Architecture comparison | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fig_architecture_comparison(results: dict, output_dir: Path): | |
| archs = list(results.keys()) | |
| if not archs: | |
| return | |
| metrics = ["macro_f1_mean", "chunk_f1_mean"] | |
| labels = ["Coarse Macro F1", "Chunk F1"] | |
| means = defaultdict(list) | |
| stds = defaultdict(list) | |
| for arch in archs: | |
| runs = results[arch] | |
| for m in metrics: | |
| vals = [r.get(m, 0) for r in runs] | |
| means[m].append(np.mean(vals)) | |
| stds[m].append(np.std(vals)) | |
| x = np.arange(len(archs)) | |
| width = 0.35 | |
| fig, ax = plt.subplots(figsize=(DOUBLE_COL, 2.5)) | |
| for i, (m, lbl) in enumerate(zip(metrics, labels)): | |
| ax.bar(x + i * width, means[m], width, yerr=stds[m], | |
| label=lbl, color=COLORS[i], capsize=2) | |
| ax.set_xticks(x + width / 2) | |
| ax.set_xticklabels(archs, rotation=15, ha="right") | |
| ax.set_ylabel("Score") | |
| ax.legend(loc="lower right") | |
| ax.set_ylim(0, 1) | |
| fig.savefig(output_dir / "fig1_architecture_comparison.pdf") | |
| plt.close(fig) | |
| print(" Figure 1: architecture comparison") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Figure 2: Learning curves (placeholder β needs training-subset data) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fig_learning_curves(results: dict, output_dir: Path): | |
| fig, ax = plt.subplots(figsize=(SINGLE_COL, 2.5)) | |
| ax.set_xlabel("Number of labeled sheets") | |
| ax.set_ylabel("Chunk F1") | |
| ax.set_title("Learning Curves") | |
| ax.text(0.5, 0.5, "Run with --learning-curves to populate", | |
| ha="center", va="center", transform=ax.transAxes, | |
| fontsize=FONT_SIZE, color="gray") | |
| fig.savefig(output_dir / "fig2_learning_curves.pdf") | |
| plt.close(fig) | |
| print(" Figure 2: learning curves (placeholder)") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Figure 3: Ablation waterfall | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fig_ablation_waterfall(results: dict, output_dir: Path): | |
| variants = ["A (Full)", "B (Heur. Tables)", "C (Coarse+LrnTbl)", "D (Baseline)"] | |
| values = [0.85, 0.80, 0.78, 0.72] | |
| fig, ax = plt.subplots(figsize=(SINGLE_COL, 2.5)) | |
| bars = ax.bar(variants, values, color=COLORS[:4]) | |
| for bar, v in zip(bars, values): | |
| ax.text(bar.get_x() + bar.get_width() / 2, v + 0.01, | |
| f"{v:.2f}", ha="center", fontsize=FONT_SIZE - 1) | |
| ax.set_ylabel("Chunk F1") | |
| ax.set_title("Ablation: Contribution of Learned Components") | |
| ax.set_ylim(0, 1) | |
| plt.xticks(rotation=15, ha="right") | |
| fig.savefig(output_dir / "fig3_ablation_waterfall.pdf") | |
| plt.close(fig) | |
| print(" Figure 3: ablation waterfall (placeholder values)") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Figure 4: Feature group ablation | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fig_feature_ablation(results: dict, output_dir: Path): | |
| groups = ["No text embed", "No formatting", "No formula", "No position"] | |
| drops = [0.12, 0.05, 0.03, 0.08] | |
| fig, ax = plt.subplots(figsize=(SINGLE_COL, 2.2)) | |
| bars = ax.barh(groups, drops, color=COLORS[0]) | |
| ax.set_xlabel("Chunk F1 Drop") | |
| ax.set_title("Feature Group Impact") | |
| for bar, d in zip(bars, drops): | |
| ax.text(d + 0.003, bar.get_y() + bar.get_height() / 2, | |
| f"-{d:.2f}", va="center", fontsize=FONT_SIZE - 1) | |
| fig.savefig(output_dir / "fig4_feature_ablation.pdf") | |
| plt.close(fig) | |
| print(" Figure 4: feature ablation (placeholder values)") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Figure 5: RAG retrieval | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fig_rag_retrieval(rag_results: dict, output_dir: Path): | |
| fig, ax = plt.subplots(figsize=(SINGLE_COL, 2.5)) | |
| ks = [1, 5, 10] | |
| for i, (method, label) in enumerate([ | |
| ("graph", "Graph-based"), | |
| ("naive", "Naive row"), | |
| ("single_cell", "Single cell"), | |
| ]): | |
| data = rag_results.get(method, {}) | |
| vals = [data.get(f"recall@{k}", 0) for k in ks] | |
| ax.plot(ks, vals, marker="o", label=label, color=COLORS[i]) | |
| ax.set_xlabel("K") | |
| ax.set_ylabel("Recall@K") | |
| ax.set_title("RAG Retrieval Quality") | |
| ax.legend() | |
| ax.set_xticks(ks) | |
| ax.set_ylim(0, 1.05) | |
| fig.savefig(output_dir / "fig5_rag_retrieval.pdf") | |
| plt.close(fig) | |
| print(" Figure 5: RAG retrieval") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Figure 6: Confusion matrix | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fig_confusion_matrix(results: dict, output_dir: Path): | |
| n = len(CELL_LABELS) | |
| cm = np.random.randint(0, 50, size=(n, n)) | |
| np.fill_diagonal(cm, np.random.randint(100, 500, size=n)) | |
| fig, ax = plt.subplots(figsize=(DOUBLE_COL, DOUBLE_COL * 0.85)) | |
| im = ax.imshow(cm, cmap="Blues") | |
| ax.set_xticks(range(n)) | |
| ax.set_yticks(range(n)) | |
| ax.set_xticklabels(CELL_LABELS, rotation=45, ha="right", fontsize=FONT_SIZE - 2) | |
| ax.set_yticklabels(CELL_LABELS, fontsize=FONT_SIZE - 2) | |
| ax.set_xlabel("Predicted") | |
| ax.set_ylabel("True") | |
| ax.set_title("Confusion Matrix (Best Model)") | |
| fig.colorbar(im, ax=ax, shrink=0.8) | |
| fig.savefig(output_dir / "fig6_confusion_matrix.pdf") | |
| plt.close(fig) | |
| print(" Figure 6: confusion matrix (placeholder)") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # LaTeX tables | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _export_main_results_table(results: dict, output_dir: Path): | |
| rows = [] | |
| for arch, runs in results.items(): | |
| macro_f1 = np.mean([r.get("macro_f1_mean", 0) for r in runs]) | |
| chunk_f1 = np.mean([r.get("chunk_f1_mean", 0) for r in runs]) | |
| acc = np.mean([r.get("best_acc", 0) for r in runs]) | |
| params = runs[0].get("num_params", 0) if runs else 0 | |
| rows.append({ | |
| "Architecture": arch, | |
| "Params": f"{params:,}", | |
| "Coarse F1": f"{macro_f1:.3f}", | |
| "Chunk F1": f"{chunk_f1:.3f}", | |
| "Accuracy": f"{acc:.3f}", | |
| }) | |
| df = pd.DataFrame(rows) | |
| latex = df.to_latex(index=False, escape=False, column_format="lrrrr") | |
| (output_dir / "main_results.tex").write_text(latex) | |
| print(" Table: main_results.tex") | |
| def _export_dataset_stats_table(output_dir: Path): | |
| rows = [ | |
| {"Statistic": "Total sheets", "Value": "29"}, | |
| {"Statistic": "Total labeled cells", "Value": "~24K"}, | |
| {"Statistic": "Total unlabeled cells", "Value": "~133K"}, | |
| {"Statistic": "Multi-table sheets", "Value": "9"}, | |
| {"Statistic": "Max col header depth", "Value": "2"}, | |
| {"Statistic": "Max row header depth", "Value": "1"}, | |
| {"Statistic": "Fine-grained classes", "Value": "13"}, | |
| {"Statistic": "Coarse classes", "Value": "9"}, | |
| ] | |
| df = pd.DataFrame(rows) | |
| latex = df.to_latex(index=False, escape=False, column_format="lr") | |
| (output_dir / "dataset_stats.tex").write_text(latex) | |
| print(" Table: dataset_stats.tex") | |
| def _export_ablation_table(output_dir: Path): | |
| rows = [ | |
| {"Variant": "A (Full)", "Cell Type": "Fine (13)", "Table": "Learned", | |
| "Levels": "Learned", "Chunk F1": "β"}, | |
| {"Variant": "B", "Cell Type": "Fine (13)", "Table": "Heuristic", | |
| "Levels": "Learned", "Chunk F1": "β"}, | |
| {"Variant": "C", "Cell Type": "Coarse (9)", "Table": "Learned", | |
| "Levels": "Heuristic", "Chunk F1": "β"}, | |
| {"Variant": "D (Baseline)", "Cell Type": "Coarse (9)", "Table": "Heuristic", | |
| "Levels": "Heuristic", "Chunk F1": "β"}, | |
| ] | |
| df = pd.DataFrame(rows) | |
| latex = df.to_latex(index=False, escape=False, column_format="lllll") | |
| (output_dir / "ablation.tex").write_text(latex) | |
| print(" Table: ablation.tex") | |
| def _export_feature_ablation_table(output_dir: Path): | |
| rows = [ | |
| {"Features": "Full (792)", "Chunk F1": "β"}, | |
| {"Features": "No text embeddings (24)", "Chunk F1": "β"}, | |
| {"Features": "No formatting", "Chunk F1": "β"}, | |
| {"Features": "No formula", "Chunk F1": "β"}, | |
| {"Features": "No position", "Chunk F1": "β"}, | |
| ] | |
| df = pd.DataFrame(rows) | |
| latex = df.to_latex(index=False, escape=False, column_format="lr") | |
| (output_dir / "feature_ablation.tex").write_text(latex) | |
| print(" Table: feature_ablation.tex") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_all(output_dir: Path | None = None): | |
| if output_dir is None: | |
| output_dir = config.DATA_DIR / "figures" | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| results = _load_results() | |
| rag_results = _load_rag_results() | |
| print(f"Generating figures and tables in {output_dir}\n") | |
| # Figures | |
| fig_architecture_comparison(results, output_dir) | |
| fig_learning_curves(results, output_dir) | |
| fig_ablation_waterfall(results, output_dir) | |
| fig_feature_ablation(results, output_dir) | |
| fig_rag_retrieval(rag_results, output_dir) | |
| fig_confusion_matrix(results, output_dir) | |
| # Tables | |
| print() | |
| _export_main_results_table(results, output_dir) | |
| _export_dataset_stats_table(output_dir) | |
| _export_ablation_table(output_dir) | |
| _export_feature_ablation_table(output_dir) | |
| print(f"\nDone. All outputs in {output_dir}") | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Generate NeurIPS figures and tables") | |
| parser.add_argument("--output-dir", type=str, help="Output directory for figures") | |
| args = parser.parse_args() | |
| od = Path(args.output_dir) if args.output_dir else None | |
| generate_all(od) | |