Prinsip pada chapter sebelumnya — akurasi latih tidak berarti, data uji disentuh sekali — membutuhkan mekanika konkret untuk dijalankan. Mekanika paling sederhana dan paling sering dipakai adalah holdout: pisahkan sebagian data sebelum melatih model, lalu gunakan bagian tersebut sebagai ujian. Chapter ini membedah fungsi train_test_split dari scikit-learn parameter demi parameter, membuktikan pentingnya stratify pada data tidak seimbang, lalu memperlihatkan kelemahan mendasar holdout: pada data kecil, satu-satunya angka yang dihasilkannya bisa berayun cukup jauh hanya karena keberuntungan pembagian.

Mekanika Metode Holdout #

Holdout bekerja dalam tiga langkah. Pertama, sebelum model apa pun dilatih, sebagian data (lazimnya 20–30%) disisihkan ke “amplop tertutup” bernama data uji. Kedua, model dilatih hanya pada sisanya, yaitu data latih. Ketiga, setelah model selesai disetel, data uji dibuka tepat satu kali untuk mengukur performa. Karena data uji tidak pernah ikut dalam proses fit(), skor yang dihasilkannya adalah perkiraan generalisasi yang jujur — setidaknya sejujur yang bisa diberikan satu pembagian data.

Porsi yang disisihkan adalah kompromi. Data uji terlalu kecil membuat perkiraan tidak stabil; data uji terlalu besar membuat model kehilangan bahan belajar. Praktik umum untuk klasifikasi adalah test_size antara 0,2 hingga 0,3; untuk dataset besar (ratusan ribu sampel), porsi 1% pun sudah cukup karena jumlah absolutnya tetap besar.

Parameter train_test_split yang Perlu Dipahami #

Fungsi train_test_split terlihat sederhana, tetapi lima parameternya menentukan kualitas seluruh evaluasi berikutnya.

ParameterNilai umumEfek
test_size0,2–0,3Porsi data yang disisihkan sebagai ujian
train_size— (ikut sisa)Porsi data latih; cukup salah satu dari keduanya
random_state42Mengunci pembagian agar bisa direproduksi
shuffleTrue (bawaan)Mengacak urutan sebelum membelah data
stratifyy (klasifikasi)Menjaga proporsi kelas di kedua sisi split

Parameter shuffle perlu penjelasan khusus karena bawaannya sering dianggap remeh. Dataset nyata sering tersimpan terurut — semua transaksi sah lebih dulu, semua transaksi penipuan di belakangnya. Tanpa pengacakan, pembelahan berurutan bisa menempatkan hampir seluruh kelas langka di satu sisi saja, sehingga model belajar dari data yang timpang dan diuji pada data yang timpang secara berbeda. Bawaan shuffle=True melindungi kita dari kejadian tersebut; mematikanya hanya masuk akal untuk data yang memang harus dijaga urutannya, seperti deret waktu.

Stratify dalam Angka: Proporsi Kelas yang Terjaga #

stratify menuntaskan masalah yang tidak sepenuhnya diatasi shuffle: pada data tidak seimbang yang banyak, pengacakan pun bisa menghasilkan data uji yang proporsinya melenceng dari dataset aslinya. Mari kita buktikan dengan 100 sampel berproporsi kelas 70:30 — ukuran yang lazim ketika label harus diberi satu per satu oleh analis.

python
# Holdout: satu split dengan dua peran (latih dan uji).
# Stratifikasi menjaga proporsi kelas pada kedua sisi split;
# seed berbeda memperlihatkan variasi estimasi holdout pada data kecil.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split

# 100 sampel dengan proporsi kelas 70:30 -- ukuran yang lazim dijumpai
# saat label mahal dikumpulkan (labeling manual oleh analis).
X, y = make_classification(
    n_samples=100,
    n_features=8,
    n_informative=6,
    n_redundant=2,
    weights=[0.70, 0.30],
    flip_y=0.08,
    class_sep=1.0,
    random_state=42,
)
print(
    f"Dataset penuh  : kelas 0 = {int(np.sum(y == 0))} ,"
    f" kelas 1 = {int(np.sum(y == 1))}"
)

# Seed 6 dipakai untuk kedua split di bawah supaya satu-satunya
# perbedaan hanyalah parameter stratify (bukan keberuntungan seed).
# Tanpa stratify, kebetulan uji menjadi sangat timpang (25 : 5).
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.3, random_state=6
)
print(
    f"Tanpa stratify : latih = {int(np.sum(y_latih == 0))} : {int(np.sum(y_latih == 1))} ,"
    f" uji = {int(np.sum(y_uji == 0))} : {int(np.sum(y_uji == 1))}"
)

X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.3, stratify=y, random_state=6
)
print(
    f"Dengan stratify: latih = {int(np.sum(y_latih == 0))} : {int(np.sum(y_latih == 1))} ,"
    f" uji = {int(np.sum(y_uji == 0))} : {int(np.sum(y_uji == 1))}"
)

# Model dan data tetap sama; HANYA seed split yang berubah.
# Dua angka yang berbeda = variansi estimasi holdout yang nyata.
print()
for seed in (42, 13):
    Xl, Xu, yl, yu = train_test_split(
        X, y, test_size=0.3, stratify=y, random_state=seed
    )
    model = LogisticRegression(max_iter=1000, random_state=42)
    model.fit(Xl, yl)
    akurasi = accuracy_score(yu, model.predict(Xu))
    print(f"seed {seed:2d}     : akurasi uji = {akurasi:.3f}")

Output yang Dihasilkan:

text
Dataset penuh  : kelas 0 = 70 , kelas 1 = 30
Tanpa stratify : latih = 45 : 25 , uji = 25 : 5
Dengan stratify: latih = 49 : 21 , uji = 21 : 9

seed 42     : akurasi uji = 0.800
seed 13     : akurasi uji = 0.733

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 13–22 (make_classification): dataset 100 sampel dibuat dengan weights=[0.70, 0.30] sehingga proporsi kelas persis 70:30, dan random_state=42 mengunci dataset ini untuk selamanya.
  2. Baris 31–33 versus 39–41 (dua train_test_split): kedua pemanggilan memakai test_size=0.3 dan random_state=6 yang sama; satu-satunya perbedaan ada pada parameter stratify=y di pemanggilan kedua. Dengan menyamakan seed, efek yang terlihat murni berasal dari stratifikasi.
  3. Baris 50–57 (for seed in (42, 13)): model LogisticRegression yang sama dilatih pada dua pembagian data yang berbeda seed-nya. Data dan algoritme tidak berubah sedikit pun — hanya keberuntungan pembagian.

Bacaan output bagian pertama menunjukkan inti masalah. Tanpa stratify, data uji berisi 25 sampel kelas 0 dan hanya 5 sampel kelas 1 — kelas 1 terwakili 17% padahal di dataset 30%. Model yang diuji pada set seperti ini diuji pada soal yang tidak mewakili populasi. Dengan stratify=y, kedua sisi mempertahankan rasio 70:30 secara persis: latih 49:21, uji 21:9. Pada data ribuan sampel pengacakan biasa biasanya cukup; pada data ratusan sampel atau kurang, stratify wajib dipakai untuk klasifikasi.

Ketika Holdout Gagal: Variansi pada Data Kecil #

Bagian kedua output memperlihatkan kelemahan struktural holdout. Model dan data persis sama, tetapi seed 42 menghasilkan akurasi uji 0,800 sementara seed 13 menghasilkan 0,733 — selisih hampir 7 poin persentase hanya karena 30 sampel uji yang berbeda. Dengan data uji sekecil itu, nasib 2 sampel yang berpindah kiri-kanan sudah menggeser angka sejauh 6,7 poin. Angka tunggal dari holdout dengan demikian membawa ketidakpastian yang tidak terlihat: kita mendapat satu perkiraan, tetapi tidak tahu seberapa lebar rentang kebenarannya.

Solusinya bukan memilih seed yang “untung”, melainkan memakai setiap sampel secara bergiliran sebagai data uji sehingga estimasi dirata-rata dari beberapa pembagian sekaligus. Prosedur tersebut bernama cross-validation, dan itulah pokok bahasan Chapter Cross-Validation.

Referensi Resmi #