kozynear / docs /data_dedup_rules.md
DYmazeh's picture
deploy: 59cac6b1dd28
03b34b2 verified
|
Raw
History Blame Contribute Delete
6.19 kB
# 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
```