| # Data Deduplication Rules |
|
|
| > β οΈ **DOKUMEN HISTORIS** (iterasi V1βV2). Pipeline data **saat ini** |
| > (100% real Mamikos, synthetic sudah di-drop) didokumentasikan di |
| > [`data/README.md`](../data/README.md). Beberapa file yang disebut di bawah |
| > (`mamikos_real.jsonl`, `_extra`, `_merged`, `kozynear_synthetic.jsonl`) |
| > **sudah dihapus** β bagian ini disimpan sebagai catatan metodologi dedup |
| > by-judul yang dipakai saat masih ada multi-batch real + synthetic. |
|
|
| Dokumen ini menjelaskan rule dedup yang dipakai saat membentuk |
| `data/raw/kozynear_combined.jsonl` (canonical corpus) dari sub-source. |
|
|
| ## Sumber data |
|
|
| | File | Records | Source | |
| |---|---|---| |
| | `mamikos_real.jsonl` | 69 | Scrape Mamikos batch 1 (sitemap) | |
| | `mamikos_real_extra.jsonl` | 95 | Scrape Mamikos batch 2 (Playwright extra search) | |
| | `mamikos_real_merged.jsonl` | 122 | Dedup union dari batch 1 + 2 | |
| | `kozynear_synthetic.jsonl` | 2000 | Generated synthetic listings | |
| | `kozynear_combined.jsonl` | 2074 | merged + synthetic, post-cleaning | |
|
|
| ## Rule dedup `mamikos_real_merged.jsonl` |
|
|
| **Catatan**: ID Mamikos (`mamikos-{listing_id}`) berbeda untuk listing fisik |
| yang sama karena re-scrape me-generate ID baru di sesi berbeda. Jadi dedup |
| by ID gak akan menemukan duplikasi β perlu dedup by konten. |
|
|
| **Aturan**: dedup by lowercased + stripped `judul`, prefer first occurrence. |
|
|
| ```python |
| def dedup_judul(records): |
| seen = set() |
| out = [] |
| for r in records: |
| key = r["judul"].strip().lower() |
| if key not in seen: |
| seen.add(key) |
| out.append(r) |
| return out |
| |
| real = load_jsonl("mamikos_real.jsonl") # 69 records, 69 unique juduls |
| extra = load_jsonl("mamikos_real_extra.jsonl") # 95 records, 95 unique juduls |
| |
| # Prefer real (batch 1) over extra (batch 2) untuk judul yang sama |
| merged = dedup_judul(real + extra) # 122 records (69 + 53 new) |
| ``` |
|
|
| **Hasil**: 69 real (semua dipertahankan) + 53 extra (yang judul-nya belum |
| ada di real) = 122 unique juduls. |
|
|
| **Drop count**: 42 records dari extra di-skip karena judul-nya sudah ada di |
| real. |
|
|
| ## Verifikasi |
|
|
| ```python |
| real_ids = {r['id'] for r in real} |
| extra_ids = {r['id'] for r in extra} |
| merged_ids = {r['id'] for r in merged} |
| |
| assert real_ids - extra_ids == real_ids # zero ID overlap |
| assert merged_ids <= real_ids | extra_ids # subset of union |
| assert len({r['judul'].strip().lower() for r in merged}) == len(merged) # all unique |
| ``` |
|
|
| ## V2 Real Data Pipeline (2026-05-29, replaces v1 templated) |
|
|
| V1 data (122 listings di `mamikos_real_merged.jsonl`) menggunakan scrape card-only |
| + `build_full_deskripsi()` template generator β deskripsi 100% templated (bukan |
| real owner story). V2 fix ini dengan extract langsung dari halaman detail |
| Mamikos via embedded `var detail = {...}` JSON. |
|
|
| **Pipeline v2** (3 scripts): |
| 1. `backend/scripts/discover_mamikos_slugs.py` β Playwright crawl category pages |
| untuk dapat `/room/` slug URLs (fallback: WebSearch `site:mamikos.com inurl:/room/`) |
| 2. `backend/scripts/extract_mamikos_detail.py` β HTTP-only fetch detail pages, |
| parse `var detail` (~28KB JSON dengan 146 fields), map ke canonical schema |
| 3. `backend/scripts/rebuild_v2.py`: normalize, drop empty deskripsi (real-only), |
| replace `kozynear_combined.jsonl` |
|
|
| **Schema v2** (extra fields vs v1): |
| - `koordinat` [lat, lng] β REAL Mamikos data (sebelumnya null) |
| - `kampus_terdekat` β computed via haversine vs 9 universitas |
| - `url_source` β canonical Mamikos URL (sebelumnya null) |
| - `owner_name`, `available_room`, `rules`, `verified`, `view_count` |
| - `id` β REAL Mamikos internal ID (`mamikos-{_id}`), bukan hash judul |
|
|
| **Hasil 2026-05-29:** |
| - Discovery: 117 URLs (12 WebSearch queries Γ ~10 results, deduped) |
| - Extraction: 86 successful (74%), 31 failed (listing inactive/removed) |
| - Authenticity: 86/86 dengan REAL deskripsi pemilik (0 template phrase) |
| - Coverage: 100% koordinat, 100% url_source, 100% Mamikos-verified |
| |
| **Backups:** `data/raw/kozynear_combined.jsonl.v1.bak` (old templated), |
| `eval/*.csv.preV2.bak` (annotations sebelum filter v2 IDs). |
|
|
| ## Rule cleaning `kozynear_combined.jsonl` (P0 remediation, 2026-05-29) |
| |
| Setelah merge real+synth, ada 48 record yang di-drop sebagai data quality |
| remediation (lihat [eval/_audit_report.json](../eval/_audit_report.json)): |
| |
| | Kriteria | Count | Reason | |
| |---|---|---| |
| | `judul.lower().startswith("notification ikut daftar tunggu")` | 34 | Placeholder waitlist, bukan listing real | |
| | `harga_per_bulan < 200_000` | 8 | Physically impossible price | |
| | `harga_per_bulan > 6_000_000` | 6 | Implausible price + basic facilities β likely scraping error | |
|
|
| Script: [backend/scripts/clean_corpus.py](../backend/scripts/clean_corpus.py). |
| Backup original: `data/raw/kozynear_combined.jsonl.bak`. |
| Audit trail: `data/raw/dropped_dirty_docs.jsonl`. |
|
|
| **Net effect**: 2122 β 2074 records (β48, β2.3%). |
|
|
| ## Reproducibility |
|
|
| Untuk re-build canonical corpus dari scratch: |
|
|
| ```bash |
| cd backend |
| # 1. Scrape (jangan run ulang kalau gak perlu; menghasilkan ID baru) |
| python -m scripts.scrape_mamikos_sitemap # β mamikos_real.jsonl |
| python -m scripts.scrape_mamikos_playwright # β mamikos_real_extra.jsonl |
| |
| # 2. Dedup merge by judul |
| python -c " |
| import json |
| def load(p): return [json.loads(l) for l in open(p,encoding='utf-8')] |
| def dedup(rs): |
| seen=set(); out=[] |
| for r in rs: |
| k=r['judul'].strip().lower() |
| if k not in seen: seen.add(k); out.append(r) |
| return out |
| m = dedup(load('../data/raw/mamikos_real.jsonl') + load('../data/raw/mamikos_real_extra.jsonl')) |
| with open('../data/raw/mamikos_real_merged.jsonl','w',encoding='utf-8') as f: |
| for r in m: f.write(json.dumps(r,ensure_ascii=False)+'\n') |
| " |
| |
| # 3. Build canonical (real-only) β lihat data/README.md untuk pipeline current |
| python -m scripts.rebuild_v2 # β kozynear_combined.jsonl |
| |
| # 5. Clean (drop waitlist, low/high price outliers) |
| python -m scripts.clean_corpus |
| |
| # 6. Preprocess + build indexes |
| python -m scripts.preprocess_corpus --input ../data/raw/kozynear_combined.jsonl --output ../data/processed/corpus.json |
| python -m app.indexing.build --corpus ../data/processed/corpus.json --output-dir ../data/indexes |
| ``` |
|
|