Data nyata hampir selalu datang dengan lubang: formulir yang tidak diisi, sensor yang mati, atau sistem yang gagal mencatat. Pada tahap EDA kita sudah menemukan tiga nilai masa_kerja yang hilang, dan sekarang keputusannya harus diambil — baris dibuang, kolom dibuang, nilai diisi, atau kehilangannya justru dijadikan fitur tersendiri. Setiap pilihan punya konsekuensi berbeda terhadap model, dan pilihan yang paling sering diambil tanpa berpikir (mengisi semua lubang dengan rata-rata) ternyata punya risiko yang jarang diperhitungkan. Chapter ini membandingkan strategi-strategi tersebut secara berdampingan dengan SimpleImputer dari scikit-learn, lalu menutup dengan disiplin yang membedakan imputasi yang aman dari yang bocor informasi data uji.

Tiga Pola Kehilangan Data #

Sebelum memilih strategi, kita perlu memahami mengapa data bisa hilang — karena strategi terbaik bergantung pada polanya. Statistik membedakan tiga pola. MCAR (missing completely at random) berarti kehilangan murni acak, seperti satu lembar formulir tercabut angin: baris yang hilang tidak berbeda sistematis dari yang tersisa, sehingga membuang baris aman dilakukan. MAR (missing at random) berarti kehilangan dapat dijelaskan oleh kolom lain yang teramati — misalnya pelanggan usia muda lebih sering mengosongkan kolom pendapatan. MNAR (missing not at random) berarti kehilangan bergantung pada nilai yang hilang itu sendiri: rumah paling mahal justru tidak masuk daftar listing, transaksi terbesar tidak dicatat lengkap.

Pembedaan ini berdampak praktis. Pada MCAR, mengisi dengan mean atau median wajar dilakukan. Pada MAR, pola kehilangan itu sendiri menjadi informasi — fitur penanda “sedang hilang” sering membantu model. Pada MNAR, nilai yang kita isikan hampir pasti bias karena yang hilang justru nilai-nilai ekstrem; solusinya bukan sekadar imputasi, melainkan memperbaiki proses pengumpulan data atau setidaknya menyadari bias yang dihasilkan. Membuktikan pola secara pasti memang sulit, tetapi menanyakan “apakah alasan hilangnya berkaitan dengan nilainya sendiri?” sudah mengeliminasi sebagian besar kesalahan.

StrategiKapan dipakaiRisiko
Buang baris (dropna)Kehilangan sedikit (< ±5%), pola MCARMenghapus informasi; bias jika bukan MCAR
Buang kolomKolom hilang > ±40–50%, tidak kritisKehilangan fitur yang mungkin berguna
meanKolom numerik sebaran simetris, tanpa pencilanTertarik oleh pencilan; mengecilkan sebaran
medianKolom numerik miring atau memuat pencilanKurang optimal bila sebaran simetris
most_frequentKolom kategorikalMemperbanyak kategori dominan
constant + indikatorKehilangan itu sendiri bermaknaMenambah dimensi fitur
Tidak mengimputasiTarget yang hilang; fitur bocor— (baris dibuang atau fitur dihapus)

Contoh Kode: Perbandingan Strategi Imputasi #

Contoh berikut membangun dua belas baris data pelanggan dengan nilai hilang pada tiga kolom sekaligus — kategorikal, numerik simetris, dan numerik miring yang memuat pencilan — lalu menerapkan lima strategi berbeda dan mencetak hasilnya berdampingan.

python
# Perbandingan strategi penanganan nilai hilang dengan SimpleImputer:
# mean, median, most_frequent, constant, dan MissingIndicator.
# Bagian 1-3 membandingkan strategi pada seluruh data kecil sebagai ilustrasi;
# bagian 4 menunjukkan disiplin yang benar: fit HANYA pada data latih.

import numpy as np
import pandas as pd
from sklearn.impute import MissingIndicator, SimpleImputer

rng = np.random.default_rng(42)

n = 12
kota = rng.choice(["Jakarta", "Bandung", "Surabaya"], size=n).astype(object)
umur = rng.integers(21, 55, size=n).astype(float)
pendapatan_juta = rng.integers(6, 15, size=n).astype(float)

# Nilai hilang dan pencilan ditanam pada posisi tetap (deterministik).
umur[[2, 9]] = np.nan
pendapatan_juta[[4, 9]] = np.nan
pendapatan_juta[5] = 38.0
pendapatan_juta[10] = 45.0
kota[11] = np.nan

df = pd.DataFrame(
    {"kota": kota, "umur": umur, "pendapatan_juta": pendapatan_juta}
)
print("== Data asal (NaN = nilai hilang) ==")
print(df.to_string())

# --- 1) Umur: mean vs median vs konstanta ---
imputer_mean = SimpleImputer(strategy="mean")
imputer_median = SimpleImputer(strategy="median")
imputer_konstanta = SimpleImputer(strategy="constant", fill_value=-1.0)

umur_mean = imputer_mean.fit_transform(df[["umur"]]).ravel()
umur_median = imputer_median.fit_transform(df[["umur"]]).ravel()
umur_konstanta = imputer_konstanta.fit_transform(df[["umur"]]).ravel()

baris_hilang = df["umur"].isna().to_numpy()
tabel_umur = pd.DataFrame(
    {
        "umur_asli": df.loc[baris_hilang, "umur"].to_numpy(),
        "isi_mean": umur_mean[baris_hilang],
        "isi_median": umur_median[baris_hilang],
        "isi_konstanta": umur_konstanta[baris_hilang],
    },
    index=df.index[baris_hilang],
)
print("\n== 1) Umur: mean vs median vs konstanta ==")
print("mean kolom umur   :", round(float(imputer_mean.statistics_[0]), 3))
print("median kolom umur :", float(imputer_median.statistics_[0]))
print(tabel_umur.to_string())

# --- 2) Pendapatan: mean tertarik oleh pencilan, median tidak ---
imputer_mean_gaji = SimpleImputer(strategy="mean")
imputer_median_gaji = SimpleImputer(strategy="median")
pendapatan_mean = imputer_mean_gaji.fit_transform(
    df[["pendapatan_juta"]]
).ravel()
pendapatan_median = imputer_median_gaji.fit_transform(
    df[["pendapatan_juta"]]
).ravel()
baris_hilang_gaji = df["pendapatan_juta"].isna().to_numpy()
tabel_pendapatan = pd.DataFrame(
    {
        "pendapatan_asli": df.loc[baris_hilang_gaji, "pendapatan_juta"].to_numpy(),
        "isi_mean": pendapatan_mean[baris_hilang_gaji],
        "isi_median": pendapatan_median[baris_hilang_gaji],
    },
    index=df.index[baris_hilang_gaji],
)
print("\n== 2) Pendapatan: mean tertarik pencilan, median tidak ==")
print("mean kolom pendapatan   :", round(float(imputer_mean_gaji.statistics_[0]), 3))
print("median kolom pendapatan :", float(imputer_median_gaji.statistics_[0]))
print(tabel_pendapatan.to_string())

# --- 3) Kolom kategorikal: most_frequent ---
imputer_kota = SimpleImputer(strategy="most_frequent")
kota_terisi = imputer_kota.fit_transform(df[["kota"]]).ravel()
print("\n== 3) Kota: strategi most_frequent ==")
print("kategori terbanyak :", imputer_kota.statistics_[0])
print("baris 11 sebelum   :", df.loc[11, "kota"])
print("baris 11 sesudah   :", kota_terisi[11])

# --- 4) MissingIndicator: fitur "sedang hilang" ---
indikator = MissingIndicator(features="all")
tanda = indikator.fit_transform(df[["umur", "pendapatan_juta"]]).astype(int)
df_tanda = pd.DataFrame(tanda, columns=["umur_hilang", "pendapatan_hilang"])
print("\n== 4) MissingIndicator sebagai fitur baru ==")
print(df_tanda.to_string())

# --- 5) Disiplin: imputer di-fit hanya pada data latih ---
latih = df.iloc[:8]
uji = df.iloc[8:]
imputer_latih = SimpleImputer(strategy="mean").fit(latih[["pendapatan_juta"]])
print("\n== 5) Fit hanya pada data latih ==")
print("mean pendapatan data latih   :", round(float(imputer_latih.statistics_[0]), 3))
print("mean pendapatan seluruh data :", round(float(df["pendapatan_juta"].mean()), 3))
uji_terisi = imputer_latih.transform(uji[["pendapatan_juta"]])
print("pendapatan baris uji sebelum :", uji["pendapatan_juta"].tolist())
print(
    "pendapatan baris uji sesudah :",
    [round(float(v), 3) for v in uji_terisi.ravel()],
)

Output yang Dihasilkan:

text
== Data asal (NaN = nilai hilang) ==
        kota  umur  pendapatan_juta
0    Jakarta  46.0             13.0
1   Surabaya  46.0             11.0
2    Bandung   NaN              9.0
3    Bandung  47.0             13.0
4    Bandung  38.0              NaN
5   Surabaya  25.0             38.0
6    Jakarta  49.0             10.0
7   Surabaya  36.0              8.0
8    Jakarta  38.0              6.0
9    Jakarta   NaN              NaN
10   Bandung  27.0             45.0
11       NaN  52.0              6.0

== 1) Umur: mean vs median vs konstanta ==
mean kolom umur   : 40.4
median kolom umur : 42.0
   umur_asli  isi_mean  isi_median  isi_konstanta
2        NaN      40.4        42.0           -1.0
9        NaN      40.4        42.0           -1.0

== 2) Pendapatan: mean tertarik pencilan, median tidak ==
mean kolom pendapatan   : 15.9
median kolom pendapatan : 10.5
   pendapatan_asli  isi_mean  isi_median
4              NaN      15.9        10.5
9              NaN      15.9        10.5

== 3) Kota: strategi most_frequent ==
kategori terbanyak : Bandung
baris 11 sebelum   : nan
baris 11 sesudah   : Bandung

== 4) MissingIndicator sebagai fitur baru ==
    umur_hilang  pendapatan_hilang
0             0                  0
1             0                  0
2             1                  0
3             0                  0
4             0                  1
5             0                  0
6             0                  0
7             0                  0
8             0                  0
9             1                  1
10            0                  0
11            0                  0

== 5) Fit hanya pada data latih ==
mean pendapatan data latih   : 14.571
mean pendapatan seluruh data : 15.9
pendapatan baris uji sebelum : [6.0, nan, 45.0, 6.0]
pendapatan baris uji sesudah : [6.0, 14.571, 45.0, 6.0]

Bedah Kode Baris-per-Baris #

  1. Baris 6–8 (import): SimpleImputer dan MissingIndicator berasal dari modul sklearn.impute. Keduanya mengikuti kontrak estimator scikit-learn yang sudah kita kenal pada Section Dasar Machine Learning: fit() untuk belajar dari data, transform() untuk menerapkan.
  2. Baris 10–15 (data sintetis): tiga kolom dibangun dari satu rng berseed. Kolom umur sebarannya kira-kira simetris; kolom pendapatan_juta akan dibuat miring lewat dua pencilan yang ditanam.
  3. Baris 17–22 (penanaman nilai hilang): posisi NaN ditulis eksplisit agar contoh deterministik. Dua nilai ekstrem (38 dan 45 juta) sengaja ditanam pada kolom pendapatan untuk memperlihatkan daya tarik mean terhadap pencilan.
  4. Baris 31–37 (tiga imputer): satu imputer per strategi. Atribut fit_transform() menggabungkan fit() dan transform() — imputer mempelajari statistik kolom, lalu mengganti setiap NaN dengan statistik tersebut.
  5. Baris 39–52 (tabel perbandingan umur): hasil tiga strategi dikumpulkan menjadi satu tabel hanya untuk baris yang hilang. Atribut statistics_ memperlihatkan nilai yang dipakai tiap imputer untuk mengisi — mean 40,4 dan median 42,0 terlihat bersebelahan.
  6. Baris 54–75 (pendapatan): eksperimen yang sama pada kolom miring. Perbedaan mean dan median di sini jauh lebih lebar karena dua pencilan ikut masuk perhitungan mean.
  7. Baris 77–83 (most_frequent): strategi untuk kolom kategorikal yang tidak punya mean. Baris 11 yang kosong diisi dengan kategori terbanyak — statistics_ menyimpan kategori tersebut.
  8. Baris 85–90 (MissingIndicator): transformer ini tidak mengisi apa pun; transformer ini menghasilkan matriks penanda 0/1 untuk setiap kolom input. Penanda tersebut dapat digabungkan sebagai fitur baru sehingga model dapat memanfaatkan informasi “nilai ini sedang hilang”.
  9. Baris 92–104 (disiplin fit di data latih): imputer di-fit hanya pada delapan baris pertama, lalu dipakai untuk mentransformasi empat baris sisanya. Mean yang tersimpan (14,571) berbeda dari mean seluruh data (15,9) — dan baris uji yang hilang diisi dengan mean data latih, bukan mean keseluruhan.

Membaca Hasil: Kapan Mean Berbahaya #

Perbandingan kedua pada output adalah pelajaran inti chapter ini. Kolom umur yang sebarannya seimbang menghasilkan isi mean 40,4 dan median 42,0 — keduanya wajar, pilihan hampir tidak berbeda. Kolom pendapatan_juta bercerita lain: karena memuat dua pencilan (38 dan 45), mean melonjak ke 15,9 sementara median tetap 10,5. Setiap nilai hilang yang diisi mean akan terisi 15,9 — nilai yang lebih tinggi dari hampir seluruh data nyata. Pada kolom miring, median jauh lebih tahan; pada kolom kategorikal, most_frequent adalah satu-satunya pilihan non-konstanta. Bagian kelima menambahkan lapisan disiplin: statistik imputasi harus berasal dari data latih saja, karena mean seluruh data telah menyentuh baris data uji — pembuka menuju masalah data leakage yang dibahas tuntas pada Chapter Data Leakage dan Cara Mencegahnya.

Nilai ekstrem 38 dan 45 juta pada kolom pendapatan tadi bukan sekadar gangguan imputasi — nilai-nilai tersebut adalah pencilan yang perlu dideteksi dan diputuskan secara sadar, bukan dibuang begitu saja. Chapter berikutnya, Menangani Outlier, membahas deteksi IQR dan z-score beserta strategi penanganannya.

Referensi Resmi #