spreadsheetlabeller / plot_results.py
zofiasmolenasana
Add evaluation, RAG eval, and training comparison scripts
ce594de unverified
Raw History Blame Contribute Delete
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)