Outlier masih boleh jadi kebenaran, tetapi ada nilai yang pasti salah: umur negatif, umur 250 tahun, atau persentase diskon 120. Nilai semacam ini melanggar logika domain data, bukan sekadar jauh dari rata-rata. Chapter ini membahas pola koreksi tiga langkah untuk kasus seperti itu: deteksi dengan boolean mask, penandaan sebagai NaN, lalu pengisian ulang dengan nilai yang bisa dipertanggungjawabkan.

Menetapkan Aturan Domain Tiap Kolom #

Aturan domain adalah batas yang membuat sebuah nilai masuk akal menurut logika bisnis, bukan menurut sebaran data. Aturan ini disepakati bersama pemilik data dan berbeda antar konteks: persentase diskon memang dibatasi 0 sampai 100, sedangkan batas umur pelanggan mengikuti produk yang dijual. Tabel berikut merangkum aturan yang kita pakai sepanjang chapter ini.

KolomAturan domainContoh pelanggaran
umur0 sampai 120 tahun-5 dan 250
persen_diskon0 sampai 100 persen120

Pola Koreksi: Deteksi, Tandai, Isi Ulang #

Skrip berikut menerapkan pola tiga langkah pada kedua kolom sekaligus. Kita deteksi baris bermasalah, tampilkan dulu sebagai bukti, tandai nilainya sebagai NaN, lalu isi ulang dengan strategi yang berbeda per kolom.

python
import pandas as pd
import numpy as np

data = pd.DataFrame({
    "pelanggan": ["Budi", "Siti", "Dewi", "Andi", "Rina"],
    "umur": [29, -5, 34, 250, 27],
    "persen_diskon": [10, 15, 120, 8, 25],
})

mask_umur = (data["umur"] < 0) | (data["umur"] > 120)
mask_diskon = data["persen_diskon"] > 100

print("Baris dengan nilai tidak valid:")
print(data[mask_umur | mask_diskon])
print()

data.loc[mask_umur, "umur"] = np.nan
data.loc[mask_diskon, "persen_diskon"] = np.nan

median_umur = data["umur"].median()
print("Median umur data yang valid :", median_umur)
data["umur"] = data["umur"].fillna(median_umur)
data["persen_diskon"] = data["persen_diskon"].fillna(0)

print()
print("Data setelah koreksi:")
print(data)
print(data.dtypes)

Output yang Dihasilkan:

text
Baris dengan nilai tidak valid:
  pelanggan  umur  persen_diskon
1      Siti    -5             15
2      Dewi    34            120
3      Andi   250              8

Median umur data yang valid : 29.0

Data setelah koreksi:
  pelanggan  umur  persen_diskon
0      Budi  29.0           10.0
1      Siti  29.0           15.0
2      Dewi  34.0            0.0
3      Andi  29.0            8.0
4      Rina  27.0           25.0
pelanggan            str
umur             float64
persen_diskon    float64
dtype: object

Mari kita telaah apa yang terjadi pada kode di atas:

  1. Baris 10-11 (mask_umur = ... dan mask_diskon = ...): dua mask boolean mendeteksi pelanggaran aturan domain; tanda kurung di sekitar tiap perbandingan pada mask_umur wajib ada.
  2. Baris 13-14 (print(data[mask_umur | mask_diskon])): gabungan kedua mask menampilkan tiga baris bermasalah sebelum data disentuh, sehingga keputusan koreksi punya dasar yang terlihat.
  3. Baris 17-18 (data.loc[mask_umur, "umur"] = np.nan): nilai pelanggar ditandai NaN melalui satu akses .loc; pola ini adalah cara penulisan data yang benar di pandas 3.0 dengan Copy-on-Write.
  4. Baris 20-22 (median_umur = ... dan data["umur"] = data["umur"].fillna(median_umur)): median dihitung dari sisa data yang valid, lalu menjadi nilai pengisi; median dipilih karena tidak tergeser oleh nilai ekstrem.
  5. Baris 23 (data["persen_diskon"] = data["persen_diskon"].fillna(0)): kolom diskon diisi nol sesuai asumsi bisnis bahwa baris tanpa data diskon berarti tanpa diskon.
  6. Baris 27-28: data akhir dan data.dtypes dicetak; Siti dan Andi kini berumur 29.0, dan Dewi memiliki diskon 0.0.

Mengapa Ditandai NaN Terlebih Dahulu #

Mengganti umur -5 langsung dengan median memang lebih pendek, tetapi pola penandaan NaN memberi dua keuntungan. Pertama, setiap keputusan pengisian menjadi eksplisit dan terdokumentasi di kode: median untuk umur, nol untuk diskon. Kedua, jalur ini identik dengan teknik pengisian missing values pada chapter Mengisi Nilai Kosong, sehingga satu kebiasaan yang sama melayani dua masalah berbeda.

dtype Berubah Saat NaN Masuk #

Perhatikan hasil data.dtypes di akhir output: kolom umur dan persen_diskon yang semula int64 kini bertipe float64. Pandas menyimpan NaN sebagai nilai floating-point, sehingga kolom bilangan bulat otomatis naik tipe begitu ada satu pun NaN di dalamnya; akibatnya nilai tampil sebagai 29.0, bukan 29.

Jika bentuk bilangan bulat perlu dipertahankan, buat kolom dengan dtype nullable "Int64" sejak awal. Untuk referensi lengkap strategi pengisian NaN, buka user guide missing data .

Koreksi seperti di atas menyasar nilai yang sudah kita ketahui bermasalah. Ketika data baru terus mengalir, kita butuh pemeriksaan yang dapat dijalankan ulang kapan saja; pemeriksaan sistematis itulah topik chapter Validasi Rentang Nilai.