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.
| Strategi | Kapan dipakai | Risiko |
|---|---|---|
Buang baris (dropna) | Kehilangan sedikit (< ±5%), pola MCAR | Menghapus informasi; bias jika bukan MCAR |
| Buang kolom | Kolom hilang > ±40–50%, tidak kritis | Kehilangan fitur yang mungkin berguna |
mean | Kolom numerik sebaran simetris, tanpa pencilan | Tertarik oleh pencilan; mengecilkan sebaran |
median | Kolom numerik miring atau memuat pencilan | Kurang optimal bila sebaran simetris |
most_frequent | Kolom kategorikal | Memperbanyak kategori dominan |
constant + indikator | Kehilangan itu sendiri bermakna | Menambah dimensi fitur |
| Tidak mengimputasi | Target 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.
# 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:
== 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 #
- Baris 6–8 (import):
SimpleImputerdanMissingIndicatorberasal dari modulsklearn.impute. Keduanya mengikuti kontrak estimator scikit-learn yang sudah kita kenal pada Section Dasar Machine Learning:fit()untuk belajar dari data,transform()untuk menerapkan. - Baris 10–15 (data sintetis): tiga kolom dibangun dari satu
rngberseed. Kolomumursebarannya kira-kira simetris; kolompendapatan_jutaakan dibuat miring lewat dua pencilan yang ditanam. - Baris 17–22 (penanaman nilai hilang): posisi
NaNditulis eksplisit agar contoh deterministik. Dua nilai ekstrem (38 dan 45 juta) sengaja ditanam pada kolom pendapatan untuk memperlihatkan daya tarik mean terhadap pencilan. - Baris 31–37 (tiga imputer): satu imputer per strategi. Atribut
fit_transform()menggabungkanfit()dantransform()— imputer mempelajari statistik kolom, lalu mengganti setiapNaNdengan statistik tersebut. - 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. - 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.
- 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. - 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”.
- 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 #
- scikit-learn — Imputation of missing values — panduan resmi strategi imputasi.
- scikit-learn — SimpleImputer — dokumentasi API seluruh strategi.
- scikit-learn — MissingIndicator — dokumentasi transformer penanda nilai hilang.
- pandas — Working with missing data — operasi nilai hilang di sisi pandas.