File size: 5,343 Bytes
465b477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Markdown tables and plots from ``metrics.json``. Numbers come only from the run directory."""

from __future__ import annotations

import json
import math
from pathlib import Path

HEADLINE = [
    ("correct", "Accuracy"),
    ("path_all_valid", "Path valid (all)"),
    ("path_completeness", "Path completeness"),
    ("citation_rate", "Citation rate"),
    ("grounding_precision", "Grounding precision"),
    ("false_premise_rejected", "False-premise rejection"),
    ("conflict_flagged", "Conflict flagged"),
    ("cost_usd", "Cost / item ($)"),
]


def fmt(cell: dict, pct: bool = True) -> str:
    m = cell.get("mean")
    if m is None or (isinstance(m, float) and math.isnan(m)):
        return "n/a"
    if pct:
        return f"{100 * m:.1f} [{100 * cell['low']:.1f}, {100 * cell['high']:.1f}]"
    return f"{m:.4f}"


def _table(title: str, block: dict, keys: list[tuple[str, str]]) -> list[str]:
    lines = [title, "", "| Method | n | errors | " + " | ".join(h for _, h in keys) + " |", "|---" * (len(keys) + 3) + "|"]
    for m, agg in block.items():
        cells = [fmt(agg[k], pct=(k != "cost_usd")) for k, _ in keys]
        lines.append(f"| {m} | {agg['n']} | {agg.get('errors', 0)} | " + " | ".join(cells) + " |")
    return lines + [""]


def markdown(metrics: dict) -> str:
    cats = ", ".join(metrics.get("headline_categories", []))
    lines = [f"Evaluation judge: **{metrics.get('judge_status', 'unknown')}**.", ""]
    headline_keys = [kv for kv in HEADLINE if kv[0] != "conflict_flagged"]
    lines += _table(f"## Headline ({cats}; mean with 95% bootstrap CI; percentages)", metrics.get("headline", metrics["methods"]), headline_keys)
    if metrics.get("conflict_insufficient"):
        ci_keys = [("correct", "Accuracy"), ("conflict_flagged", "Conflict flagged (C1)"), ("insufficient_abstained", "Abstained (C2)")]
        lines += _table(
            "## Conflict / insufficient evidence (reported separately: the GraphTrace verifier shares the C1 agreement rule)",
            metrics["conflict_insufficient"],
            ci_keys,
        )
    lines += _table("## All items", metrics["methods"], HEADLINE)
    lines += ["", "## Accuracy by category", ""]
    cats = sorted({c for v in metrics["by_category"].values() for c in v})
    lines.append("| Method | " + " | ".join(cats) + " |")
    lines.append("|---" * (len(cats) + 1) + "|")
    for m, by in metrics["by_category"].items():
        lines.append(f"| {m} | " + " | ".join(fmt(by[c]["correct"]) if c in by else "n/a" for c in cats) + " |")
    lines += ["", "## Calibration", "", "| Method | ECE | Brier | AUROC | AURC |", "|---|---|---|---|---|"]
    for m, c in metrics["calibration"].items():
        lines.append(f"| {m} | {c['ece']:.3f} | {c['brier']:.3f} | {c['auroc']:.3f} | {c['aurc']:.3f} |")
    if metrics.get("paired_vs_reference"):
        lines += [
            "",
            "## Paired differences vs reference on headline items (method - reference)",
            "",
            "| Method | Metric | n | diff | 95% CI | p |",
            "|---|---|---|---|---|---|",
        ]
        for m, tests in metrics["paired_vs_reference"].items():
            for k, t in tests.items():
                if t["n"]:
                    lines.append(f"| {m} | {k} | {t['n']} | {100 * t['diff']:+.1f} | [{100 * t['low']:+.1f}, {100 * t['high']:+.1f}] | {t['p_value']:.4f} |")
    return "\n".join(lines) + "\n"


def plots(metrics: dict, out_dir: Path) -> list[Path]:
    import matplotlib

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

    paths = []
    fig, ax = plt.subplots(figsize=(5, 4))
    for m, c in metrics["calibration"].items():
        pts = c["risk_coverage"]
        if pts:
            ax.plot([p[0] for p in pts], [p[1] for p in pts], label=m)
    ax.set_xlabel("Coverage")
    ax.set_ylabel("Risk (error rate)")
    ax.set_title("Risk-coverage")
    ax.legend()
    fig.tight_layout()
    p = out_dir / "risk_coverage.png"
    fig.savefig(p, dpi=150)
    plt.close(fig)
    paths.append(p)

    cats = sorted({c for v in metrics["by_category"].values() for c in v})
    methods = list(metrics["by_category"])
    fig, ax = plt.subplots(figsize=(7, 4))
    w = 0.8 / max(1, len(methods))
    for i, m in enumerate(methods):
        vals = [metrics["by_category"][m].get(c, {}).get("correct", {}).get("mean", float("nan")) for c in cats]
        ax.bar([j + i * w for j in range(len(cats))], vals, width=w, label=m)
    ax.set_xticks([j + 0.4 - w / 2 for j in range(len(cats))])
    ax.set_xticklabels(cats, rotation=15)
    ax.set_ylabel("Accuracy")
    ax.set_ylim(0, 1)
    ax.legend()
    fig.tight_layout()
    p = out_dir / "accuracy_by_category.png"
    fig.savefig(p, dpi=150)
    plt.close(fig)
    paths.append(p)
    return paths


def write_report(run_dir: str | Path, with_plots: bool = True) -> Path:
    run_dir = Path(run_dir)
    metrics = json.loads((run_dir / "metrics.json").read_text(encoding="utf-8"))
    text = f"# Results: {run_dir.name}\n\n" + markdown(metrics)
    if with_plots:
        try:
            for p in plots(metrics, run_dir):
                text += f"\n![{p.stem}]({p.name})\n"
        except ImportError:
            text += "\n(plots skipped: install the 'report' extra)\n"
    out = run_dir / "report.md"
    out.write_text(text, encoding="utf-8")
    return out