""" ============================================================================= KORAK 4 — USPOREDBA: regex vs LLM (mjerenje + analiza grešaka) ============================================================================= Uzima označeni "gold set" (točne odgovore) i uspoređuje ga s predikcijama oba pristupa. Računa PRECIZNOST, ODZIV i F1 za izvlačenje MJESTA te točnost za VRIJEME, radi grafove i tablicu grešaka. Radi u fazama (pokreni koliko god možeš): • samo regex (ne treba API ključ) — čim je gold set označen • + LLM (treba API ključ u .env) — za punu usporedbu Pokretanje (okruženje aktivno): cd doprinos1_geolokacija python usporedba.py Rezultati se spremaju u rezultati/ (tablica + grafovi + sažetak). ============================================================================= """ from __future__ import annotations import sys import re from pathlib import Path import pandas as pd import matplotlib matplotlib.use("Agg") # crtaj u datoteku, bez prozora import matplotlib.pyplot as plt try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass sys.path.insert(0, str(Path(__file__).resolve().parent)) from putanje import IZLAZ from geolokator_regex import ucitaj_naselja GOLD_XLSX = IZLAZ / "gold_set_za_oznacavanje.xlsx" LLM_CACHE = IZLAZ / "llm_predikcije.csv" # da ne plaćamo LLM dvaput # --------------------------------------------------------------------------- # Pomoćne funkcije za usporedbu # --------------------------------------------------------------------------- def norm(s) -> str: """Normaliziraj tekst za usporedbu: mala slova, bez viška razmaka.""" if s is None: return "" s = str(s).strip().lower() return "" if s in ("nan", "none", "") else s def sat(s) -> str: """Izvuci HH:MM iz bilo kojeg oblika (npr. '02.02. 16:51' -> '16:51').""" m = re.search(r"(\d{1,2}):(\d{2})", str(s)) return f"{int(m.group(1)):02d}:{m.group(2)}" if m else "" def oznaci_mjesto(pred: str, gold: str) -> str: """Kategoriziraj jednu predikciju mjesta u odnosu na točan odgovor.""" p, g = norm(pred), norm(gold) if g and p: return "TP" if p == g else "FP" # predvidio mjesto: točno ili krivo if g and not p: return "FN" # postoji mjesto, nije pronađeno if not g and p: return "FP" # nema mjesta, a nešto je predvidio return "TN" # nema mjesta i nije ništa predvidio def metrike(oznake: list[str]) -> dict: """Iz liste TP/FP/FN/TN izračunaj preciznost, odziv, F1.""" tp = oznake.count("TP"); fp = oznake.count("FP"); fn = oznake.count("FN") 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 {"TP": tp, "FP": fp, "FN": fn, "preciznost": prec, "odziv": rec, "F1": f1} # --------------------------------------------------------------------------- # LLM predikcije (s predmemorijom da ne plaćamo dvaput) # --------------------------------------------------------------------------- def llm_predikcije(df: pd.DataFrame, naselja) -> pd.DataFrame | None: """Vrati DataFrame s LLM predikcijama po id-u, ili None ako LLM nije dostupan.""" import os if os.environ.get("BEZ_LLM"): # namjerno preskoči LLM (npr. za brzu provjeru) print(" LLM preskočen (BEZ_LLM postavljen).") return None postojeci = {} if LLM_CACHE.exists(): cache = pd.read_csv(LLM_CACHE) postojeci = {int(r["id"]): r for _, r in cache.iterrows()} print(f" Učitano {len(postojeci)} LLM predikcija iz predmemorije.") nedostaju = [r for _, r in df.iterrows() if int(r["id"]) not in postojeci] if nedostaju: try: from geolokator_llm import geolociraj_llm, _klijent klijent = _klijent() except Exception as e: if postojeci: print(f" LLM se ne može pokrenuti ({e}); koristim samo predmemoriju.") else: print(f" LLM preskočen: {e}") return None else: print(f" Pokrećem LLM na {len(nedostaju)} tekstova (može potrajati)...") novi = [] for i, r in enumerate(nedostaju, 1): try: res = geolociraj_llm(r["tekst"], naselja, klijent) except Exception as e: res = {"mjesto": None, "vrijeme": None, "je_pozar": None, "obrazlozenje": f"GREŠKA: {e}"} postojeci[int(r["id"])] = { "id": int(r["id"]), "llm_mjesto": res.get("mjesto") or "", "llm_vrijeme": res.get("vrijeme") or "", "llm_je_pozar": res.get("je_pozar"), "llm_obrazlozenje": res.get("obrazlozenje") or "", } if i % 10 == 0: print(f" ...{i}/{len(nedostaju)}") # spremi predmemoriju pd.DataFrame(list(postojeci.values())).to_csv(LLM_CACHE, index=False, encoding="utf-8") print(f" Spremljeno u {LLM_CACHE.name}") return pd.DataFrame([postojeci[int(r['id'])] for _, r in df.iterrows() if int(r['id']) in postojeci]) # --------------------------------------------------------------------------- # Glavni tijek # --------------------------------------------------------------------------- def main(): if not GOLD_XLSX.exists(): print(f"Nema gold seta: {GOLD_XLSX}\nPrvo pokreni: python napravi_gold_set.py") return df = pd.read_excel(GOLD_XLSX, sheet_name="Podaci").fillna("") print(f"Učitan gold set: {len(df)} primjera.\n") # Upozori ako gold set izgleda NEOZNAČEN (točno == regex prijedlog svugdje) isti = sum(norm(r["tocno_mjesto"]) == norm(r["regex_mjesto"]) for _, r in df.iterrows()) if isti >= len(df) - 1: print(" ⚠ UPOZORENJE: 'tocno_mjesto' je gotovo svugdje jednak regex prijedlogu.") print(" Gold set vjerojatno JOŠ NIJE ručno označen — rezultati regexa bit će") print(" lažno savršeni. Ovo je samo provjera da skripta radi.\n") naselja = ucitaj_naselja() # --- LLM predikcije --- print("LLM predikcije:") llm = llm_predikcije(df, naselja) ima_llm = llm is not None if ima_llm: df = df.merge(llm, on="id", how="left").fillna("") print() # --- Ocjena po retku --- df["ocjena_regex"] = [oznaci_mjesto(r["regex_mjesto"], r["tocno_mjesto"]) for _, r in df.iterrows()] df["regex_vrijeme_ok"] = [sat(r["regex_vrijeme"]) == sat(r["tocno_vrijeme"]) and bool(sat(r["tocno_vrijeme"])) for _, r in df.iterrows()] if ima_llm: df["ocjena_llm"] = [oznaci_mjesto(r["llm_mjesto"], r["tocno_mjesto"]) for _, r in df.iterrows()] df["llm_vrijeme_ok"] = [sat(r["llm_vrijeme"]) == sat(r["tocno_vrijeme"]) and bool(sat(r["tocno_vrijeme"])) for _, r in df.iterrows()] # --- Metrike (mjesto) --- print("=" * 60) print("REZULTATI — izvlačenje MJESTA") print("=" * 60) m_regex = metrike(list(df["ocjena_regex"])) _ispis("REGEX", m_regex) m_llm = None if ima_llm: m_llm = metrike(list(df["ocjena_llm"])) _ispis("LLM ", m_llm) # --- Vrijeme --- with_time = df[df["tocno_vrijeme"].apply(lambda s: bool(sat(s)))] if len(with_time): print("\n--- VRIJEME (točnost na primjerima gdje vrijeme postoji) ---") print(f" REGEX: {with_time['regex_vrijeme_ok'].mean():.1%} ({len(with_time)} primjera)") if ima_llm: print(f" LLM : {with_time['llm_vrijeme_ok'].mean():.1%}") # --- Snimi tablicu + grafove + sažetak --- IZLAZ.mkdir(parents=True, exist_ok=True) stupci = ["id", "izvor", "tekst", "tocno_mjesto", "regex_mjesto", "ocjena_regex"] if ima_llm: stupci += ["llm_mjesto", "ocjena_llm", "llm_obrazlozenje"] df[stupci].to_excel(IZLAZ / "rezultati_usporedbe.xlsx", index=False) print(f"\nTablica: {(IZLAZ / 'rezultati_usporedbe.xlsx').name}") _graf(m_regex, m_llm) _analiza_gresaka(df, ima_llm) def _ispis(ime, m): print(f" {ime} preciznost={m['preciznost']:.1%} odziv={m['odziv']:.1%} " f"F1={m['F1']:.1%} (TP={m['TP']} FP={m['FP']} FN={m['FN']})") def _graf(m_regex, m_llm): """Stupčasti graf usporedbe P/R/F1.""" mjere = ["preciznost", "odziv", "F1"] x = range(len(mjere)) plt.figure(figsize=(7, 4.5)) if m_llm: w = 0.38 plt.bar([i - w/2 for i in x], [m_regex[k] for k in mjere], w, label="regex", color="#8A8F98") plt.bar([i + w/2 for i in x], [m_llm[k] for k in mjere], w, label="LLM", color="#C6402A") else: plt.bar(list(x), [m_regex[k] for k in mjere], 0.5, label="regex", color="#8A8F98") plt.xticks(list(x), ["Preciznost", "Odziv", "F1"]) plt.ylim(0, 1); plt.ylabel("rezultat"); plt.title("Izvlačenje mjesta: regex vs LLM") plt.legend(); plt.tight_layout() put = IZLAZ / "graf_mjesto.png" plt.savefig(put, dpi=130); plt.close() print(f"Graf: {put.name}") def _analiza_gresaka(df, ima_llm): """Ispiši nekoliko primjera gdje pristupi griješe (građa za rad).""" print("\n" + "=" * 60) print("ANALIZA GREŠAKA (nekoliko primjera)") print("=" * 60) krivi_regex = df[df["ocjena_regex"].isin(["FP", "FN"])].head(5) for _, r in krivi_regex.iterrows(): print(f"\n[{r['izvor']}] {str(r['tekst'])[:70]}...") print(f" točno: '{r['tocno_mjesto']}' | regex: '{r['regex_mjesto']}' ({r['ocjena_regex']})", end="") if ima_llm: print(f" | LLM: '{r['llm_mjesto']}' ({r['ocjena_llm']})") else: print() if ima_llm: # gdje LLM pobjeđuje regex i obrnuto llm_bolji = df[(df["ocjena_llm"] == "TP") & (df["ocjena_regex"] != "TP")] regex_bolji = df[(df["ocjena_regex"] == "TP") & (df["ocjena_llm"] != "TP")] print(f"\nLLM točan gdje regex nije: {len(llm_bolji)} slučajeva") print(f"Regex točan gdje LLM nije: {len(regex_bolji)} slučajeva") if __name__ == "__main__": main()