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:

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

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

python
import pandas as pd

df = pd.read_csv("penjualan_kotor.csv")
print(df)
print()
df.info()

Output yang Dihasilkan:

text
      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+ bytes

info() 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:

python
print(df.isna().sum())
print()
print("baris duplikat:", df.duplicated().sum())

Output yang Dihasilkan:

text
Tanggal    0
Kota       0
Produk     0
Harga      0
Jumlah     2
dtype: int64

baris duplikat: 1

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

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

  1. Baris df["kota"] = df["kota"].str.strip(): accessor .str membuka seluruh metode string Python (strip, lower, replace) untuk dijalankan vektorial pada setiap nilai kolom; " Jakarta " menjadi "Jakarta".
  2. Baris df["harga"] = df["harga"].replace("—", np.nan): sentinel em-dash diubah menjadi NaN dulu agar tidak ikut diproses sebagai angka.
  3. Baris .str.replace("Rp", "").str.replace(".", ""): format mata uang dibongkar menjadi digit murni — "Rp95.000" menjadi "95000".
  4. Baris pd.to_numeric(..., errors="coerce"): konversi ke angka; nilai yang tetap gagal dikonversi (misalnya NaN) diubah menjadi NaN alih-alih melempar ValueError.

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:

python
# 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():

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

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

SituasiTindakanAlasan
Data sensor berkala, celah singkatffill() / bfill()nilai berangsur perlahan
Kolom numerik, NaN berarti nol nyatafillna(0)misalnya jumlah penjualan kosong
Baris tidak bisa dipercaya isinyadropna()mencegah data karangan masuk analisis

Sebelum dan Sesudah #

Hasil akhir selalu diverifikasi dengan membandingkan info() sebelum dan sesudah:

python
print(df)
print()
df.info()

Output yang Dihasilkan:

text
     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+ bytes

Kontras 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.