| |
| """ |
| Normalized confusion matrices + accuracy analysis for all four systems |
| vs ground truth from query_translation_eval.csv. |
| """ |
|
|
| from __future__ import annotations |
| import csv, re |
| from pathlib import Path |
| import numpy as np |
| import matplotlib.pyplot as plt |
| import matplotlib.gridspec as gridspec |
|
|
| ROOT = Path(__file__).resolve().parents[1] |
|
|
| |
| def load_ground_truth() -> list[str]: |
| gt = [] |
| with (ROOT / "datasets" / "query_translation_eval.csv").open(encoding="utf-8") as f: |
| for row in csv.DictReader(f): |
| gt.append(row["answer"].strip().upper()) |
| return gt |
|
|
| |
| def load_raw_qna(path: Path) -> list[str]: |
| preds = [] |
| for line in path.read_text(encoding="utf-8").splitlines(): |
| line = line.strip() |
| if not line: |
| continue |
| m = re.search(r":\s*([TF])\s*$", line, re.IGNORECASE) |
| if m: |
| preds.append(m.group(1).upper()) |
| return preds |
|
|
| |
| def load_ours() -> list[str]: |
| preds = [] |
| with (ROOT / "artifacts" / "e2e_eval" / "e2e_query_eval.csv").open(encoding="utf-8") as f: |
| for row in csv.DictReader(f): |
| v = row["verdict"].strip().upper() |
| preds.append(v if v in ("T", "F") else "?") |
| return preds |
|
|
| |
| def confusion(gt: list[str], pred: list[str]): |
| """Returns 2x2 numpy array normalized by row (true label). |
| Rows = actual [T, F], Cols = predicted [T, F] |
| """ |
| cm = np.zeros((2, 2), dtype=float) |
| for g, p in zip(gt, pred): |
| if g not in ("T", "F") or p not in ("T", "F"): |
| continue |
| ri = 0 if g == "T" else 1 |
| ci = 0 if p == "T" else 1 |
| cm[ri, ci] += 1 |
| |
| row_sums = cm.sum(axis=1, keepdims=True) |
| row_sums[row_sums == 0] = 1 |
| return cm / row_sums, cm |
|
|
| def accuracy(gt, pred): |
| correct = sum(g == p for g, p in zip(gt, pred) if g in ("T","F") and p in ("T","F")) |
| total = sum(1 for g in gt if g in ("T","F")) |
| return correct / total if total else 0.0 |
|
|
| def precision_recall_f1(gt, pred): |
| tp = sum(1 for g,p in zip(gt,pred) if g=="T" and p=="T") |
| fp = sum(1 for g,p in zip(gt,pred) if g=="F" and p=="T") |
| fn = sum(1 for g,p in zip(gt,pred) if g=="T" and p=="F") |
| tn = sum(1 for g,p in zip(gt,pred) if g=="F" and p=="F") |
| prec = tp/(tp+fp) if (tp+fp) else 0.0 |
| rec = tp/(tp+fn) if (tp+fn) else 0.0 |
| f1 = 2*prec*rec/(prec+rec) if (prec+rec) else 0.0 |
| return prec, rec, f1, tp, fp, fn, tn |
|
|
| |
| gt = load_ground_truth() |
|
|
| systems = { |
| "Ours\n(pipeline)": load_ours(), |
| "Claude\n(direct)": load_raw_qna(ROOT / "artifacts/baselines/claude/raw_qna.txt"), |
| "GPT\n(direct)": load_raw_qna(ROOT / "artifacts/baselines/gpt/raw_qna.txt"), |
| "Grok\n(direct)": load_raw_qna(ROOT / "artifacts/baselines/grok/raw_qna.txt"), |
| } |
|
|
| |
| print(f"\n{'System':<22} {'Acc':>5} {'Prec':>5} {'Rec':>5} {'F1':>5} {'TP':>3} {'TN':>3} {'FP':>3} {'FN':>3}") |
| print("-" * 70) |
| stats = {} |
| for name, pred in systems.items(): |
| acc = accuracy(gt, pred) |
| prec, rec, f1, tp, fp, fn, tn = precision_recall_f1(gt, pred) |
| label = name.replace("\n", " ") |
| print(f"{label:<22} {acc:.3f} {prec:.3f} {rec:.3f} {f1:.3f} {tp:>3} {tn:>3} {fp:>3} {fn:>3}") |
| stats[name] = (acc, prec, rec, f1) |
|
|
| |
| fig, axes = plt.subplots(1, 4, figsize=(14, 4)) |
| fig.suptitle("Normalized Confusion Matrices β QA Verdict Prediction\n(rows = actual, cols = predicted; normalized by true label)", |
| fontsize=12, y=1.02) |
|
|
| labels = ["T (True)", "F (False)"] |
| cmap = "Blues" |
|
|
| for ax, (name, pred) in zip(axes, systems.items()): |
| cm_norm, cm_raw = confusion(gt, pred) |
| acc = stats[name][0] |
|
|
| im = ax.imshow(cm_norm, vmin=0, vmax=1, cmap=cmap) |
|
|
| |
| for i in range(2): |
| for j in range(2): |
| val = cm_norm[i, j] |
| raw = int(cm_raw[i, j]) |
| color = "white" if val > 0.6 else "black" |
| ax.text(j, i, f"{val:.2f}\n({raw})", ha="center", va="center", |
| fontsize=11, color=color, fontweight="bold") |
|
|
| ax.set_xticks([0, 1]); ax.set_yticks([0, 1]) |
| ax.set_xticklabels(labels, fontsize=9) |
| ax.set_yticklabels(labels, fontsize=9) |
| ax.set_xlabel("Predicted", fontsize=10) |
| ax.set_ylabel("Actual", fontsize=10) |
| ax.set_title(f"{name}\nAcc = {acc:.2f}", fontsize=10) |
|
|
| plt.colorbar(im, ax=axes[-1], fraction=0.046, pad=0.04, label="Proportion") |
| plt.tight_layout() |
| out = ROOT / "docs" / "confusion_matrices.png" |
| plt.savefig(out, dpi=150, bbox_inches="tight") |
| print(f"\nSaved -> {out}") |
| plt.show() |
|
|