| """Bangun notebook showcase model (kode + teks + output nyata) lalu eksekusi |
| + render ke HTML untuk ditampilkan di tab "Notebook" aplikasi. |
| |
| Output: |
| - notebooks/05_model_showcase.ipynb (executed; bisa dibuka di HF Files tab) |
| - frontend/public/notebook.html (render Jupyter, di-embed React tab) |
| |
| Notebook self-contained: tiap eksekusi nyatanya menyentuh index + eval CSV |
| yang sudah ada (cepat & deterministik). Jalankan ulang tiap update model: |
| cd backend && python -m scripts.build_showcase_notebook |
| """ |
| from __future__ import annotations |
| import sys |
| from pathlib import Path |
|
|
| import nbformat as nbf |
| from nbconvert import HTMLExporter |
| from nbconvert.preprocessors import ExecutePreprocessor |
|
|
| ROOT = Path(__file__).resolve().parents[2] |
| BACKEND = ROOT / "backend" |
| NB_OUT = ROOT / "notebooks" / "05_model_showcase.ipynb" |
| HTML_OUT = ROOT / "frontend" / "public" / "notebook.html" |
|
|
|
|
| def md(text: str): |
| return nbf.v4.new_markdown_cell(text) |
|
|
|
|
| def code(src: str): |
| return nbf.v4.new_code_cell(src) |
|
|
|
|
| SETUP = f''' |
| import sys, json, warnings |
| from pathlib import Path |
| warnings.filterwarnings("ignore") |
| ROOT = Path(r"{ROOT.as_posix()}") |
| sys.path.insert(0, str(ROOT / "backend")) |
| import pandas as pd |
| pd.set_option("display.max_colwidth", 46) |
| |
| from app.indexing.loader import load_all_indexes |
| from app.indexing.hybrid import HybridIndex |
| from app.preprocessing import PreprocessingPipeline |
| from app.search.gazetteer import Gazetteer |
| from app.search.pipeline import smart_rank |
| from scripts.eval_smart import load_listings |
| |
| idx = load_all_indexes(ROOT / "data" / "indexes", include_neural=True) |
| bm25, tfidf, neural = idx["bm25"], idx["tfidf"], idx["indobert"] |
| pipe = PreprocessingPipeline() |
| pre = lambda s: pipe.process(s).processed |
| hybrid = HybridIndex(bm25, neural, query_preprocessor=pre) |
| gz = Gazetteer.load() |
| listings = load_listings() |
| print(f"Corpus: {{len(listings)}} listing | vocab BM25: {{len(bm25.bm25.idf)}} term") |
| print(f"Index siap: {{', '.join(idx.keys())}} + smart + hybrid") |
| '''.strip() |
|
|
| PREP = ''' |
| # Pipeline preprocessing 9-stage pada contoh teks (judul + deskripsi pemilik) |
| contoh = "Kost Putri AC KM Dlm dkt UNILA 800rb/bln, wifi kenceng" |
| res = pipe.process(contoh, trace=True) |
| print("INPUT :", res.raw) |
| for s in res.trace: |
| out = s["output"] if isinstance(s["output"], str) else " | ".join(map(str, s["output"])) |
| print(f" {s['stage']:<20} -> {out[:70]}") |
| print("HASIL :", res.processed) |
| print("Harga terdeteksi:", res.extracted_prices) |
| '''.strip() |
|
|
| QUERY = ''' |
| # Bandingkan lima model pada satu query natural language |
| q = "kos putri dekat unila wifi murah" |
| |
| def top(model, n=5): |
| if model == "bm25": ids = [(h.doc_id, h.score) for h in bm25.query(pre(q), top_k=n)] |
| elif model == "tfidf":ids = [(h.doc_id, h.score) for h in tfidf.query(pre(q), top_k=n)] |
| elif model == "neural":ids = [(h.doc_id, h.score) for h in neural.query(q, top_k=n)] |
| elif model == "hybrid":ids = [(h.doc_id, h.score) for h in hybrid.query(q, top_k=n)] |
| else: ids = smart_rank(q, bm25, listings, gz, top_k=n, preprocess=pre)[0] |
| return [listings[i].judul for i, _ in ids] |
| |
| pd.DataFrame({m: top(m) for m in ["bm25","tfidf","neural","hybrid","smart"]}) |
| '''.strip() |
|
|
| SMART = ''' |
| # Smart pipeline: query understanding + geo + fusion (model live) |
| res, understood, relaxed = (lambda r: (r[0], r[1], r[2]))( |
| smart_rank(q, bm25, listings, gz, top_k=5, preprocess=pre)) |
| print("Yang dipahami sistem dari query:") |
| for k, v in understood.items(): |
| if v: print(f" {k:<10}: {v}") |
| print("\\nTop-5 smart:") |
| for did, score in res: |
| r = listings[did] |
| print(f" [{r.tipe:<6} Rp{r.harga_per_bulan:>8} {(r.kecamatan or '-'):<14}] {r.judul[:44]}") |
| '''.strip() |
|
|
| EVAL = ''' |
| # Hasil evaluasi 30 query (dibaca dari CSV hasil eksperimen) |
| df = pd.read_csv(ROOT / "eval" / "results.csv") |
| agg = (df.groupby("model")[["p_at_5","p_at_10","ap","ndcg_at_10","rr"]] |
| .mean().round(3).sort_values("ap", ascending=False)) |
| agg.columns = ["P@5","P@10","MAP","NDCG@10","MRR"] |
| agg |
| '''.strip() |
|
|
| CHART = ''' |
| import matplotlib.pyplot as plt |
| fig, ax = plt.subplots(figsize=(7, 3.4)) |
| order = agg.sort_values("MAP") |
| colors = ["#2563eb" if m == "smart" else "#94a3b8" for m in order.index] |
| ax.barh(order.index, order["MAP"], color=colors) |
| ax.set_xlabel("MAP (standard top-K, n=30)"); ax.set_title("Perbandingan MAP per model β smart (biru) = model live") |
| for i, v in enumerate(order["MAP"]): ax.text(v + 0.004, i, f"{v:.3f}", va="center", fontsize=9) |
| plt.tight_layout(); plt.show() |
| '''.strip() |
|
|
| CONSTRAINT = ''' |
| # Constraint Satisfaction @5: % top-5 yang penuhi SEMUA kebutuhan user |
| # (gender + budget + fasilitas + radius 3km kampus) β bebas pooling bias |
| cs = pd.read_csv(ROOT / "eval" / "results_constraints.csv") |
| cols = [c for c in cs.columns if c.startswith("cs_at_5_")] |
| cs_mean = cs[cols].mean().round(3).sort_values(ascending=False) |
| cs_mean.index = [c.replace("cs_at_5_","") for c in cs_mean.index] |
| cs_mean.to_frame("mean CS@5 (n=30)") |
| '''.strip() |
|
|
| EXPERIMENTS = ''' |
| # Ringkasan eksperimen lain (dibaca dari artefak eval) |
| import json |
| abl = pd.read_csv(ROOT / "eval" / "preprocess_ablation.csv")[["config","map","delta_map"]] |
| pb = json.loads((ROOT / "eval" / "explore_pooling_bias.json").read_text())["per_model"] |
| print("Ablation preprocessing (delta MAP saat stage dimatikan):") |
| print(abl.to_string(index=False)) |
| print("\\nPooling bias β MAP saat pool adil (5 model) vs BM25-only:") |
| for m, d in pb.items(): |
| print(f" {m:<8} {d['map_bm25pool']:.3f} -> {d['map_unionpool']:.3f} (delta {d['delta']:+.3f})") |
| '''.strip() |
|
|
|
|
| def main() -> int: |
| nb = nbf.v4.new_notebook() |
| nb.cells = [ |
| md("# KozyNear β Showcase Model IR\n\n" |
| "Notebook reproducible: lima model retrieval (TF-IDF, BM25, Neural " |
| "MiniLM, Hybrid, **Smart**) di corpus **227 listing kos REAL** " |
| "Bandar Lampung. Tiap sel dieksekusi sungguhan; output di bawah " |
| "adalah hasil nyata, bukan tangkapan layar.\n\n" |
| "Mata Kuliah Temu Kembali Informasi β Universitas Lampung."), |
| md("## 1. Setup: muat corpus + index + pipeline"), |
| code(SETUP), |
| md("## 2. Preprocessing 9-stage\n\nJargon domain (`KM Dlm`β`kamar mandi " |
| "dalam`), ekstraksi harga, stemming Sastrawi β langkah demi langkah."), |
| code(PREP), |
| md("## 3. Lima model pada satu query\n\n" |
| "`\"kos putri dekat unila wifi murah\"` β bandingkan judul top-5 " |
| "tiap model."), |
| code(QUERY), |
| md("## 4. Smart pipeline (model live)\n\nMemecah query jadi constraint " |
| "terstruktur (gender/harga/fasilitas/anchor), lalu fusi teks + geo " |
| "+ atribut dengan hard filter."), |
| code(SMART), |
| md("## 5. Evaluasi 30 query β metrik standard\n\nMAP, P@K, NDCG, MRR " |
| "per model."), |
| code(EVAL), |
| code(CHART), |
| md("## 6. Constraint Satisfaction @5 (lensa kebutuhan user)\n\n" |
| "Bebas pooling bias: mengukur apakah hasil benar-benar memenuhi " |
| "gender + budget + fasilitas + jarak kampus."), |
| code(CONSTRAINT), |
| md("## 7. Eksperimen pendukung\n\nAblation preprocessing & kuantifikasi " |
| "pooling bias."), |
| code(EXPERIMENTS), |
| md("## Kesimpulan\n\n**Smart** unggul di MAP standard (0.359) dan " |
| "dominan di CS@5 (0.867 vs BM25 0.527, p=0.0001) tanpa model neural " |
| "di runtime. Skor standard cenderung meremehkan smart karena " |
| "*pooling bias* (lihat sel 7): saat pool dibuat adil, jarak smart " |
| "vs BM25 makin lebar. Detail metodologi di `LAPORAN.md`."), |
| ] |
| nb.metadata["kernelspec"] = {"name": "python3", "display_name": "Python 3", "language": "python"} |
|
|
| print("[execute] menjalankan notebook (load neural model, butuh ~1-2 menit)...") |
| ep = ExecutePreprocessor(timeout=600, kernel_name="python3") |
| ep.preprocess(nb, {"metadata": {"path": str(BACKEND)}}) |
|
|
| NB_OUT.parent.mkdir(parents=True, exist_ok=True) |
| nbf.write(nb, str(NB_OUT)) |
| print(f"[saved] {NB_OUT}") |
|
|
| exporter = HTMLExporter() |
| exporter.exclude_input_prompt = False |
| body, _ = exporter.from_notebook_node(nb) |
| HTML_OUT.parent.mkdir(parents=True, exist_ok=True) |
| HTML_OUT.write_text(body, encoding="utf-8") |
| print(f"[saved] {HTML_OUT} ({len(body)//1024} KB)") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|