File size: 16,824 Bytes
a72d1cf | 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 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | """Karşılaştırmalı başarım raporu (tek dosya HTML, dış bağımlılık yok)."""
from __future__ import annotations
import html
import json
from datetime import date
from pathlib import Path
from .errors import CATALOG
ROOT = Path(__file__).resolve().parent.parent
CSS = """
:root{--bg:#f7f7f5;--surface:#fcfcfb;--ink:#0b0b0b;--ink2:#52514e;--muted:#8a8984;--line:#e4e3de;
--s1:#2a78d6;--s2:#eb6834;--track:#ecebe6;--good:#1c7c4a;--bad:#b42318}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#121211;--surface:#1a1a19;--ink:#fff;
--ink2:#c3c2b7;--muted:#8f8e86;--line:#2e2e2b;--s1:#3987e5;--s2:#d95926;--track:#2a2a27;--good:#4cc38a;--bad:#f97066}}
:root[data-theme="dark"]{--bg:#121211;--surface:#1a1a19;--ink:#fff;--ink2:#c3c2b7;--muted:#8f8e86;--line:#2e2e2b;
--s1:#3987e5;--s2:#d95926;--track:#2a2a27;--good:#4cc38a;--bad:#f97066}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
main{max-width:1060px;margin:0 auto;padding:32px 16px 64px}h1{font-size:28px;margin:0 0 4px;letter-spacing:-.01em}
h2{font-size:19px;margin:40px 0 10px;padding-top:8px;border-top:1px solid var(--line)}h3{font-size:15px;margin:20px 0 8px}
.sub{color:var(--ink2);margin:0 0 24px}.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}
.tile{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.tile .k{font-size:12px;color:var(--ink2);text-transform:uppercase;letter-spacing:.04em}.tile .v{font-size:30px;font-weight:650;margin-top:2px}
.tile .n{font-size:13px;color:var(--muted)}
table{border-collapse:collapse;width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:13.5px}
th,td{padding:7px 10px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--ink2);font-weight:600;background:var(--bg)}
td.num{text-align:right;font-variant-numeric:tabular-nums}.wrap{overflow-x:auto}
.bars{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:12px 14px}
.bar{display:grid;grid-template-columns:minmax(120px,240px) 1fr 56px;gap:10px;align-items:center;margin:5px 0;font-size:13.5px}
.track{background:var(--track);height:14px;border-radius:4px;position:relative}
.fill{height:14px;border-radius:0 4px 4px 0;background:var(--s1)}.fill.alt{background:var(--s2)}
.bar .val{text-align:right;font-variant-numeric:tabular-nums;color:var(--ink)}
.note{color:var(--ink2);font-size:13.5px}.ok{color:var(--good);font-weight:600}.no{color:var(--bad);font-weight:600}
code{font:12.5px ui-monospace,Menlo,monospace;background:var(--track);padding:1px 4px;border-radius:4px;word-break:break-word}
.legend{display:flex;gap:16px;font-size:13px;color:var(--ink2);margin:4px 0 8px}.sw{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:6px;vertical-align:middle}
@media (max-width:600px){.bar{grid-template-columns:110px 1fr 48px}}
"""
def e(x) -> str:
return html.escape(str(x))
def pct(x) -> str:
return "—" if x is None else f"%{str(round(x, 1)).replace('.', ',')}"
def bars(rows: list[tuple[str, float | None]], alt_rows: list[float | None] | None = None,
labels: tuple[str, str] | None = None) -> str:
out = ['<div class="bars">']
if labels:
out.append(f'<div class="legend"><span><i class="sw" style="background:var(--s1)"></i>{e(labels[0])}</span>'
f'<span><i class="sw" style="background:var(--s2)"></i>{e(labels[1])}</span></div>')
for i, (name, v) in enumerate(rows):
w = 0 if v is None else max(0.0, min(100.0, v))
out.append(f'<div class="bar" title="{e(name)}: {pct(v)}"><span>{e(name)}</span>'
f'<div class="track"><div class="fill" style="width:{w}%"></div></div><span class="val">{pct(v)}</span></div>')
if alt_rows is not None:
a = alt_rows[i]
wa = 0 if a is None else max(0.0, min(100.0, a))
out.append(f'<div class="bar" title="{e(name)} ({e(labels[1] if labels else "")}): {pct(a)}"><span></span>'
f'<div class="track"><div class="fill alt" style="width:{wa}%"></div></div><span class="val">{pct(a)}</span></div>')
out.append("</div>")
return "\n".join(out)
SYS_LABEL = {
"kural_tr": "Kural tabanlı · TR normalizasyon · kendini düzeltme",
"kural_tr_duzeltmesiz": "Kural tabanlı · TR normalizasyon · tek deneme",
"kural_en_naif": "Kural tabanlı · naif (İngilizce yerel ayar)",
"kural_metin_naif": "Kural tabanlı · naif (sayılar metin)",
}
def sys_label(name: str) -> str:
if name in SYS_LABEL:
return SYS_LABEL[name]
for k, v in (("_tr_duzeltmesiz", " · TR · tek deneme"), ("_tr", " · TR · kendini düzeltme"),
("_en_naif", " · naif (İngilizce yerel ayar)"), ("_metin_naif", " · naif (sayılar metin)")):
if name.endswith(k):
return name[: -len(k)].capitalize() + v
return name
def _load_records(name: str) -> list[dict]:
p = ROOT / "outputs" / "sonuclar" / f"sonuclar_{name}.jsonl"
if not p.exists():
return []
return [json.loads(l) for l in p.read_text(encoding="utf-8").splitlines() if l.strip()]
def write_report(ozet: dict, path: Path) -> Path:
S = ozet["sistemler"]
by = {s["sistem"]: s for s in S}
main_s = by.get("kural_tr") or S[0]
llm_main = next((s for s in S if s["sistem"].endswith("_tr") and not s["sistem"].startswith("kural")), None)
naive = {r["kip"]: r for r in ozet["sayi_ablasyonu"]}
P = []
P.append(f"<h1>TR-TabloSQL Başarım Raporu</h1><p class='sub'>Dokümanlardaki tabloların yapılandırılmış bilgi tabanına "
f"dönüştürülmesi ve Türkçe doğal dille sorgulanması · {date.today().strftime('%d.%m.%Y')} · "
f"{ozet['soru_sayisi']} soru</p>")
# ---- özet kutuları
tiles = [("En iyi EX (TR kipi)", pct(max(s["EX"] for s in S if s["sayi_kipi"] == "tr")),
"tüm sorular, kendini düzeltme açık"),
("Ayrılmış test EX", pct(main_s["bolum"].get("test")), f"{sys_label(main_s['sistem']).split(' ·')[0]}, "
"geliştirmede kullanılmayan sorular")]
if "en" in naive:
tiles.append(("Sayı biçimi kaybı", pct(100 - naive["en"]["EX"]),
"doğru SQL bile naif ayrıştırmada yanlış cevap veriyor"))
tiles.append(("Cevapsız sorularda doğru red", pct(main_s["cevapsiz_dogru_red"]), "uydurma yerine açık 'cevap yok'"))
P.append("<div class='tiles'>" + "".join(
f"<div class='tile'><div class='k'>{e(k)}</div><div class='v'>{v}</div><div class='n'>{e(n)}</div></div>"
for k, v, n in tiles) + "</div>")
if llm_main is None:
P.append("<p class='note'>Not: Bu çalıştırmada LLM arka ucu kullanılmadı (API anahtarı yoktu). "
"<code>python scripts/run_all.py --claude</code>, <code>--openai</code> veya <code>--ollama</code> "
"ile LLM sistemleri aynı tabloya eklenir.</p>")
# ---- sistem karşılaştırması
P.append("<h2>1. Sistem karşılaştırması (yürütme doğruluğu)</h2>")
P.append("<div class='wrap'><table><tr><th>Sistem</th><th>Sayı kipi</th><th class='num'>EX</th>"
"<th class='num'>Geliştirme</th><th class='num'>Ayrılmış test</th><th class='num'>İlk deneme EX</th>"
"<th class='num'>Cevapsız → doğru red</th><th class='num'>Yanlış red</th><th class='num'>Ort. deneme</th>"
"<th class='num'>Ort. süre</th></tr>")
for s in S:
P.append(f"<tr><td>{e(sys_label(s['sistem']))}</td><td>{e(s['sayi_kipi'])}</td><td class='num'><b>{pct(s['EX'])}</b></td>"
f"<td class='num'>{pct(s['bolum'].get('gelistirme'))}</td><td class='num'>{pct(s['bolum'].get('test'))}</td>"
f"<td class='num'>{pct(s['EX_ilk_deneme'])}</td><td class='num'>{pct(s['cevapsiz_dogru_red'])}</td>"
f"<td class='num'>{pct(s['yanlis_red_orani'])}</td><td class='num'>{str(s['ort_deneme']).replace('.', ',')}</td>"
f"<td class='num'>{str(s['ort_sure_sn']).replace('.', ',')} sn</td></tr>")
P.append("</table></div>")
P.append(bars([(sys_label(s["sistem"]), s["EX"]) for s in S]))
# ---- sayı biçimi
P.append("<h2>2. Türkçe sayı biçiminin etkisi ('1.234,56')</h2>")
P.append("<p class='note'>Aynı belgeler üç farklı sayı ayrıştırma kipiyle veritabanına aktarıldı. "
"<b>tr</b>: doğru Türkçe yorum. <b>en</b>: yaygın naif yaklaşım — nokta ondalık, virgül binlik sanılır "
"(<code>'1.346'</code> → 1,346; <code>'4,5'</code> → 45; <code>'258.107.640,00'</code> ayrıştırılamaz → sütun metne düşer). "
"<b>text</b>: hiç ayrıştırma yok; SQLite <code>SUM('1.234,56')</code> değerini 1,234 olarak toplar. "
"Altın SQL sorguları bu veritabanlarında çalıştırılınca, sorgu doğru olsa bile kaç cevabın bozulduğu ölçüldü.</p>")
P.append("<div class='wrap'><table><tr><th>Kip</th><th class='num'>Altın SQL EX</th><th class='num'>Bozulan cevap</th>"
"<th class='num'>Sayısal sütun</th><th class='num'>Metin sütun</th><th class='num'>Ayrıştırılamayan hücre</th></tr>")
st = {r["kip"]: r for r in ozet.get("sayi_istatistik", [])}
for k, r in naive.items():
s2 = st.get(k, {})
P.append(f"<tr><td>{e(k)}</td><td class='num'>{pct(r['EX'])}</td><td class='num'>{len(r['bozulan'])} / {r['n']}</td>"
f"<td class='num'>{s2.get('sayisal_sutun', '—')}</td><td class='num'>{s2.get('metin_sutun', '—')}</td>"
f"<td class='num'>{s2.get('ayristirilamayan_hucre', '—')}</td></tr>")
P.append("</table></div>")
kip_sys = [(f"{sys_label(s['sistem'])}", s["EX"]) for s in S if s["sistem"].startswith("kural") and s["kendini_duzeltme"]]
P.append("<h3>Uçtan uca sistem, kip başına</h3>" + bars(kip_sys))
tr_cat = main_s["kategori"].get("tr_sayi")
P.append(f"<p class='note'>TR kipinde, Türkçe biçimli sayı içeren sorularda (tr_sayi, n={main_s['kategori_n'].get('tr_sayi')}) "
f"EX {pct(tr_cat)}; naif kiplerdeki aynı alt kümedeki düşüş tabloda kategori satırlarında görülür.</p>")
# ---- kategori
P.append("<h2>3. Soru kategorisine göre EX</h2>")
cats = sorted(main_s["kategori"].items(), key=lambda kv: (kv[1] is None, -(kv[1] or 0)))
comp = by.get("kural_en_naif")
P.append(bars([(f"{k} (n={main_s['kategori_n'][k]})", v) for k, v in cats],
[comp["kategori"].get(k) for k, _ in cats] if comp else None,
(sys_label(main_s["sistem"]), sys_label(comp["sistem"])) if comp else None))
P.append("<h3>Zorluk düzeyi</h3><div class='wrap'><table><tr><th>Sistem</th>" +
"".join(f"<th class='num'>{z}</th>" for z in ("kolay", "orta", "zor")) + "</tr>" +
"".join(f"<tr><td>{e(sys_label(s['sistem']))}</td>" + "".join(
f"<td class='num'>{pct(s['zorluk'].get(z))}</td>" for z in ("kolay", "orta", "zor")) + "</tr>" for s in S)
+ "</table></div>")
# ---- hata kataloğu
P.append("<h2>4. Hata kataloğu</h2>")
P.append("<p class='note'>Başarısız örnekler aşağıdaki tiplere otomatik atandı (değerlendirme anında, altın sonuçla "
"karşılaştırılarak). Çalışma zamanında ajan aynı tipleri statik denetim, yürütme ve sonuç denetimi "
"adımlarında yakalayıp geri besleme olarak kullanır.</p>")
types = list(CATALOG.keys())
P.append("<div class='wrap'><table><tr><th>Hata tipi</th><th>Tanım</th>" +
"".join(f"<th class='num'>{e(sys_label(s['sistem']).split(' · ')[-1])}</th>" for s in S) + "</tr>")
for t in types:
counts = [s["hata_tipleri"].get(t, 0) for s in S]
if not any(counts) and t in ("YANLIS_SONUC", "YURUTME"):
continue
P.append(f"<tr><td><b>{e(CATALOG[t]['ad'])}</b><br><code>{t}</code></td><td>{e(CATALOG[t]['tanim'])}"
f"<br><span class='note'>Örnek: {e(CATALOG[t]['ornek'])} · Önlem: {e(CATALOG[t]['cozum'])}</span></td>" +
"".join(f"<td class='num'>{c}</td>" for c in counts) + "</tr>")
P.append("</table></div>")
# ---- kendini düzeltme
P.append("<h2>5. Kendini düzeltme döngüsü</h2>")
sc = [s for s in S if s["kendini_duzeltme"] and s["sayi_kipi"] == "tr"]
P.append(bars([(sys_label(s["sistem"]), s["EX"]) for s in sc], [s["EX_ilk_deneme"] for s in sc],
("Son deneme", "İlk deneme")))
P.append("<p class='note'>Kural tabanlı üreticide 'düzeltme', hata/boş sonuç sonrası sıradaki aday sorguya geçmektir; "
"LLM arka uçlarında hata tipi, mesaj ve öneriler (ör. en yakın sütun adları, tablodaki gerçek değerler, ISO tarih "
"dönüşümü) bir sonraki isteme eklenir.</p>")
# ---- çıkarım
P.append("<h2>6. Tablo çıkarma doğruluğu</h2>")
ex = ozet["cikarim"]
tabs = [r["tablo"] for r in next(iter(ex.values()))]
P.append("<div class='wrap'><table><tr><th>Tablo</th>" + "".join(f"<th class='num'>{e(m)}</th>" for m in ex) + "</tr>")
for i, t in enumerate(tabs):
cells = []
for m in ex:
r = ex[m][i]
mark = "" if r["baslik_dogru"] else " <span class='no' title='başlık yapısı hatalı'>•</span>"
cells.append(f"<td class='num'>{pct(100 * r['hucre_dogrulugu']) if r['bulundu'] else '<span class=no>bulunamadı</span>'}{mark}</td>")
P.append(f"<tr><td>{e(t)}</td>{''.join(cells)}</tr>")
P.append("<tr><td><b>Ortalama hücre doğruluğu</b></td>" + "".join(
f"<td class='num'><b>{pct(100 * sum(r['hucre_dogrulugu'] for r in ex[m]) / len(ex[m]))}</b></td>" for m in ex) + "</tr>")
P.append("</table></div><p class='note'>• = çok satırlı/birleşik başlık yapısı doğru çözülemedi. "
"<b>auto</b>: pdfplumber (çizgili tablolar, hücre kutularından birleşim çözümleme) → bulamazsa Camelot-stream "
"(çizgisiz tablolar). HTML tablolarında rowspan/colspan doğrudan okunur.</p>")
# ---- hatalı örnekler
recs = _load_records(main_s["sistem"])
bad = [r for r in recs if not r["dogru"]]
P.append(f"<h2>7. {e(sys_label(main_s['sistem']))}: başarısız örnekler ({len(bad)})</h2>")
if bad:
P.append("<div class='wrap'><table><tr><th>ID</th><th>Soru</th><th>Hata tipi</th><th>Tahmin</th><th>Altın SQL</th></tr>")
for r in bad:
pred = r["tahmin_sql"] or r["cevap"]
P.append(f"<tr><td>{e(r['id'])}</td><td>{e(r['soru'])}</td><td>{e(r['hata_tipi'])}</td>"
f"<td><code>{e(pred)}</code></td><td><code>{e(r['altin_sql'] or 'CEVAPSIZ')}</code></td></tr>")
P.append("</table></div>")
# ---- yöntem
P.append("<h2>8. Yöntem ve sınırlılıklar</h2><ul class='note'>"
"<li><b>Veri:</b> Belgeler sentetiktir (gerçek kurum verisi değildir) ama Türk raporlarının yapısal zorluklarını "
"taşır: birleşik başlıklar, rowspan'li gruplar, '1.234,56', parantezli negatifler, '%4,5', 'Ocak 2024', "
"'Toplam' satırları, çizgisiz tablo.</li>"
"<li><b>Test kümesi:</b> Spider (seçim, filtre, toplama, gruplama, sıralama, iç içe, çoklu tablo) ve BIRD "
"(kanıt/formül, değer eşleme, birim) soru tiplerinden uyarlanmıştır; cevapsız sorular dahildir.</li>"
"<li><b>EX:</b> Sistem SQL'i kendi çıkardığı veritabanında, altın SQL ise elle doğrulanmış referans veritabanında "
"çalışır; böylece çıkarım hataları da ölçüme yansır. Sayısal tolerans ±(0,01 + %0,1).</li>"
"<li><b>Önemli:</b> Kural tabanlı sistemin kuralları geliştirme bölümündeki sorular üzerinde ayarlandı; bu nedenle "
"geliştirme skoru iyimserdir. Genellemeyi gösteren sayı <b>ayrılmış test</b> skorudur.</li>"
"<li>Kural tabanlı sistem çoklu tablo ve alan bilgisi gerektiren formül sorularında (ör. brüt kâr marjı) zayıftır; "
"bu sorular LLM arka uçlarının katkısını ölçmek için kümede tutulmuştur.</li></ul>")
page = (f"<!doctype html><html lang='tr'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
f"<title>TR-TabloSQL Raporu</title><style>{CSS}</style></head><body><main>{''.join(P)}</main></body></html>")
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(page, encoding="utf-8")
return path
|