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:

text
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,80

Coba 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:

python
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:

text
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:

python
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:

text
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:

python
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:

text
 'Rp 32.000' -> 32000
     '85000' -> 85000
       'N/A' -> None
          '' -> None

Mengembalikan 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:

python
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:

text
Sebelum dedup: 13 baris
Sesudah dedup: 11 baris

Tiga 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:

python
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:

text
Stok negatif      : P009 Susu Kental Manis = -5
Harga mencurigakan: P010 Keripik Kentang = 15,000,000

Merangkai 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:

python
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:

text
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 #

MasalahOpsi penangananKapan dipakai
Nilai hilangbuang bariskolom tersebut krusial (harga, qty)
Nilai hilangisi nilai pengganti (0, rata-rata, nilai sebelumnya)analisis tetap bermakna tanpa nilai asli
Duplikatbuang, pertahankan kemunculan pertama/terakhirtergantung sumber mana yang paling tepercaya
Format tak konsistennormalisasi (strip, kapital, koersi)hampir selalu — murah dan aman
Pencilanbuang, tandai, atau periksa manualwajib 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.