Holdout memberi satu angka dari satu pembagian data, dan Chapter sebelumnya menunjukkan betapa rapuh angka tunggal itu: pergantian seed saja menggeser akurasi hampir 7 poin. Cross-validation (CV) menjawab kelemahan tersebut dengan memakai seluruh data secara bergiliran sebagai data ujian. Chapter ini membedah mekanika K-fold, alasan StratifiedKFold menjadi pilihan baku untuk klasifikasi, perbedaan cross_val_score dan cross_validate, serta cara membaca variansi antar-fold sebagai sinyal kestabilan model. Kita juga akan melihat kasus khusus data bergrup, misalnya data pasien, di mana CV biasa justru membocorkan informasi.

Mekanika K-Fold Cross-Validation #

Ide K-fold sederhana: bagi data menjadi K bagian (fold) yang sama besar, lalu lakukan K putaran pelatihan. Pada tiap putaran, satu fold berperan sebagai data validasi dan K−1 fold sisanya sebagai data latih. Setelah K putaran, setiap sampel telah menjadi anggota data validasi tepat satu kali, dan kita memperoleh K skor. Rata-ratanya menjadi estimasi performa yang jauh lebih stabil daripada holdout karena tiap sampel menyumbang skor persis sekali; simpangan bakunya memberi gambaran seberapa bervariasi performa antar pembagian.

Lima fold adalah standar pragmatis: cukup banyak untuk estimasi stabil, cukup murah untuk dijalankan berulang saat menyetel hyperparameter. Estimasi rata-rata dari K fold juga “miskin bias” karena tiap model mempelajari 4/5 data — hampir seluruh data yang tersedia.

StratifiedKFold untuk Klasifikasi #

Pada masalah klasifikasi, KFold biasa bisa membagi kelas secara timpang: satu fold mungkin kekurangan sampel kelas langka, sehingga skornya menyesatkan. StratifiedKFold membagi data dengan tetap menjaga proporsi kelas di setiap fold — kelanjutan logika stratify=y pada train_test_split. Perlu dicatat: fungsi cross_val_score dan cross_validate sebenarnya sudah memakai stratifikasi bawaan ketika estimator-nya adalah classifier dan targetnya berupa label kelas; menetapkan cv=StratifiedKFold(...) secara eksplisit seperti pada contoh kita membuat perilaku tersebut terlihat dan terkunci, bukan sekadar bergantung pada bawaan.

Lima Skor, Satu Ringkasan #

Mari kita jalankan 5-fold CV pada LogisticRegression dengan data 120 sampel, lalu rangkum hasilnya secara manual dari array skor fold.

python
# 5-fold cross-validation: kelima fold dipakai bergiliran sebagai ujian,
# lalu skor dirangkum dengan rata-rata dan simpangan baku.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score, cross_validate

# Data sintetis tetap: 120 sampel, proporsi kelas seimbang 60:60.
X, y = make_classification(
    n_samples=120,
    n_features=8,
    n_informative=6,
    n_redundant=1,
    flip_y=0.05,
    random_state=42,
)

# StratifiedKFold menjaga proporsi kelas di tiap fold.
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
model = LogisticRegression(max_iter=1000, random_state=42)

# --- cross_val_score: satu metrik, satu array skor fold ---
skor_fold = cross_val_score(model, X, y, cv=cv, scoring="accuracy")
print("Skor tiap fold  :", " ".join(f"{s:.3f}" for s in skor_fold))

# Rata-rata dan std dihitung manual dari array fold -- tidak ada angka lain.
rata2 = skor_fold.mean()
std = skor_fold.std()
print(f"Rata-rata        : {rata2:.3f}")
print(f"Simpangan baku   : {std:.3f}")
print(f"Ringkasan        : {rata2:.3f} +- {std:.3f}")

# --- cross_validate: beberapa metrik sekaligus ---
print()
hasil = cross_validate(model, X, y, cv=cv, scoring=["accuracy", "recall"])
print("Akurasi per fold :", " ".join(f"{s:.3f}" for s in hasil["test_accuracy"]))
print("Recall per fold  :", " ".join(f"{s:.3f}" for s in hasil["test_recall"]))

# --- Setelah model terpilih: latih ulang pada SELURUH data ---
model.fit(X, y)
print()
print("Model final dilatih ulang pada seluruh 120 sampel data.")

Output yang Dihasilkan:

text
Skor tiap fold  : 0.875 0.583 0.750 1.000 0.792
Rata-rata        : 0.800
Simpangan baku   : 0.138
Ringkasan        : 0.800 +- 0.138

Akurasi per fold : 0.875 0.583 0.750 1.000 0.792
Recall per fold  : 0.833 0.667 0.583 1.000 0.692

Model final dilatih ulang pada seluruh 120 sampel data.

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 20 (cv = StratifiedKFold(...)): parameter shuffle=True, random_state=42 mengunci pengacakan sebelum pembelahan fold sehingga kelima fold selalu sama antar eksekusi.
  2. Baris 24 (cross_val_score): fungsi ini menjalankan seluruh siklus latih-then-ukur sebanyak 5 kali dan mengembalikan array berisi 5 skor — bukan satu angka. Estimator model tidak berubah; setiap putaran memakai salinan internal yang baru dilatih.
  3. Baris 27–29 (rata2, std): ringkasan dihitung langsung dari array fold, memperlihatkan bahwa rata-rata dan simpangan baku bukan angka terpisah melainkan turunan dari kelima skor yang dicetak di atasnya.
  4. Baris 36 (cross_validate): berbeda dari cross_val_score, fungsi ini menerima daftar metrik dan mengembalikan dictionary berisi array per metrik (test_accuracy, test_recall), plus durasi fit dan score tiap putaran yang bisa dipakai untuk analisis biaya komputasi.
  5. Baris 41 (model.fit(X, y)): CV hanya untuk menilai; setelah keputusan diambil, model produksi dilatih ulang pada seluruh data agar tidak ada sampel terbuang.

Output pertama memperlihatkan kekuatan sekaligus pesan penting CV. Skor fold berkisar dari 0.583 sampai 1.000 — rentang yang jauh lebih lebar daripada selisih dua seed holdout pada chapter sebelumnya. Rata-ratanya 0.800 dengan simpangan baku 0.138. Jika kita hanya melihat satu fold (misalnya fold keempat yang sempurna), kesimpulan kita akan terlalu optimistis; fold kedua akan membuat kita terlalu pesimistis. Rata-rata lima skor adalah gambaran yang lebih bisa dipercaya, dan simpangan baku 0.138 adalah sinyal bahwa model ini sensitif terhadap data yang dipakai melatihnya.

cross_val_score vs cross_validate #

Aspekcross_val_scorecross_validate
Jumlah metrikSatu per pemanggilanBanyak sekaligus (daftar)
Bentuk hasilArray skor tunggalDictionary array per metrik
Informasi waktuTidak adaDurasi fit dan score per putaran
Skor data latihTidak tersediaTersedia lewat return_train_score=True

Untuk eksplorasi cepat satu angka, cross_val_score memadai. Untuk keputusan serius — membandingkan kandidat model — cross_validate lebih tepat karena satu eksekusi menghasilkan beberapa sudut pandang sekaligus; pada contoh kita, recall per fold (0.833 hingga 0.583) menunjukkan ketahanan kelas positif yang tidak terlihat dari akurasi saja.

Varian CV yang Perlu Dikenal #

Dua varian lain layak disebut sebelum kita lanjut. Pertama, leave-one-out CV (LOOCV) adalah K-fold ekstrem dengan K = jumlah sampel: tiap putaran menguji tepat satu sampel. LOOCV memakai hampir seluruh data untuk melatih (bias rendah), tetapi model-modelnya bertetangga sangat mirip sehingga skornya berkorelasi tinggi dan estimasinya bervariasi — ditambah biaya komputasi yang berat pada data besar. Kedua, GroupKFold untuk data yang datang dalam grup: bila satu pasien menyumbang banyak sampel, semua sampel pasien tersebut harus berada di fold yang sama, sebab sampel dari satu sumber membawa informasi yang sama ke latih dan validasi — bentuk kebocoran halus yang membuat skor terlihat lebih baik dari kenyataannya. StratifiedKFold tidak menangani struktur grup; untuk itu gunakan GroupKFold.

Estimasi performa yang stabil hanyalah separuh pekerjaan evaluasi; separuh lainnya adalah tahu apa yang diukur. Akurasi menggabungkan semua jenis galat ke satu angka, padahal jenis galat yang berbeda berharga sangat berbeda di dunia nyata. Alat pertama untuk membedahnya adalah confusion matrix, pokok bahasan Chapter Confusion Matrix.

Referensi Resmi #