mirror of
https://github.com/mcloh/decision-models-benchmark.git
synced 2026-11-05 22:04:12 +00:00
- analysis/charts.py: gráficos SVG (acurácia, qualidade × latência, GPU × CPU) com paleta validada para daltonismo. - collect.py regenera só a tabela entre marcadores e preserva o README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
"""Gráficos (SVG) a partir dos resumos coletados em reports/<etapa>/ (tarefa 74).
|
||
|
||
Paleta: slots 1 (azul, GPU) e 2 (laranja, CPU) da paleta categórica de referência, validados
|
||
(CVD ΔE 24,7; visão normal 33,6; contraste ≥ 3:1 sobre #fcfcfb). Uma série → sem legenda;
|
||
duas séries → legenda + rótulos diretos. Um único eixo y por gráfico.
|
||
|
||
Uso: python -m analysis.charts reports/pilot
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import matplotlib
|
||
|
||
matplotlib.use("Agg")
|
||
import matplotlib.pyplot as plt # noqa: E402
|
||
|
||
SURFACE, INK, INK_2, GRID = "#fcfcfb", "#0b0b0b", "#52514e", "#e6e5e1"
|
||
GPU, CPU = "#2a78d6", "#eb6834"
|
||
NAMES = {"semif": "SemIf", "rizzo_flow": "Rizzo Flow", "laya": "Laya", "gliner": "GLiNER"}
|
||
P95_LIMIT_MS = 150
|
||
|
||
plt.rcParams.update({
|
||
"svg.fonttype": "none", "font.family": "sans-serif", "font.size": 11,
|
||
"axes.edgecolor": GRID, "axes.labelcolor": INK_2, "xtick.color": INK_2, "ytick.color": INK_2,
|
||
"axes.facecolor": SURFACE, "figure.facecolor": SURFACE, "text.color": INK,
|
||
})
|
||
|
||
|
||
def save(fig, path: Path):
|
||
fig.savefig(path)
|
||
preview = os.environ.get("DMB_CHART_PREVIEW") # PNG para inspeção visual, fora do repositório
|
||
if preview:
|
||
fig.savefig(Path(preview) / path.with_suffix(".png").name, dpi=110)
|
||
|
||
|
||
def load(dest: Path) -> dict:
|
||
out = {}
|
||
for f in dest.glob("summary-*.json"):
|
||
s = json.loads(f.read_text())
|
||
out[(s["candidate"]["name"], s["device"])] = s
|
||
return out
|
||
|
||
|
||
def style(ax, grid_axis="x"):
|
||
for side in ("top", "right"):
|
||
ax.spines[side].set_visible(False)
|
||
ax.grid(axis=grid_axis, color=GRID, linewidth=0.8)
|
||
ax.set_axisbelow(True)
|
||
|
||
|
||
def accuracy_chart(data, dest):
|
||
rows = sorted(((NAMES[c], s["quality"]["accuracy"] * 100) for (c, d), s in data.items() if d == "cuda"),
|
||
key=lambda r: r[1])
|
||
fig, ax = plt.subplots(figsize=(7, 2.8))
|
||
bars = ax.barh([r[0] for r in rows], [r[1] for r in rows], height=0.55, color=GPU)
|
||
for bar, (_, v) in zip(bars, rows):
|
||
ax.text(v + 1, bar.get_y() + bar.get_height() / 2, f"{v:.0f}%", va="center", color=INK, fontsize=11)
|
||
ax.set_xlim(0, 105)
|
||
ax.set_xlabel("Acurácia (%)")
|
||
ax.set_title("Acurácia por candidato — piloto, 50 exemplos de dev", loc="left", fontsize=12, color=INK)
|
||
style(ax)
|
||
fig.tight_layout()
|
||
save(fig, dest / "chart-acuracia.svg")
|
||
plt.close(fig)
|
||
|
||
|
||
def latency_chart(data, dest):
|
||
cands = sorted({c for c, _ in data}, key=lambda c: data[(c, "cuda")]["latency_ms"]["p50"])
|
||
fig, ax = plt.subplots(figsize=(7, 3.0))
|
||
for i, c in enumerate(cands):
|
||
g = data[(c, "cuda")]["latency_ms"]["p50"]
|
||
cpu = data.get((c, "cpu"), {}).get("latency_ms", {}).get("p50")
|
||
if cpu:
|
||
ax.plot([g, cpu], [i, i], color=GRID, linewidth=2, zorder=1)
|
||
ax.scatter(cpu, i, s=70, color=CPU, edgecolor=SURFACE, linewidth=2, zorder=3,
|
||
label="CPU (8 OCPU)" if i == 0 else None)
|
||
ax.text(cpu * 1.18, i, f"{cpu / 1000:.1f} s" if cpu >= 1000 else f"{cpu:.0f} ms",
|
||
va="center", color=INK, fontsize=10)
|
||
ax.scatter(g, i, s=70, color=GPU, edgecolor=SURFACE, linewidth=2, zorder=3,
|
||
label="GPU (A10)" if i == 0 else None)
|
||
ax.text(g / 1.18, i, f"{g:.0f} ms", va="center", ha="right", color=INK, fontsize=10)
|
||
ax.set_xscale("log")
|
||
ax.set_xlim(5, 200_000)
|
||
ax.set_yticks(range(len(cands)), [NAMES[c] for c in cands])
|
||
ax.set_ylim(-0.6, len(cands) - 0.4)
|
||
ax.set_xlabel("Latência mediana por decisão (ms, escala log)")
|
||
ax.set_title("Latência por decisão: GPU × CPU (lote 1)", loc="left", fontsize=12, color=INK)
|
||
ax.legend(loc="lower right", frameon=False, fontsize=10)
|
||
style(ax)
|
||
fig.tight_layout()
|
||
save(fig, dest / "chart-latencia.svg")
|
||
plt.close(fig)
|
||
|
||
|
||
def tradeoff_chart(data, dest):
|
||
fig, ax = plt.subplots(figsize=(7, 3.6))
|
||
ax.axvline(P95_LIMIT_MS, color=INK_2, linewidth=1, linestyle=(0, (4, 3)))
|
||
ax.text(P95_LIMIT_MS + 3, 102, f"limite p95 = {P95_LIMIT_MS} ms", color=INK_2, fontsize=9, va="top")
|
||
for (c, d), s in data.items():
|
||
if d != "cuda":
|
||
continue
|
||
x, y = s["latency_ms"]["p95"], s["quality"]["accuracy"] * 100
|
||
ax.scatter(x, y, s=80, color=GPU, edgecolor=SURFACE, linewidth=2, zorder=3)
|
||
ax.annotate(NAMES[c], (x, y), xytext=(8, -4), textcoords="offset points", color=INK, fontsize=10)
|
||
ax.set_xlim(0, 220)
|
||
ax.set_ylim(30, 104)
|
||
ax.set_xlabel("Latência p95 por decisão em GPU A10 (ms)")
|
||
ax.set_ylabel("Acurácia (%)")
|
||
ax.set_title("Qualidade × latência em GPU — piloto", loc="left", fontsize=12, color=INK)
|
||
style(ax, grid_axis="both")
|
||
fig.tight_layout()
|
||
save(fig, dest / "chart-qualidade-latencia.svg")
|
||
plt.close(fig)
|
||
|
||
|
||
def main():
|
||
dest = Path(sys.argv[1])
|
||
data = load(dest)
|
||
accuracy_chart(data, dest)
|
||
latency_chart(data, dest)
|
||
tradeoff_chart(data, dest)
|
||
print(sorted(p.name for p in dest.glob("chart-*.svg")))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|