Data mentah yang terbaca utuh belum berarti data yang bisa dihitung: 'Rp 85.000' tidak bisa dijumlah, 'Selesai' dan 'selesai' adalah dua string berbeda bagi mesin, dan baris duplikat akan menggelembungkan GMV. Pada bagian ini kita menulis src/clean.py yang menerapkan aturan pembersihan eksplisit untuk kedua sumber. Prinsipnya satu: tidak ada baris yang dibuang tanpa jejak — setiap baris yang ditolak masuk karantina beserta alasannya, sehingga rekonsiliasi mentah = bersih + duplikat + karantina selalu bisa diverifikasi. Materi pendukung ada pada chapter Deduplikasi Data dan Penanganan Data yang Gagal.
Aturan Pembersihan yang Mengikat #
Enam aturan berikut ditulis dalam docstring modul dan diuji satu per satu pada tests/test_clean.py:
- Koersi angka:
Rp 85.000,Rp65.000, dan145000semuanya diuraikan menjadi bilangan bulat. - Koersi tanggal: format
YYYY-MM-DDdanDD/MM/YYYYditerima; keduanya dinormalkan ke ISO. - Normalisasi status: huruf kecil dan tanpa spasi; nilai di luar
selesai,dibatalkan,dikembalikanditolak. - Validasi baris:
pesanan_id,pelanggan_id,produk_idwajib terisi;kuantitas >= 1;harga_satuan > 0. - Deduplikasi: pesanan ganda disisakan kemunculan terakhir (ekspor ulang menimpa); event ganda disisakan kemunculan pertama (pengiriman ulang dari tracker).
- Total dihitung ulang dari
kuantitas * harga_satuan - diskonsehingga salah ketik pada kolom total terdeteksi dan diperbaiki.
Koersi dan Validasi Baris #
Fungsi ke_angka dan ke_tanggal adalah garda pertama; keduanya menolak dengan pengecualian BarisDitolak yang membawa alasan:
def ke_angka(nilai, kolom):
"""Uraikan nilai rupiah/integer teks menjadi bilangan bulat."""
teks = (
str(nilai).strip().lower().replace("rp", "").replace(" ", "").replace(".", "")
)
if not teks:
raise BarisDitolak(f"kolom {kolom} kosong")
try:
return int(teks)
except ValueError:
raise BarisDitolak(f"kolom {kolom} bukan angka: {nilai!r}") from Nonedef ke_tanggal(nilai):
"""Uraikan tanggal dengan dua format yang dikenal."""
teks = str(nilai).strip()
for pola in ("%Y-%m-%d", "%d/%m/%Y"):
try:
return datetime.strptime(teks, pola).date()
except ValueError:
continue
raise BarisDitolak(f"format tanggal tidak dikenal: {nilai!r}") from NoneKedua potongan di atas menempati src/clean.py baris 42-52 dan baris 63-72. Mari kita telaah baris-barisnya:
ke_angkabaris 45 (str(nilai).strip().lower()...): pembersihan teks dilakukan berantai — spasi di tepi dihapus, huruf dikecilkan, lalurp, spasi, dan titik ribuan dibuang.Rp 85.000menjadi85000.ke_angkabaris 47-52 (raise BarisDitolak): string kosong dan teks non-angka ditolak dengan pesan yang menyebut kolomnya, bukan mengembalikanNaN.ke_tanggalbaris 66 (for pola in (...)): dua format tanggal diuji berurutan; formatDD/MM/YYYYditafsirkan tanggal-bulan-tahun sesuai kebiasaan ekspor lokal.ke_tanggalbaris 72 (from None): rantai pengecualian diputus agar pesan galat menampilkan alasan pembersihan, bukan jejakstrptimeinternal.
Daftar lengkap kode format tanggal yang diterima strptime tercantum pada dokumentasi resmi Python di https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
sehingga menambah format baru cukup satu baris pada daftar pola.
Fungsi _baris_pesanan_bersih kemudian merangkai seluruh aturan untuk satu baris: pengenal diperiksa, angka dikoersi, status divalidasi, dan total dihitung ulang. Baris sehat mengembalikan dict bersih plus penanda apakah total sumbernya salah; baris sakit menaikkan BarisDitolak yang ditangkap pemanggilnya.
Deduplikasi dengan Arah yang Berbeda #
Strategi dedup untuk kedua sumber sengaja berbeda arah, dan alasannya melekat pada karakter sumber:
| Sumber | Kunci duplikat | Disisakan | Alasan |
|---|---|---|---|
| pesanan | pesanan_id + produk_id | kemunculan terakhir | ekspor ulang berisi koreksi data lama |
| events | event_id | kemunculan pertama | tracker mengirim ulang event yang sama |
Pada pesanan, dedup memakai duplicated(subset=..., keep="last") sehingga baris duplikat parsial ORD-2110 — kemunculan pertamanya memuat total salah ketik 17000 — tersisakan kemunculan terakhir yang benar. Pada events, kumpulan event_id yang sudah dipakai menentukan baris kedua sebagai duplikat.
Menjalankan Pembersihan #
Skrip berikut menjalankan pembersihan penuh pada kedua sumber dan menyimpan hasilnya ke data/staging/:
from src import clean, collect
df = collect.baca_pesanan("data/raw/pesanan.csv")
events, karantina_baca = collect.baca_events("data/raw/events.jsonl")
hasil_p = clean.bersihkan_pesanan(df)
hasil_e = clean.bersihkan_events(events, karantina_baca)
ringkasan = clean.simpan_staging("data/staging", hasil_p, hasil_e)
print("ringkasan pembersihan:")
for sumber in ("pesanan", "events"):
r = ringkasan[sumber]
print(
f" {sumber:8s}: mentah {r['baris_mentah']:3d} -> bersih {r['baris_bersih']:3d}, "
f"duplikat {r['duplikat']}, karantina {r['karantina']}"
)
print(f" total diperbaiki: {ringkasan['pesanan']['total_diperbaiki']} baris")
print("karantina pesanan:")
for _, k in hasil_p["karantina"].iterrows():
print(f" {k['pesanan_id']} ({k['produk_id']}): {k['alasan']}")
print("karantina events:")
for k in hasil_e["karantina"]:
if "baris" in k:
print(f" baris {k['baris']}: {k['alasan']}")
else:
print(f" {k['isi'].get('event_id')}: {k['alasan']}")Output yang Dihasilkan:
ringkasan pembersihan:
pesanan : mentah 38 -> bersih 33, duplikat 2, karantina 3
events : mentah 116 -> bersih 112, duplikat 1, karantina 3
total diperbaiki: 1 baris
karantina pesanan:
ORD-2199 (PRD-006): kuantitas harus >= 1 (dapat -2)
ORD-2198 (PRD-002): status tidak dikenal: 'batal'
ORD-2197 (PRD-005): kolom pelanggan_id kosong
karantina events:
baris 116: JSON tidak valid
EVT-0113: event tidak dikenal: 'scrool_produk'
EVT-0114: pelanggan_id kosongRekonsiliasi kedua sumber tertutup rapi: 38 = 33 + 2 + 3 untuk pesanan dan 116 = 112 + 1 + 3 untuk events. Tiga baris karantina pesanan masing-masing melanggar aturan berbeda, dan baris ORD-2107 yang totalnya salah ketik (1600000) diperbaiki menjadi 160000 karena total selalu dihitung ulang dari komponen.
Berkas Staging dan Karantina #
simpan_staging menulis lima berkas ke data/staging/: pesanan_bersih.csv, events_bersih.jsonl, karantina_pesanan.csv, karantina_events.jsonl, dan ringkasan_pembersihan.json. Isi karantina_pesanan.csv memperlihatkan bahwa baris ditolak apa adanya — kolom asli lengkap, hanya ditambah kolom alasan:
pesanan_id,tanggal,pelanggan_id,kanal,produk_id,nama_produk,kategori,kuantitas,harga_satuan,diskon,total,status,alasan
ORD-2199,2025-01-10,PLG-006,web,PRD-006,Kopi Instan Sachet Isi 10,Kopi Instan,-2,32000,0,-64000,selesai,kuantitas harus >= 1 (dapat -2)
ORD-2198,2025-01-11,PLG-008,web,PRD-002,Kopi Robusta Temanggung 200g,Kopi Biji,1,65000,0,65000,batal,status tidak dikenal: 'batal'
ORD-2197,2025-01-12,,web,PRD-005,V60 Dripper Keramik,Alat Seduh,1,175000,0,175000,selesai,kolom pelanggan_id kosongBerkas ringkasan_pembersihan.json menyarikan angka-angka di atas dan akan dibaca langsung oleh tahap validasi pada Bagian 7. Data bersih sudah siap; pada Bagian 4 kita merancang model analitik yang mengolahnya menjadi tabel siap dashboard.