Data leakage adalah kebocoran informasi dari data uji — atau dari informasi target — ke dalam proses pelatihan, sehingga model mengetahui sesuatu yang seharusnya tidak diketahuinya saat belajar. Kejadian ini adalah kesalahan persiapan data yang paling mahal karena bekerja secara senyap: tidak ada galat yang muncul, tidak ada peringatan yang berbunyi — yang muncul justru skor evaluasi yang indah. Laporan tertulis “akurasi 95%”, model dioperasikan, dan kinerjanya jauh di bawah angka tersebut. Seluruh disiplin yang kita bangun sepanjang Section ini — imputer di-fit di data latih, resampling hanya di data latih, encoder yang tidak menyentuh data uji — bermuara ke chapter ini: memahami bentuk-bentuk kebocoran, melihatnya terjadi dalam demonstrasi terukur, lalu membangun protokol yang membuatnya mustahil terulang.

Empat Bentuk Kebocoran yang Sering Terjadi #

Target leakage terjadi ketika sebuah fitur menyerap informasi label — sering tanpa disengaja. Contoh klasiknya pada prediksi pelanggan berhenti: kolom jumlah_refund yang isinya nol untuk semua pelanggan bertahan dan positif untuk semua pelanggan berhenti. Fitur tersebut tersedia di dataset karena tim keuangan mengisinya setelah pelanggan benar-benar berhenti; model pun mempelajari jembatan menuju jawaban. Pola yang sama: durasi_rawat_inap saat memprediksi diagnosis akhir, nilai_transaksi_terakhir yang baru terisi setelah penipuan terkonfirmasi. Pertanyaan pembedanya selalu satu: apakah nilai fitur ini sudah tersedia pada saat prediksi akan dibuat?

Train-test contamination terjadi ketika statistik atau keputusan dihitung dari seluruh data sebelum dipisah: scaler yang di-fit pada seluruh data (mean dan simpangan data uji ikut dipakai), imputer yang mengisi berdasarkan rata-rata seluruh data, seleksi fitur atau tuning hyperparameter yang menyapu data uji, resampling SMOTE yang dilakukan sebelum split. Evaluasi yang dihasilkan bukan lagi ukuran kemampuan generalisasi — angkanya ditentukan sebagian oleh data yang seharusnya diuji.

Temporal leakage menyerang data berurutan waktu: pemisahan acak pada data deret waktu membuat baris masa depan masuk ke data latih, sehingga model “sudah melihat besok”. Statistik yang tampak aman seperti rata-rata berjalan tujuh hari juga bocor bila jendelanya menjangkau masa depan. Duplikat lintas split adalah bentuk termutu: baris kembar — satu di data latih, satu di data uji — membuat evaluasi membaca hafalan, bukan generalisasi.

BentukMekanisme bocorPertanyaan pemeriksaan
Target leakageFitur menyerap label (terisi setelah peristiwa)Apakah nilai ini tersedia saat prediksi dibuat?
Train-test contaminationStatistik fit/transform memuat data ujiApakah transformer di-fit sebelum atau sesudah split?
Temporal leakageData masa depan masuk ke pelatihanApakah waktu baris latih selalu sebelum baris uji?
Duplikat lintas splitBaris kembar tersebar di dua sisiApakah data sudah dideduplikasi sebelum split?

Contoh Kode: Kebocoran yang Terukur #

Demonstrasi berikut memperlihatkan dua bentuk kebocoran dengan angka nyata. Bagian pertama meng-fit StandardScaler pada seluruh data alih-alih data latih; bagian kedua menambahkan satu fitur yang menyerap label. Model dan pemisahan data dibuat identik — satu-satunya perbedaan adalah kebocorannya.

python
# Demonstrasi dua bentuk data leakage yang paling sering terjadi:
# 1) Train-test contamination: scaler di-fit pada seluruh data (termasuk uji).
# 2) Target leakage: fitur yang menyerap informasi label.
# Keduanya menghasilkan skor indah yang runtuh saat model menyentuh data nyata.

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier

rng = np.random.default_rng(42)

# ---------- Bagian 1: scaler di-fit pada seluruh data ----------
n = 120
skor_kredit = rng.normal(100, 10, size=n)  # skala ratusan
usia = rng.normal(50, 5, size=n)           # skala puluhan

# Satu pelanggan berskor ekstrem (pencilan seperti chapter sebelumnya),
# dan pelanggan tersebut kebetulan jatuh di data uji.
skor_kredit[11] = 420.0

za = (skor_kredit - 100) / 10
zb = (usia - 50) / 5
logit = 1.4 * za - 1.4 * zb + rng.normal(0, 1.6, size=n)
y = ((1 / (1 + np.exp(-logit))) > 0.5).astype(int)
X = np.column_stack([skor_kredit, usia])

X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.3, random_state=42
)

# Cara benar: statistik scaler hanya dipelajari dari data latih.
scaler_benar = StandardScaler().fit(X_latih)
model_benar = KNeighborsClassifier(n_neighbors=5).fit(
    scaler_benar.transform(X_latih), y_latih
)
skor_benar = model_benar.score(scaler_benar.transform(X_uji), y_uji)

# Cara keliru: statistik scaler dihitung dari seluruh data, sehingga
# nilai ekstrem milik data uji ikut menentukan pemetaan seluruh baris.
scaler_bocor = StandardScaler().fit(X)
model_bocor = KNeighborsClassifier(n_neighbors=5).fit(
    scaler_bocor.transform(X_latih), y_latih
)
skor_bocor = model_bocor.score(scaler_bocor.transform(X_uji), y_uji)

prediksi_benar = model_benar.predict(scaler_benar.transform(X_uji))
prediksi_bocor = model_bocor.predict(scaler_bocor.transform(X_uji))
berubah = int((prediksi_benar != prediksi_bocor).sum())

print("== Bagian 1: train-test contamination lewat scaler ==")
print(
    "simpangan skor_kredit (latih vs seluruh data) :",
    round(float(scaler_benar.scale_[0]), 2),
    "vs",
    round(float(scaler_bocor.scale_[0]), 2),
)
print(f"scaler dari data latih saja : akurasi uji {skor_benar:.3f}")
print(f"scaler dari seluruh data    : akurasi uji {skor_bocor:.3f}")
print(f"prediksi data uji yang berubah : {berubah} dari {len(y_uji)} baris")

# ---------- Bagian 2: target leakage lewat fitur sempurna ----------
pendapatan_juta = rng.integers(4, 25, size=n).astype(float)
lama_langganan_bulan = rng.integers(1, 60, size=n).astype(float)

logit2 = 0.15 * (14 - pendapatan_juta) + 0.02 * (36 - lama_langganan_bulan)
logit2 = logit2 + rng.normal(0, 0.8, size=n)
churn = ((1 / (1 + np.exp(-logit2))) > 0.5).astype(int)

# Fitur bocor: refund hanya pernah terjadi pada pelanggan yang berhenti.
jumlah_refund = np.where(churn == 1, rng.integers(1, 5, size=n), 0).astype(float)

print("\n== Bagian 2: target leakage lewat fitur sempurna ==")
print("pelanggan bertahan dengan refund > 0 :", int(((jumlah_refund > 0) & (churn == 0)).sum()))
print("pelanggan berhenti dengan refund > 0 :", int(((jumlah_refund > 0) & (churn == 1)).sum()))

X_bersih = np.column_stack([pendapatan_juta, lama_langganan_bulan])
X_bocor_target = np.column_stack(
    [pendapatan_juta, lama_langganan_bulan, jumlah_refund]
)

Xl_b, Xu_b, yl_b, yu_b = train_test_split(X_bersih, churn, test_size=0.3, random_state=42)
model_bersih = DecisionTreeClassifier(random_state=42).fit(Xl_b, yl_b)
skor_bersih = model_bersih.score(Xu_b, yu_b)

Xl_k, Xu_k, yl_k, yu_k = train_test_split(
    X_bocor_target, churn, test_size=0.3, random_state=42
)
model_bocor_target = DecisionTreeClassifier(random_state=42).fit(Xl_k, yl_k)
skor_bocor_target = model_bocor_target.score(Xu_k, yu_k)

print("\n== Ringkasan: skor jujur vs skor terkontaminasi ==")
print(f"scaler dari data latih saja      : akurasi {skor_benar:.3f}")
print(f"scaler dari seluruh data (bocor) : akurasi {skor_bocor:.3f}")
print(f"model tanpa fitur jumlah_refund  : akurasi {skor_bersih:.3f}")
print(f"model dengan fitur jumlah_refund : akurasi {skor_bocor_target:.3f}")

Output yang Dihasilkan:

text
== Bagian 1: train-test contamination lewat scaler ==
simpangan skor_kredit (latih vs seluruh data) : 7.87 vs 30.17
scaler dari data latih saja : akurasi uji 0.889
scaler dari seluruh data    : akurasi uji 0.917
prediksi data uji yang berubah : 1 dari 36 baris

== Bagian 2: target leakage lewat fitur sempurna ==
pelanggan bertahan dengan refund > 0 : 0
pelanggan berhenti dengan refund > 0 : 65

== Ringkasan: skor jujur vs skor terkontaminasi ==
scaler dari data latih saja      : akurasi 0.889
scaler dari seluruh data (bocor) : akurasi 0.917
model tanpa fitur jumlah_refund  : akurasi 0.750
model dengan fitur jumlah_refund : akurasi 1.000

Bedah Kode Baris-per-Baris #

  1. Baris 16–21 (data dan pencilan): dua fitur dengan skala berbeda dibangun dari rng berseed. Baris 21 menanam satu nilai ekstrem pada indeks 11 — pelanggan yang, berdasarkan pemisahan random_state=42, jatuh ke data uji. Inilah “bahan bocor” bagian pertama.
  2. Baris 23–31 (label dan split): label dihitung dari kombinasi linier kedua fitur plus derau, lalu data dipisah sekali dan tidak pernah diacak ulang — kedua jalur berikutnya memakai pemisahan yang persis sama sehingga perbandingannya adil.
  3. Baris 33–38 (jalur benar): StandardScaler().fit(X_latih) hanya membaca 84 baris latih; KNN kemudian dilatih dan diuji pada data yang diskalakan dengan statistik data latih.
  4. Baris 40–46 (jalur bocor): StandardScaler().fit(X) membaca seluruh 120 baris — termasuk 36 baris uji. Modelnya masih KNN yang sama; yang berubah hanya statistik pemetaan.
  5. Baris 48–50 (prediksi yang berubah): prediksi kedua model pada data uji dibandingkan satu per satu — jumlah baris yang berubah adalah bukti paling konkret bahwa kebocoran mengubah hasil.
  6. Baris 63–72 (fitur sempurna): label churn dibangun dari pendapatan dan masa langganan, lalu jumlah_refund diisi positif hanya untuk pelanggan berhenti — meniru kolom operasional yang terisi setelah peristiwa terjadi.
  7. Baris 74–76 (bukti kebocoran): hitungan refund > 0 per kelas memperlihatkan pola 0 lawan 65 — fitur tersebut memisahkan label secara sempurna.
  8. Baris 83–91 (dua model identik): DecisionTreeClassifier yang sama dengan pemisahan yang sama dilatih dua kali; satu-satunya perbedaan adalah kehadiran kolom jumlah_refund.

Membaca Hasil: Dua Kebocoran, Dua Wajah #

Bagian pertama memperlihatkan kebocoran yang halus. Simpangan skor_kredit yang dipakai scaler melonjak dari 7,87 menjadi 30,17 — nilai ekstrem milik satu baris data uji ikut membesarkan statistik pemetaan hampir empat kali lipat. Akibatnya satu dari 36 prediksi uji berubah dan akurasi naik dari 0,889 ke 0,917. Perhatikan bahwa versi bocor justru mendapat skor lebih tinggi — dan itulah jebakannya: angka yang dihitung dengan statistik data uji bukan lagi ukuran generalisasi. Arahnya bisa naik bisa turun tergantung kebetulan; yang pasti, angka tersebut tidak lagi bisa dipercaya sebagai perkiraan kinerja di data baru.

Bagian kedua memperlihatkan wajah yang tidak halus sama sekali. Model tanpa fitur bocor mencatat akurasi 0,750 — jujur untuk data yang memang tumpang tindih. Menambahkan satu kolom jumlah_refund melonjakkan akurasi menjadi 1,000 sempurna, karena pohon keputusan menemukan bahwa fitur tersebut memisahkan label tanpa galat. Di produksi, model ini tidak berguna: jumlah_refund belum terisi ketika prediksi harus dibuat, jadi “akurasi sempurna” tersebut tidak pernah bisa direproduksi di dunia nyata. Selisih 0,250 antara dua angka itu adalah harga yang dibayar saat laporan dan kenyataan berpisah.

Protokol Pencegahan #

Urutan pada diagram di atas meringkas protokol lengkapnya. Pertama, audit kolom sebelum melatih: untuk setiap fitur, tanyakan apakah nilainya tersedia pada saat prediksi akan dibuat — jika tidak, kolom tersebut dikeluarkan. Kedua, pisahkan data sebelum transformasi apa pun; deduplikasi juga dilakukan sebelum split agar baris kembar tidak tersebar. Ketiga, semua transformer di-fit hanya pada data latih — imputer, scaler, encoder, seleksi fitur, resampler. Keempat, data uji disentuh tepat sekali, pada evaluasi final; semua eksperimen dan tuning berlangsung di data latih lewat cross-validation. Kelima, data deret waktu memakai pemisahan temporal, bukan acak. Keenam, kumpulkan seluruh langkah dalam satu Pipeline scikit-learn sehingga urutan yang benar diterapkan secara otomatis — materi Section Pipeline dan Produktivitas.

Dengan protokol ini, tahap persiapan data tuntas: data termuat benar, dipahami lewat EDA, nilai hilang dan pencilannya ditangani secara sadar, skala dan encodingnya tepat, ketimpangan kelasnya dijawab, dan kebocoran dikunci. Seluruh bekal tersebut kita bawa ke jenis masalah pertama yang dibedah mendalam pada Section Regresi — dimulai dari Chapter Konsep Regresi.

Referensi Resmi #