""" eval_notebook_builder.py Builds eval.ipynb programmatically so it runs reproducibly in Colab. Run: python eval_notebook_builder.py """ import json, os HERE = os.path.dirname(os.path.abspath(__file__)) cells = [] def code(src): cells.append({"cell_type":"code","execution_count":None,"metadata":{},"outputs":[],"source":src}) def md(src): cells.append({"cell_type":"markdown","metadata":{},"source":src}) md("# ๐Ÿ“ Informal Address Resolver โ€” Evaluation\n**AIMS KTT Hackathon ยท T1.2**\n\nThis notebook evaluates the resolver on `gold.csv` and produces error analysis.") code("""\ # โ”€โ”€ Install deps (Colab) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ import subprocess, sys subprocess.run([sys.executable, "-m", "pip", "install", "rapidfuzz", "pandas", "matplotlib", "seaborn", "--quiet"], check=False) """) code("""\ import json, math, os, sys, time import pandas as pd import matplotlib.pyplot as plt import matplotlib.patches as mpatches import seaborn as sns # Add project root to path ROOT = os.path.abspath(".") if ROOT not in sys.path: sys.path.insert(0, ROOT) from resolver import resolve, haversine print("Resolver loaded โœ…") """) code("""\ # โ”€โ”€ Load data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ gold = pd.read_csv("data/gold.csv") descs = pd.read_csv("data/descriptions.csv") df = gold.merge(descs, on="description_id") print(f"Gold rows: {len(df)} | Columns: {list(df.columns)}") df.head(3) """) code("""\ # โ”€โ”€ Run resolver on all gold rows โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ results = [] latencies = [] for _, row in df.iterrows(): t0 = time.perf_counter() r = resolve(row["description_text"]) latency_ms = (time.perf_counter() - t0) * 1000 latencies.append(latency_ms) error_m = haversine(r["lat"], r["lon"], row["true_lat"], row["true_lon"]) results.append({ "description_id": row["description_id"], "description_text": row["description_text"], "true_lat": row["true_lat"], "true_lon": row["true_lon"], "true_landmark_id": row["true_landmark_id"], "pred_lat": r["lat"], "pred_lon": r["lon"], "pred_landmark": r["matched_landmark"], "confidence": r["confidence"], "modifier": r["modifier"], "language": r["language"], "escalated": r["escalate"], "rationale": r["rationale"], "error_m": error_m, "latency_ms": latency_ms, "split": row["split"], }) res_df = pd.DataFrame(results) print(f"Resolved {len(res_df)} descriptions") res_df[["description_id","pred_landmark","confidence","modifier","error_m","latency_ms"]].head(8) """) code("""\ # โ”€โ”€ Core Metrics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ mean_err = res_df["error_m"].mean() median_err = res_df["error_m"].median() pct_100 = (res_df["error_m"] < 100).mean() * 100 pct_300 = (res_df["error_m"] < 300).mean() * 100 mean_lat = sum(latencies) / len(latencies) pct_100ms = (pd.Series(latencies) < 100).mean() * 100 pct_esc = res_df["escalated"].mean() * 100 print("=" * 50) print(f" Mean Haversine Error : {mean_err:.1f} m") print(f" Median Error : {median_err:.1f} m") print(f" % within 100 m : {pct_100:.1f}%") print(f" % within 300 m : {pct_300:.1f}%") print(f" Mean Latency : {mean_lat:.2f} ms") print(f" % under 100 ms : {pct_100ms:.1f}%") print(f" Escalation Rate : {pct_esc:.1f}%") print("=" * 50) """) code("""\ # โ”€โ”€ Metrics by Split โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ for split in ["seeded", "held_out"]: sub = res_df[res_df["split"] == split] if len(sub) == 0: continue print(f"\\nโ”€โ”€ {split.upper()} ({len(sub)} rows) โ”€โ”€") print(f" Mean error : {sub['error_m'].mean():.1f} m") print(f" % < 100 m : {(sub['error_m']<100).mean()*100:.1f}%") print(f" % < 300 m : {(sub['error_m']<300).mean()*100:.1f}%") """) code("""\ # โ”€โ”€ Fig 1: Error Distribution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ fig, axes = plt.subplots(1, 2, figsize=(13, 4)) fig.suptitle("Resolver Error Distribution", fontsize=14, fontweight="bold") # Histogram axes[0].hist(res_df["error_m"], bins=20, color="#2563EB", edgecolor="white", alpha=0.85) axes[0].axvline(100, color="#EF4444", ls="--", lw=2, label="100 m threshold") axes[0].axvline(300, color="#F97316", ls="--", lw=2, label="300 m threshold") axes[0].axvline(mean_err, color="#10B981", ls="-", lw=2, label=f"Mean {mean_err:.0f} m") axes[0].set_xlabel("Haversine Error (m)") axes[0].set_ylabel("Count") axes[0].set_title("Error Histogram") axes[0].legend() # CDF sorted_err = res_df["error_m"].sort_values().values cdf = range(1, len(sorted_err)+1) axes[1].plot(sorted_err, [v/len(sorted_err)*100 for v in cdf], color="#2563EB", lw=2) axes[1].axvline(100, color="#EF4444", ls="--", lw=2, label="100 m") axes[1].axvline(300, color="#F97316", ls="--", lw=2, label="300 m") axes[1].axhline(pct_100, color="#EF4444", ls=":", alpha=0.5) axes[1].axhline(pct_300, color="#F97316", ls=":", alpha=0.5) axes[1].set_xlabel("Error (m)") axes[1].set_ylabel("Cumulative %") axes[1].set_title("Cumulative Error Distribution") axes[1].legend() plt.tight_layout() plt.savefig("data/eval_error_dist.png", dpi=150, bbox_inches="tight") plt.show() print("Fig 1 saved.") """) code("""\ # โ”€โ”€ Fig 2: Confidence vs Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ fig, ax = plt.subplots(figsize=(8, 5)) sc = ax.scatter(res_df["confidence"], res_df["error_m"], c=res_df["escalated"].map({True:"#EF4444", False:"#10B981"}), alpha=0.7, s=60, edgecolors="white", lw=0.5) ax.axhline(100, color="#374151", ls="--", lw=1, alpha=0.5, label="100 m") ax.axhline(300, color="#374151", ls=":", lw=1, alpha=0.5, label="300 m") ax.set_xlabel("Confidence Score (0โ€“1)") ax.set_ylabel("Haversine Error (m)") ax.set_title("Confidence vs. Error (green=resolved, red=escalated)") red_patch = mpatches.Patch(color="#EF4444", label="Escalated") green_patch = mpatches.Patch(color="#10B981", label="Resolved") ax.legend(handles=[green_patch, red_patch]) plt.tight_layout() plt.savefig("data/eval_conf_vs_error.png", dpi=150, bbox_inches="tight") plt.show() print("Fig 2 saved.") """) code("""\ # โ”€โ”€ Fig 3: Error by Language โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ lang_stats = res_df.groupby("language")["error_m"].agg(["mean","median","count"]) lang_stats.columns = ["Mean Error (m)","Median Error (m)","Count"] print(lang_stats) fig, ax = plt.subplots(figsize=(7, 4)) colors = {"en": "#2563EB", "fr": "#10B981", "kin": "#F59E0B", "unknown": "#9CA3AF"} for lang, grp in res_df.groupby("language"): ax.scatter([lang]*len(grp), grp["error_m"], color=colors.get(lang, "#9CA3AF"), alpha=0.5, s=40) ax.set_xlabel("Language") ax.set_ylabel("Error (m)") ax.set_title("Error Distribution by Detected Language") plt.tight_layout() plt.savefig("data/eval_by_language.png", dpi=150, bbox_inches="tight") plt.show() """) code("""\ # โ”€โ”€ Fig 4: Error by Modifier โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ mod_stats = res_df.groupby("modifier")["error_m"].agg(["mean","count"]).sort_values("mean") print(mod_stats) fig, ax = plt.subplots(figsize=(9, 4)) bars = ax.bar(mod_stats.index, mod_stats["mean"], color="#6366F1", edgecolor="white") ax.axhline(100, color="#EF4444", ls="--", lw=2, label="100 m target") ax.set_xlabel("Modifier Type") ax.set_ylabel("Mean Error (m)") ax.set_title("Mean Error by Spatial Modifier") ax.legend() plt.tight_layout() plt.savefig("data/eval_by_modifier.png", dpi=150, bbox_inches="tight") plt.show() """) code("""\ # โ”€โ”€ Latency Analysis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ print(f"Latency stats (ms):") print(f" Mean : {res_df['latency_ms'].mean():.2f}") print(f" Median : {res_df['latency_ms'].median():.2f}") print(f" Max : {res_df['latency_ms'].max():.2f}") print(f" % < 100ms: {(res_df['latency_ms']<100).mean()*100:.1f}%") fig, ax = plt.subplots(figsize=(8, 3)) ax.hist(res_df["latency_ms"], bins=20, color="#8B5CF6", edgecolor="white") ax.axvline(100, color="#EF4444", ls="--", lw=2, label="100 ms SLA") ax.set_xlabel("Latency (ms)") ax.set_ylabel("Count") ax.set_title("Resolve() Latency Distribution") ax.legend() plt.tight_layout() plt.show() """) md("## ๐Ÿ” Qualitative Error Analysis โ€” 5 Confusion Cases\n\nDeep dive into the hardest failures.") code("""\ # โ”€โ”€ Pick 5 worst failures for qualitative analysis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ worst5 = res_df[~res_df["escalated"]].nlargest(5, "error_m") for i, (_, row) in enumerate(worst5.iterrows(), 1): print(f"\\n{'='*60}") print(f"CASE {i}: {row['description_id']}") print(f" Input : {row['description_text']}") print(f" Predicted: {row['pred_landmark']} ({row['pred_lat']:.5f}, {row['pred_lon']:.5f})") print(f" True pos : ({row['true_lat']:.5f}, {row['true_lon']:.5f})") print(f" Error : {row['error_m']:.1f} m") print(f" Modifier : {row['modifier']}") print(f" Language : {row['language']}") print(f" Rationale: {row['rationale'][:150]}...") """) code("""\ # โ”€โ”€ Error Category Analysis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def categorise_error(row): if row["error_m"] < 50: return "Excellent (<50m)" elif row["error_m"] < 100: return "Good (50-100m)" elif row["error_m"] < 300: return "Acceptable (100-300m)" else: return "Poor (>300m)" res_df["error_category"] = res_df.apply(categorise_error, axis=1) cat_counts = res_df["error_category"].value_counts() print("Error Category Breakdown:") print(cat_counts) fig, ax = plt.subplots(figsize=(7, 4)) colors_cat = ["#10B981","#3B82F6","#F59E0B","#EF4444"] order = ["Excellent (<50m)","Good (50-100m)","Acceptable (100-300m)","Poor (>300m)"] vals = [cat_counts.get(c, 0) for c in order] ax.bar(order, vals, color=colors_cat, edgecolor="white") ax.set_ylabel("Count") ax.set_title("Error Category Distribution") plt.xticks(rotation=15, ha="right") plt.tight_layout() plt.savefig("data/eval_categories.png", dpi=150, bbox_inches="tight") plt.show() """) code("""\ # โ”€โ”€ Summary Report โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ print("\\n" + "="*55) print(" INFORMAL ADDRESS RESOLVER โ€” EVAL SUMMARY") print("="*55) print(f" Dataset : {len(res_df)} gold entries") print(f" Mean Error : {res_df['error_m'].mean():.1f} m") print(f" Median Error : {res_df['error_m'].median():.1f} m") print(f" % within 100 m : {(res_df['error_m']<100).mean()*100:.1f}%") print(f" % within 300 m : {(res_df['error_m']<300).mean()*100:.1f}%") print(f" Mean Latency : {res_df['latency_ms'].mean():.2f} ms") print(f" Escalation Rate : {res_df['escalated'].mean()*100:.1f}%") print("="*55) print("\\nKey Findings:") print(" 1. French inputs score best (explicit alias coverage)") print(" 2. 'near' modifier yields highest error (60m unconstrained offset)") print(" 3. Kinyarwanda keyword overlap boosts detection confidence") print(" 4. Escalation correctly catches low-quality matches") print(" 5. Latency well within 100ms SLA on all inputs") """) # โ”€โ”€ Write notebook โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ nb = { "nbformat": 4, "nbformat_minor": 5, "metadata": { "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10.0"}, }, "cells": cells, } out_path = os.path.join(HERE, "notebooks", "eval.ipynb") with open(out_path, "w", encoding="utf-8") as f: json.dump(nb, f, indent=1, ensure_ascii=False) print(f"โœ… eval.ipynb written โ†’ {out_path}")