Data leakage adalah kesalahan evaluasi paling mahal dalam machine learning karena bekerja secara diam: skor test terlihat tinggi, tim merayakan, model dikirim ke produksi, lalu performanya anjlok. Penyebabnya selalu satu pola — informasi dari data uji atau validasi ikut masuk ke proses training, sehingga model diuji dengan soal yang jawabannya sudah pernah dilihatnya. Chapter ini mendemonstrasikan tiga bentuk kebocoran paling umum dengan angka terukur, lalu menutup dengan protokol pencegahan.
Tiga Jenis Kebocoran Umum #
| Jenis kebocoran | Gejala | Pencegahan |
|---|---|---|
| Statistik preprocessing dari seluruh data | scaler/imputasi di-fit sebelum split | fit semua transformasi hanya pada data latih |
| Duplikat lintas split | baris identik muncul di latih dan uji | dedup sebelum membagi data |
| Fitur turunan label | kolom fitur merekam informasi dari target | audit skema kolom: kapan fitur itu tercatat? |
| Seleksi fitur pada seluruh data | ranking fitur memakai label uji | seleksi di dalam pipeline yang di-fit pada latih |
Demo: Harga Setiap Kebocoran #
Berkas data_leakage.py mengukur inflasi akurasi dari tiga kebocoran sekaligus. Bagian A memakai data uji yang bergeser (simulasi data produksi dari periode berikutnya) supaya kebocoran statistik scaler terlihat; Bagian B menanam duplikat lalu mengukur efeknya pada kNN; Bagian C menambahkan satu fitur yang merekam label:
"""Data leakage: tiga kebocoran klasik dan harga bagi skor test.
Bagian A: StandardScaler di-fit pada seluruh data sebelum split.
Bagian B: duplikat yang menyeberang latih-test menggelembungkan skor.
Bagian C: fitur bocor (turunan dari label) membuat akurasi palsu.
Semua angka berasal dari eksekusi nyata dengan seed tetap.
"""
import numpy as np
from sklearn.datasets import make_classification
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
rng = np.random.default_rng(42)
# ---------- A. Kebocoran lewat statistik scaler ----------
X, y = make_classification(
n_samples=700, n_features=8, n_informative=4, random_state=11
)
skala = np.array([200.0, 0.001, 50.0, 0.02, 5.0, 0.5, 20.0, 2.0])
X = X * skala
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, y, test_size=0.35, random_state=42
)
# data uji bergeser (simulasi data produksi dari periode berikutnya)
X_uji = X_uji + np.array([400.0, 0.0, 100.0, 0.0, 10.0, 0.0, 40.0, 0.0])
def skor_lr(skal_fit_basis):
skal = StandardScaler().fit(skal_fit_basis)
model = LogisticRegression(
max_iter=5000, C=0.05, random_state=42
).fit(skal.transform(X_latih), y_latih)
return float(model.score(skal.transform(X_uji), y_uji))
salah_A = skor_lr(np.vstack([X_latih, X_uji])) # fit: latih + uji
benar_A = skor_lr(X_latih) # fit: latih saja
print("===== A. SCALER FIT SEMUA DATA vs LATIH SAJA =====")
print(f"akurasi test (salah, fit semua): {salah_A:.4f}")
print(f"akurasi test (benar, fit latih) : {benar_A:.4f}")
print(f"inflasi akibat kebocoran : {salah_A - benar_A:+.4f}")
# ---------- B. Kebocoran lewat duplikat lintas split ----------
Xb, yb = make_classification(
n_samples=400, n_features=8, n_informative=3, random_state=7
)
sumber = rng.choice(len(Xb), size=80, replace=False)
Xb_dup = np.vstack([Xb, Xb[sumber]])
yb_dup = np.concatenate([yb, yb[sumber]])
L, U, ly, uy = train_test_split(Xb_dup, yb_dup, test_size=0.35,
random_state=42)
kunci_latih = set(row.tobytes() for row in L)
nyebrang = sum(1 for row in U if row.tobytes() in kunci_latih)
model_dup = KNeighborsClassifier(n_neighbors=1).fit(L, ly)
skor_dup = float(model_dup.score(U, uy))
# dedup sebelum split (kunci bytes: deterministik penuh)
lihat, keep = set(), []
for i in range(len(Xb_dup)):
kunci = Xb_dup[i].tobytes()
if kunci not in lihat:
lihat.add(kunci)
keep.append(i)
keep = np.array(keep)
L2, U2, ly2, uy2 = train_test_split(Xb_dup[keep], yb_dup[keep],
test_size=0.35, random_state=42)
model_bersih = KNeighborsClassifier(n_neighbors=1).fit(L2, ly2)
skor_bersih = float(model_bersih.score(U2, uy2))
print()
print("===== B. DUPLIKAT LINTAS SPLIT (kNN k=1) =====")
print("baris total (dengan duplikat) :", len(Xb_dup))
print("baris setelah dedup :", len(keep))
print("baris uji yang identik dgn latih:", nyebrang)
print(f"akurasi test (tanpa dedup) : {skor_dup:.4f}")
print(f"akurasi test (dengan dedup) : {skor_bersih:.4f}")
print(f"inflasi akibat duplikat : {skor_dup - skor_bersih:+.4f}")
# ---------- C. Kebocoran lewat fitur turunan label ----------
Xc, yc = make_classification(
n_samples=600, n_features=6, n_informative=4, class_sep=1.0,
random_state=3,
)
# "fitur bocor": skor risiko yang dicatat SETELAH hasil diketahui
fitur_bocor = (yc + rng.normal(0, 0.3, len(yc))).reshape(-1, 1)
Xc_bocor = np.hstack([Xc, fitur_bocor])
L3, U3, ly3, uy3 = train_test_split(Xc, yc, test_size=0.35,
random_state=42)
L4, U4, ly4, uy4 = train_test_split(Xc_bocor, yc, test_size=0.35,
random_state=42)
model_asli = LogisticRegression(max_iter=1000, random_state=42).fit(L3, ly3)
model_bocor = LogisticRegression(max_iter=1000, random_state=42).fit(L4, ly4)
print()
print("===== C. FITUR BOCOR (turunan label) =====")
print(f"akurasi tanpa fitur bocor : {float(model_asli.score(U3, uy3)):.4f}")
print(f"akurasi dengan fitur bocor: {float(model_bocor.score(U4, uy4)):.4f}")
print("koefisien fitur 0-6 :",
np.round(model_bocor.coef_[0], 2).tolist())
print("koefisien terbesar = fitur ke-6 (fitur bocor) -> model")
print("menaruh bobot dominan pada informasi dari label.")Output yang Dihasilkan:
===== A. SCALER FIT SEMUA DATA vs LATIH SAJA =====
akurasi test (salah, fit semua): 0.7592
akurasi test (benar, fit latih) : 0.7061
inflasi akibat kebocoran : +0.0531
===== B. DUPLIKAT LINTAS SPLIT (kNN k=1) =====
baris total (dengan duplikat) : 480
baris setelah dedup : 400
baris uji yang identik dgn latih: 35
akurasi test (tanpa dedup) : 0.9464
akurasi test (dengan dedup) : 0.8786
inflasi akibat duplikat : +0.0679
===== C. FITUR BOCOR (turunan label) =====
akurasi tanpa fitur bocor : 0.8286
akurasi dengan fitur bocor: 0.9667
koefisien fitur 0-6 : [0.01, -0.28, 0.88, -0.43, 0.29, 0.6, 5.25]
koefisien terbesar = fitur ke-6 (fitur bocor) -> model
menaruh bobot dominan pada informasi dari label.Contoh di atas diuji nyata dengan scikit-learn 1.5.2 pada mesin CPU dan selesai dalam waktu di bawah lima detik. Mari kita telaah kode di atas baris per baris:
- Baris 19-29 (data A): fitur dikalikan skala yang berbeda jauh (
200.0hingga0.001) lalu data uji digeser — kondisi ini meniru data produksi yang tiba dari periode berikutnya dengan statistik bergeser. - Baris 31-36 (skor_lr): fungsi menerima basis fit scaler; jalur salah menggabungkan latih+uji sebagai basis, jalur benar hanya data latih — selisih keduanya adalah harga kebocoran.
- Baris 45-55 (duplikat): 80 baris acak disalin sehingga total menjadi 480;
train_test_splitmenempatkan salinan secara acak sehingga 35 baris uji ternyata identik dengan baris latih. - Baris 57-60 (deteksi):
row.tobytes()mengubah tiap baris menjadi kunci bytes; keanggotaan disetmendeteksi duplikat lintas split secara deterministik — teknik yang sama dipakai pada proses dedup. - Baris 62-81 (dedup): salinan dibuang sebelum split, data turun kembali ke 400 baris, dan skor dihitung ulang pada kondisi jujur.
- Baris 83-89 (fitur bocor): kolom tambahan dibangun dari label
ycplus derau — persis seperti “skor risiko” yang dihitung setelah nasabah gagal membayar; kapan kolom itu tercatat adalah pertanyaan audit yang membedakan fitur sah dari fitur bocor. - Baris 99-105 (laporan C): koefisien model memperlihatkan fitur ke-6 bernilai 5.25, jauh melampaui lima fitur sah lainnya — bukti model menaruh hampir seluruh bobotnya pada informasi yang berasal dari label.
Protokol Pencegahan #
Tiga kebiasaan menutup hampir semua celah. Pertama, dedup dilakukan sebelum split — persis seperti siklus pembersihan di chapter Menyiapkan Dataset
. Kedua, seluruh transformasi (scaling, imputasi, seleksi fitur) dibungkus sklearn.Pipeline yang di-fit hanya pada data latih, sehingga jalur salah pada demo A mustahil terjadi secara tidak sengaja — polanya dijelaskan di sklearn pipeline
. Ketiga, test set disegel: disentuh sekali di akhir proyek, tidak pernah dipakai mengambil keputusan berulang — disiplin subset yang dibahas penuh di Pembagian Data
dan Normalisasi dan Standardisasi
.
Angka evaluasi yang jujur barusan aman untuk hari ini. Pertanyaan berikutnya menyangkut pekan depan: bagaimana memastikan dataset yang dipakai ulang enam bulan kemudian masih tepat sama baris demi baris dengan yang menghasilkan angka tersebut? Jawabannya adalah versioning.