Sepanjang Section ini kita mengukur performa model dengan satu angka dari satu pembagian data: pisahkan data uji, latih model, lalu hitung metriknya pada data uji tersebut. Masalahnya, angka itu ikut berganti setiap kali pembagian berganti — model yang sama bisa tampak bagus pada satu pembagian dan mengecewakan pada pembagian lain. Cross-validation menjawab kerapuhan itu dengan memutar peran data: setiap baris mendapat giliran menjadi data validasi, lalu seluruh skor dirangkum menjadi satu estimasi yang jauh lebih stabil. Pada chapter ini kita bedah cara kerja k-fold cross-validation, mempraktikkannya lewat cross_val_score, lalu membahas pemilihan nilai k.
Satu Pembagian Menghasilkan Satu Angka yang Rapuh #
Ingat kembali cara kerja train_test_split: fungsi tersebut memilih sebagian baris secara acak sebagai data uji, sisanya menjadi data latih. Bagian mana yang terpilih ditentukan oleh random_state — ganti seed, berganti pula susunan data uji, dan berganti pula skor yang kita laporkan. Pada data besar efek ini mengecil, tetapi pada dataset kecil (puluhan sampai ratusan baris) satu-dua baris yang pindah kelompok sudah cukup menggeser akurasi bermakna. Mari kita buktikan dengan data churn pelanggan Toko Kopi Kita: 60 pelanggan dengan fitur lama keanggotaan (bulan) dan belanja bulanan (ribu rupiah).
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
rng = np.random.default_rng(42)
n = 60
lama_member = rng.integers(1, 36, n)
belanja_bulanan = rng.integers(50, 500, n)
skor_churn = 0.08 * (36 - lama_member) + 0.03 * belanja_bulanan + rng.normal(0, 4, n)
churn = (skor_churn > np.quantile(skor_churn, 0.65)).astype(int)
X = np.column_stack([lama_member, belanja_bulanan])
print("jumlah pelanggan:", n, "| berhenti:", churn.sum(), "| lanjut:", n - churn.sum())
hasil = []
for seed in range(5):
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, churn, test_size=0.25, random_state=seed)
model = LogisticRegression().fit(X_latih, y_latih)
hasil.append(model.score(X_uji, y_uji))
print("akurasi per seed :", np.round(hasil, 3).tolist())
print("selisih terbesar :", round(max(hasil) - min(hasil), 3))Output yang Dihasilkan:
jumlah pelanggan: 60 | berhenti: 21 | lanjut: 39
akurasi per seed : [0.933, 0.933, 0.667, 0.8, 0.933]
selisih terbesar : 0.267Mari kita telaah baris-baris kode di atas:
- Baris 6-9 (pembuatan data):
rng.integersmenghasilkan dua fitur, laluskor_churnmenggabungkannya menjadi kecenderungan berhenti — pelanggan baru dengan belanja besar condong berhenti. Ambangnp.quantile(..., 0.65)menandai 35% teratas sebagai churn, mendekati proporsi churn nyata yang biasanya minoritas. - Baris 13 (
for seed in range(5)): kita mengulang eksperimen yang sama lima kali; satu-satunya yang berbeda adalahrandom_statepadatrain_test_split. Model, data, dan metrik tidak berubah. - Baris 17 (
model.score(...)):score()pada classifier mengembalikan akurasi di data uji. Nilainya kita kumpulkan ke listhasil.
Hasilnya tegas: model yang identik mencetak akurasi 0,933 pada tiga pembagian, tetapi anjlok ke 0,667 pada pembagian ketiga — selisih 0,267. Jika kita kebetulan memakai seed ketiga dan melaporkan 66,7% sebagai performa model, keputusan yang menyusul (misalnya menolak model ini) jadi keliru. Kita butuh mekanisme yang tidak menyerahkan nasib estimasi pada satu undian pembagian.
Cara Kerja k-Fold Cross-Validation #
k-fold cross-validation membagi data menjadi k bagian sama besar yang disebut fold atau lipatan. Eksperimen diulang k putaran: pada setiap putaran, satu lipatan berperan sebagai data validasi dan sisanya (k-1 lipatan) sebagai data latih. Setelah k putaran, setiap baris telah menjadi data validasi tepat satu kali, sehingga kita memperoleh k skor — bukan satu. Ilustrasi paling jelas datang dari KFold.split() pada sepuluh baris:
import numpy as np
from sklearn.model_selection import KFold
kf = KFold(n_splits=5)
baris_data = np.arange(10)
for i, (idx_latih, idx_validasi) in enumerate(kf.split(baris_data.reshape(-1, 1)), start=1):
print(f"lipatan {i}: validasi={idx_validasi.tolist()}"
f" latih={idx_latih.tolist()}")Output yang Dihasilkan:
lipatan 1: validasi=[0, 1] latih=[2, 3, 4, 5, 6, 7, 8, 9]
lipatan 2: validasi=[2, 3] latih=[0, 1, 4, 5, 6, 7, 8, 9]
lipatan 3: validasi=[4, 5] latih=[0, 1, 2, 3, 6, 7, 8, 9]
lipatan 4: validasi=[6, 7] latih=[0, 1, 2, 3, 4, 5, 8, 9]
lipatan 5: validasi=[8, 9] latih=[0, 1, 2, 3, 4, 5, 6, 7]Tiga baris kode ini memperlihatkan seluruh mekanismenya: KFold(n_splits=5) menyiapkan lima lipatan, split() menghasilkan pasangan indeks (latih, validasi) per putaran, dan setiap baris 0-9 muncul di sisi validasi tepat satu kali. Pada data nyata, model dilatih dari awal pada setiap putaran — inilah sebabnya cross-validation melatih model sebanyak k kali. Skor validasi dari kelima putaran kemudian dirangkum, biasanya dengan rata-rata sebagai estimasi performa dan simpangan baku sebagai ukuran ketidakpastiannya.
Menghitung Skor per Lipatan dengan cross_val_score #
Kita tidak perlu menulis perulangan manual di atas setiap kali butuh validasi silang. Fungsi cross_val_score() merangkum seluruh proses — membagi lipatan, melatih, menilai — dan mengembalikan array berisi k skor. Kita terapkan pada data churn yang sama:
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
rng = np.random.default_rng(42)
n = 60
lama_member = rng.integers(1, 36, n)
belanja_bulanan = rng.integers(50, 500, n)
skor_churn = 0.08 * (36 - lama_member) + 0.03 * belanja_bulanan + rng.normal(0, 4, n)
churn = (skor_churn > np.quantile(skor_churn, 0.65)).astype(int)
X = np.column_stack([lama_member, belanja_bulanan])
skor_lipatan = cross_val_score(LogisticRegression(), X, churn, cv=5)
tabel = pd.DataFrame({
"lipatan": range(1, 6),
"skor_validasi": skor_lipatan.round(3),
})
print(tabel.to_string(index=False))
print()
print("rata-rata :", round(skor_lipatan.mean(), 3))
print("simpangan baku :", round(skor_lipatan.std(), 3))Output yang Dihasilkan:
lipatan skor_validasi
1 0.917
2 0.750
3 0.917
4 0.750
5 0.750
rata-rata : 0.817
simpangan baku : 0.082Mari kita telaah bagian-bagian pentingnya:
- Baris 13 (
cross_val_score(...)): tiga argumen utamanya adalah model (belum dilatih), fiturX, dan targetchurn. Parametercv=5meminta lima lipatan; untuk masalah klasifikasi, scikit-learn otomatis memakaiStratifiedKFoldagar proporsi kelas tiap lipatan terjaga — topik yang kita bedah penuh pada chapter berikutnya. - Baris 15-18 (
pd.DataFrame): array skor diubah menjadi tabel satu baris per lipatan supaya mudah dipindai;round(3)menjaga tampilan tetap ringkas. - Baris 21-22 (rangkuman): rata-rata 0,817 menjadi estimasi performa, dan simpangan baku 0,082 memberi tahu seberapa jauh skor bisa bergeser antar lipatan.
Bandingkan dengan hasil sebelumnya: lima pembagian acak menghasilkan akurasi 0,667-0,933 tanpa ada petunjuk angka mana yang benar, sedangkan cross-validation memberi satu estimasi rata-rata (0,817) beserta ukuran ketidakpastiannya. Rata-rata lima lipatan menempatkan diri di tengah rentang tersebut — persis perilaku yang kita harapkan dari estimasi yang lebih stabil. Inilah alasan cross-validation menjadi standar saat membandingkan model atau melaporkan performa pada dataset kecil.
Memilih Nilai k #
Berapa lipatan yang sebaiknya dipakai? Pilihan k adalah trade-off antara bias dan variansi estimasi, plus biaya komputasi:
| Nilai k | Data latih per putaran | Kecenderungan bias | Kecenderungan variansi | Biaya |
|---|---|---|---|---|
| k=2 | 50% | Skor cenderung pesimistik (data latih sedikit) | Rendah | 2x pelatihan |
| k=5 | 80% | Ringan | Moderat | 5x pelatihan |
| k=10 | 90% | Hampir netral | Agak tinggi | 10x pelatihan |
| k=n (LOOCV) | (n-1)/n data | Paling netral | Tinggi (lipatan tumpang tindih) | n x pelatihan |
Dua ujung tabel ini menjelaskan trade-off-nya. Nilai k kecil membuat tiap model belajar dari setengah data saja, sehingga skor cenderung merendahkan kemampuan sesungguhnya — bias pesimistik. Nilai k=n (leave-one-out cross-validation, LOOCV) membuat tiap model belajar dari hampir seluruh data, tetapi n model yang saling tumpang tindih menghasilkan skor-skor yang berkorelasi kuat; rata-ratanya gampang bergeser mengikuti data (variansi tinggi), dan biayanya melonjak karena kita melatih n model. Nilai k=5 atau k=10 berada di titik tengah yang lazim di praktik dan direkomendasikan literatur; scikit-learn memilih k=5 sebagai bawaannya.
Rangkuman #
- Skor dari satu pembagian
train_test_splitbergantung padarandom_state; pada data 60 baris kita menyaksikan akurasi bergerak 0,667-0,933 untuk model yang sama. - k-fold cross-validation membagi data menjadi
klipatan, menggilirkan peran validasi, dan menghasilkankskor yang dirangkum menjadi rata-rata plus simpangan baku. cross_val_score(model, X, y, cv=5)merangkum seluruh proses tersebut; untuk klasifikasi, pembagian lipatan otomatis memakaiStratifiedKFold.- Pemilihan
kadalah trade-off:kkecil condong pesimistik,k=n(LOOCV) bervarian tinggi dan mahal;k=5atauk=10adalah pilihan standar. - Skor cross-validation mengestimasi prosedur pelatihan, bukan performa final model di lapangan.
Estimasi yang stabil kini kita miliki, tetapi masih ada satu kelemahan senyap pada semua pembagian yang telah kita lakukan: keberuntungan bisa membuat kelas minoritas tercecer — bahkan kosong — pada data uji atau lipatan tertentu. Chapter berikutnya, Stratified Sampling, memastikan proporsi kelas tetap terjaga di setiap pembagian.