Data nyata hampir tidak pernah datang bersih. Ekspor Excel membawa harga berformat "Rp95.000", sel kosong berubah jadi NaN, ada baris terkirim dua kali, dan tanggal tertulis 05/01/2025 alih-alih standar ISO. Analisis di atas data kotor menghasilkan keputusan kotor pula — maka sebelum chapter Analisis Data Eksploratif, kita berhenti satu bab penuh untuk mencuci data dengan pandas.
Dataset Kotor Latihan #
Simpan teks berikut sebagai penjualan_kotor.csv — versi rusak dari dataset penjualan chapter Dasar Pandas, lengkap dengan tujuh jenis kotoran yang paling sering ditemui:
Tanggal,Kota,Produk,Harga,Jumlah
05/01/2025," Jakarta ",Mouse Wireless,150000,4
06/01/2025,Bandung,Kaos Polos,"Rp95.000",6
09/01/2025,Surabaya,Novel Laskar Pelangi,99000,5
09/01/2025,Surabaya,Novel Laskar Pelangi,99000,5
12/01/2025,Jakarta,Keyboard Mekanik,—,1
15/01/2025,Bandung,Powerbank 10000mAh,250000,3
18/01/2025,Surabaya,Kemeja Flanel,185000,N/A
21/01/2025,Jakarta,Atlas Indonesia,150000,2
24/01/2025,Bandung,Speaker Bluetooth,350000,
27/01/2025,Surabaya,Mouse Wireless,150000,3Tujuh masalah yang ditanam: nama kolom berkapital, spasi menempel pada " Jakarta ", harga berformat mata uang, sentinel — dan N/A, satu baris duplikat, tanggal dd/mm/yyyy, dan satu nilai Jumlah kosong. Mari kita lihat bagaimana pandas membacanya:
import pandas as pd
df = pd.read_csv("penjualan_kotor.csv")
print(df)
print()
df.info()Output yang Dihasilkan:
Tanggal Kota Produk Harga Jumlah
0 05/01/2025 Jakarta Mouse Wireless 150000 4.0
1 06/01/2025 Bandung Kaos Polos Rp95.000 6.0
2 09/01/2025 Surabaya Novel Laskar Pelangi 99000 5.0
3 09/01/2025 Surabaya Novel Laskar Pelangi 99000 5.0
4 12/01/2025 Jakarta Keyboard Mekanik — 1.0
5 15/01/2025 Bandung Powerbank 10000mAh 250000 3.0
6 18/01/2025 Surabaya Kemeja Flanel 185000 NaN
7 21/01/2025 Jakarta Atlas Indonesia 150000 2.0
8 24/01/2025 Bandung Speaker Bluetooth 350000 NaN
9 27/01/2025 Surabaya Mouse Wireless 150000 3.0
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10 entries, 0 to 9
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Tanggal 10 non-null object
1 Kota 10 non-null object
2 Produk 10 non-null object
3 Harga 10 non-null object
4 Jumlah 8 non-null float64
dtypes: float64(1), object(4)
memory usage: 532.0+ bytesinfo() sudah membocorkan akar masalah: kolom Harga berjenis object (string) karena "Rp95.000" dan "—" menghalangi inferensi angka, dan Jumlah menjadi float64 karena dua sel kosong diisi NaN.
Deteksi: Nilai Kosong dan Duplikat #
Langkah diagnosis standar menghitung nilai hilang per kolom dan jumlah baris kembar:
print(df.isna().sum())
print()
print("baris duplikat:", df.duplicated().sum())Output yang Dihasilkan:
Tanggal 0
Kota 0
Produk 0
Harga 0
Jumlah 2
dtype: int64
baris duplikat: 1Di sini ada pelajaran penting: isna() hanya mengenali NaN. Dua nilai N/A dan satu sel kosong di Jumlah memang terdeteksi — pandas secara default menganggap N/A dan string kosong sebagai NaN saat membaca CSV. Tetapi sentinel — di kolom Harga tidak terdeteksi karena bertipe string biasa. Sentinel harus diubah menjadi NaN secara eksplisit sebelum bisa dibersihkan.
Perbaikan Kolom Satu per Satu #
Seluruh perbaikan berjalan sebagai rangkaian operasi Series — pola yang sama dengan chapter DataFrames dan Series. Pertama, rapikan nama kolom dan teks kota:
import numpy as np
# Langkah 1: rapikan nama kolom menjadi lowercase
df = df.rename(columns=str.lower)
# Langkah 2: buang spasi berlebih pada teks kota
df["kota"] = df["kota"].str.strip()
# Langkah 3: ubah harga berformat "Rp95.000" dan sentinel menjadi angka
df["harga"] = df["harga"].replace("—", np.nan)
df["harga"] = (
df["harga"]
.astype("string")
.str.replace("Rp", "", regex=False)
.str.replace(".", "", regex=False)
)
df["harga"] = pd.to_numeric(df["harga"], errors="coerce")Tidak ada output — empat langkah tersebut bekerja senyap. Mari kita bedah dua langkah paling penting:
- Baris
df["kota"] = df["kota"].str.strip(): accessor.strmembuka seluruh metode string Python (strip,lower,replace) untuk dijalankan vektorial pada setiap nilai kolom;" Jakarta "menjadi"Jakarta". - Baris
df["harga"] = df["harga"].replace("—", np.nan): sentinel em-dash diubah menjadiNaNdulu agar tidak ikut diproses sebagai angka. - Baris
.str.replace("Rp", "").str.replace(".", ""): format mata uang dibongkar menjadi digit murni —"Rp95.000"menjadi"95000". - Baris
pd.to_numeric(..., errors="coerce"): konversi ke angka; nilai yang tetap gagal dikonversi (misalnyaNaN) diubah menjadiNaNalih-alih melemparValueError.
Parameter errors="coerce" adalah jaring pengaman pembersihan: satu nilai rusak tidak lagi menghentikan seluruh kolom seperti int() pada Python murni, melainkan dikonversi menjadi NaN untuk ditangani langkah berikutnya.
Lanjut ke tanggal, duplikat, dan baris yang tidak bisa diselamatkan:
# Langkah 4: tanggal dd/mm/yyyy -> datetime64
df["tanggal"] = pd.to_datetime(df["tanggal"], format="%d/%m/%Y")
# Langkah 5: buang baris duplikat lalu baris tanpa harga/jumlah
df = df.drop_duplicates()
df = df.dropna(subset=["harga", "jumlah"])
# Langkah 6: kembalikan tipe integer dan rapikan index
df["harga"] = df["harga"].astype("int64")
df["jumlah"] = df["jumlah"].astype("int64")
df = df.reset_index(drop=True)Parameter format="%d/%m/%Y" memberi tahu pd.to_datetime() urutan hari-bulan-tahun; tanpa parameter tersebut, 05/01/2025 bisa salah dibaca sebagai 5 Januari atau 1 Mei tergantung tebakan pandas. Dari sepuluh baris awal: satu duplikat dibuang, tiga baris bernilai NaN pada harga atau jumlah dibuang, tersisa enam baris bersih.
dropna atau fillna #
Membuang baris bukan satu-satunya pilihan. fillna() mengisi nilai kosong — dengan konstanta, atau dengan tetangga terdekat lewat ffill()/bfill():
import numpy as np
import pandas as pd
suhu = pd.Series([30.1, np.nan, np.nan, 29.8, np.nan])
print("asli :", suhu.tolist())
print("ffill :", suhu.ffill().tolist())
print("bfill :", suhu.bfill().tolist())
print()
stok = pd.Series([40, None, 8])
print("fillna(0):", stok.fillna(0).tolist())Output yang Dihasilkan:
asli : [30.1, nan, nan, 29.8, nan]
ffill : [30.1, 30.1, 30.1, 29.8, 29.8]
bfill : [30.1, 29.8, 29.8, 29.8, nan]
fillna(0): [40.0, 0.0, 8.0]Metode ffill() (forward fill) menyeret nilai terakhir yang valid ke depan, dan bfill (backward fill) menarik nilai valid berikutnya ke belakang. Pilihan antar keduanya menentukan kejujuran data:
| Situasi | Tindakan | Alasan |
|---|---|---|
| Data sensor berkala, celah singkat | ffill() / bfill() | nilai berangsur perlahan |
Kolom numerik, NaN berarti nol nyata | fillna(0) | misalnya jumlah penjualan kosong |
| Baris tidak bisa dipercaya isinya | dropna() | mencegah data karangan masuk analisis |
Sebelum dan Sesudah #
Hasil akhir selalu diverifikasi dengan membandingkan info() sebelum dan sesudah:
print(df)
print()
df.info()Output yang Dihasilkan:
tanggal kota produk harga jumlah
0 2025-01-05 Jakarta Mouse Wireless 150000 4
1 2025-01-06 Bandung Kaos Polos 95000 6
2 2025-01-09 Surabaya Novel Laskar Pelangi 99000 5
3 2025-01-15 Bandung Powerbank 10000mAh 250000 3
4 2025-01-21 Jakarta Atlas Indonesia 150000 2
5 2025-01-27 Surabaya Mouse Wireless 150000 3
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 tanggal 6 non-null datetime64[ns]
1 kota 6 non-null object
2 produk 6 non-null object
3 harga 6 non-null int64
4 jumlah 6 non-null int64
dtypes: datetime64[ns](1), int64(2), object(2)
memory usage: 372.0+ bytesKontras dengan kondisi awal: kolom harga kini int64, tanggal menjadi datetime64[ns], nama kolom seragam lowercase, tanpa duplikat, tanpa NaN, dan index rapi dari nol. Pola enam langkah pembersihan ini — rename, strip, konversi angka, konversi tanggal, dedup, dropna — dapat diterapkan berulang pada file ekspor apa pun, dan versi stdlib-nya telah kita bahas di chapter Membersihkan Data pada Section Data Processing.