Praktisi data sepakat: sebagian besar waktu pengolahan data habis untuk membersihkannya, bukan menganalisisnya. Data nyata datang dengan spasi tersembunyi, "N/A" di kolom angka, nama yang sama ditulis dengan kapital berbeda, baris terkirim dua kali, dan harga yang tidak masuk akal. Chapter ini membangun kotak peralatan pembersih secara bertahap — deteksi, normalisasi, koersi tipe, deduplikasi, pemeriksaan pencilan — lalu merangkainya menjadi satu fungsi pembersihan utuh yang bisa dipakai ulang.
Berkas Latihan yang Kotor #
Simpan berikut sebagai transaksi_kotor.csv — sembilan masalah klasik tersembunyi di tiga belas baris:
id,nama,harga,kategori,stok
P001, Kopi Gayo ,85000,minuman,120
p001,Kopi Gayo,85000,minuman,120
P002,Teh Melati,45000,Minuman,200
P003,Gula Aren,Rp 32.000,bahan pokok,75
P004,Beras Pandan Wangi,78000,bahan pokok,40
P005,Keripik Singkong,15000,camilan,250
P005,Keripik Singkong,15000,camilan,250
P006,Sirup Markisa,,minuman,60
P007,SAMBAL BAWANG,28000,Camilan,90
P008,Mie Instan Goreng,3500,bahan pokok,500
P009,Susu Kental Manis,14500,minuman,-5
P010,Keripik Kentang,15000000,camilan,10
P011,Telur Ayam,N/A,bahan pokok,80Coba temukan semuanya sebelum lanjut: spasi di sekitar " Kopi Gayo ", id p001 yang sama dengan P001 hanya beda kapital, kategori Minuman vs minuman, harga "Rp 32.000", harga kosong, "N/A", P005 terkirim dua kali, stok negatif, dan harga lima belas juta untuk keripik.
Deteksi Nilai Hilang dan Normalisasi Teks #
Nilai hilang tidak selalu berupa string kosong — "N/A", "NA", dan "-" sama artinya. Kita definisikan satu sumber kebenaran berbentuk himpunan, lalu hitung berapa banyak nilai hilang per kolom:
import csv
from collections import Counter
with open("transaksi_kotor.csv", newline="", encoding="utf-8") as berkas:
mentah = list(csv.DictReader(berkas))
NILAI_HILANG = {"", "N/A", "NA", "-"}
def hilang(nilai):
if isinstance(nilai, str):
return nilai.strip() in NILAI_HILANG
return nilai is None
kolom_kosong = Counter(
kolom
for baris in mentah
for kolom, nilai in baris.items()
if hilang(nilai)
)
print("Nilai hilang per kolom:", dict(kolom_kosong))Output yang Dihasilkan:
Nilai hilang per kolom: {'harga': 2}Selanjutnya, spasi berlebih dan kapital tidak konsisten dinormalkan. Perhatikan bahwa id kita jadikan huruf besar dan kategori huruf kecil, sedangkan nama dibiarkan apa adanya — nama merek boleh huruf besar:
def normalisasi(baris):
bersih = {}
for kolom, nilai in baris.items():
if isinstance(nilai, str):
nilai = nilai.strip()
if kolom == "id":
nilai = nilai.upper()
elif kolom == "kategori":
nilai = nilai.lower()
bersih[kolom] = nilai
return bersih
print("Sebelum:", repr(mentah[0]["nama"]))
print("Sesudah:", repr(normalisasi(mentah[0])["nama"]))
print("Sebelum:", repr(mentah[2]["kategori"]))
print("Sesudah:", repr(normalisasi(mentah[2])["kategori"]))Output yang Dihasilkan:
Sebelum: ' Kopi Gayo '
Sesudah: 'Kopi Gayo'
Sebelum: 'Minuman'
Sesudah: 'minuman'Normalisasi seperti ini bukan kosmetik — p001 dan P001 baru dikenali sebagai record sama setelah kapitalnya diseragamkan.
Koersi Tipe dengan Validasi #
Mengubah string menjadi angka harus menangani dua format sekaligus: "85000" yang bersih dan "Rp 32.000" yang khas Indonesia. Fungsi koersi mengembalikan None untuk nilai yang tak terselamatkan, bukan melempar galat:
def ke_harga(nilai):
if isinstance(nilai, int):
return nilai
angka = nilai.replace("Rp", "").replace(".", "").strip()
try:
return int(angka)
except ValueError:
return None
def ke_int(nilai):
try:
return int(nilai)
except (TypeError, ValueError):
return None
for contoh in ["Rp 32.000", "85000", "N/A", ""]:
print(f"{contoh!r:>12} -> {ke_harga(contoh)!r}")Output yang Dihasilkan:
'Rp 32.000' -> 32000
'85000' -> 85000
'N/A' -> None
'' -> NoneMengembalikan None menjaga agar satu baris buruk tidak menghentikan proses — keputusan membuang atau menyelamatkan baris tetap di tangan kita di tahap berikutnya.
Menghapus Duplikat #
Dictionary menjaga kunci unik, jadi deduplikasi adalah soal memakainya sebagai filter — kemunculan pertama menang:
def dedup(baris_list, kunci="id"):
unik = {}
for baris in baris_list:
unik.setdefault(baris[kunci], baris)
return list(unik.values())
contoh = [normalisasi(b) for b in mentah]
print(f"Sebelum dedup: {len(contoh)} baris")
print(f"Sesudah dedup: {len(dedup(contoh))} baris")Output yang Dihasilkan:
Sebelum dedup: 13 baris
Sesudah dedup: 11 barisTiga belas berkurang dua: pasangan P005 yang identik, dan p001 yang setelah dinormalkan menjadi duplikat P001. Method setdefault() hanya menulis jika kunci belum ada — sedikit kode, semantik tepat. Jika kebijakan sebaliknya dibutuhkan (data terbaru menang), cukup ganti setdefault menjadi assignment biasa.
Nilai Pencilan dan Batas Wajar #
Data yang lolos pemeriksaan tipe belum tentu masuk akal. Aturan bisnis memberi kita batas wajar — katakanlah harga produk yang sah berkisar 1.000 sampai 1.000.000 rupiah dan stok tidak mungkin negatif:
HARGA_MIN, HARGA_MAKS = 1000, 1_000_000
for baris in contoh:
harga = ke_harga(baris["harga"])
if harga is not None and not (HARGA_MIN <= harga <= HARGA_MAKS):
print(f"Harga mencurigakan: {baris['id']} {baris['nama']} = {harga:,}")
if ke_int(baris["stok"]) is not None and ke_int(baris["stok"]) < 0:
print(f"Stok negatif : {baris['id']} {baris['nama']} = {baris['stok']}")Output yang Dihasilkan:
Stok negatif : P009 Susu Kental Manis = -5
Harga mencurigakan: P010 Keripik Kentang = 15,000,000Merangkai Seluruh Tahap #
Semua peralatan kini siap dirangkai menjadi satu fungsi pembersihan. Setiap tahap menerima list dan mengembalikan list — bentuk yang sama masuk dan keluar, sehingga tahap bisa disusun seperti pipa:
def isi_tipe(baris):
return {**baris, "harga": ke_harga(baris["harga"]), "stok": ke_int(baris["stok"])}
def bersihkan_semua(baris_list):
tahap_1 = [normalisasi(b) for b in baris_list]
tahap_2 = [isi_tipe(b) for b in tahap_1]
tahap_3 = [b for b in tahap_2 if b["harga"] is not None and b["stok"] is not None]
tahap_4 = [b for b in tahap_3
if b["stok"] >= 0 and HARGA_MIN <= b["harga"] <= HARGA_MAKS]
return dedup(tahap_4)
bersih = bersihkan_semua(mentah)
print(f"Sebelum pembersihan: {len(mentah)} baris")
print(f"Sesudah pembersihan: {len(bersih)} baris")
for baris in bersih:
print(baris)Output yang Dihasilkan:
Sebelum pembersihan: 13 baris
Sesudah pembersihan: 7 baris
{'id': 'P001', 'nama': 'Kopi Gayo', 'harga': 85000, 'kategori': 'minuman', 'stok': 120}
{'id': 'P002', 'nama': 'Teh Melati', 'harga': 45000, 'kategori': 'minuman', 'stok': 200}
{'id': 'P003', 'nama': 'Gula Aren', 'harga': 32000, 'kategori': 'bahan pokok', 'stok': 75}
{'id': 'P004', 'nama': 'Beras Pandan Wangi', 'harga': 78000, 'kategori': 'bahan pokok', 'stok': 40}
{'id': 'P005', 'nama': 'Keripik Singkong', 'harga': 15000, 'kategori': 'camilan', 'stok': 250}
{'id': 'P007', 'nama': 'SAMBAL BAWANG', 'harga': 28000, 'kategori': 'camilan', 'stok': 90}
{'id': 'P008', 'nama': 'Mie Instan Goreng', 'harga': 3500, 'kategori': 'bahan pokok', 'stok': 500}Tiga belas baris menjadi tujuh: dua duplikat, dua nilai harga yang hilang, satu stok negatif, dan satu pencilan tersaring — semuanya melalui aturan yang eksplisit, bukan kebetulan. Harga "Rp 32.000" milik P003 juga terselamatkan menjadi 32000 berkat koersi format mata uang.
Memilih Strategi untuk Tiap Masalah #
| Masalah | Opsi penanganan | Kapan dipakai |
|---|---|---|
| Nilai hilang | buang baris | kolom tersebut krusial (harga, qty) |
| Nilai hilang | isi nilai pengganti (0, rata-rata, nilai sebelumnya) | analisis tetap bermakna tanpa nilai asli |
| Duplikat | buang, pertahankan kemunculan pertama/terakhir | tergantung sumber mana yang paling tepercaya |
| Format tak konsisten | normalisasi (strip, kapital, koersi) | hampir selalu — murah dan aman |
| Pencilan | buang, tandai, atau periksa manual | wajib ada aturan bisnis yang eksplisit |
pandas menyediakan versi ekspresif dari hampir semua operasi ini (dropna, fillna, drop_duplicates) dan dibahas khusus pada chapter Data Cleaning dengan Pandas. Akan tetapi logika keputusannya — kapan membuang, kapan mengisi, batas wajar berapa — tetap sama dan justru itulah yang perlu kita pahami lebih dulu.
Data bersih baru setengah pekerjaan. Chapter berikutnya membentuknya menjadi informasi: field turunan, pengelompokan, dan agregasi.