File size: 5,787 Bytes
e35b73e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | #!/usr/bin/env python3
"""
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]
# ββ Load ground truth ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 # 100 entries, "T" or "F"
# ββ Load baseline trans_query_eval.csv (end-to-end via baseline UPPAAL model) ββ
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
# ββ Load our e2e results ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
# ββ Confusion matrix (normalized by true label) βββββββββββββββββββββββββββββββ
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
# normalize by row
row_sums = cm.sum(axis=1, keepdims=True)
row_sums[row_sums == 0] = 1
return cm / row_sums, cm # normalized, raw
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
# ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 stats table βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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)
# ββ Plot: 1 row of 4 normalized confusion matrices βββββββββββββββββββββββββββ
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)
# Cell annotations: normalized value + raw count
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()
|