Analisis kesalahan pada chapter Analisis Kesalahan Model mengajari kita membaca kelemahan satu model; sekarang kita menghadapi situasi yang lebih umum di proyek nyata: beberapa kandidat model menunggu untuk diputuskan. Tantangannya, perbandingan yang dijalankan asal — data uji berbeda untuk tiap kandidat, atau metrik yang berganti-ganti — bisa memahkotai pemenang yang keliru dengan percaya diri penuh. Perbandingan model yang adil pada dasarnya adalah eksperimen terkendali: hanya model yang boleh berbeda, sementara data, pembagian, dan cara menilainya kita kunci. Pada chapter ini kita susun aturan mainnya, mengadu tiga kandidat pada data churn Toko Kopi Kita, lalu membandingkan mereka dengan beberapa metrik sekaligus dan estimasi rata-rata beserta simpangan baku dari cross-validation.
Eksperimen Terkendali untuk Perbandingan Model #
Pertanyaan “model mana yang lebih baik?” hanya bermakna bila perbedaan skor bisa diatribusikan kepada modelnya, bukan kepada perbedaan kondisi eksperimen. Karena itu perbandingan yang adil mengikuti prinsip eksperimen terkendali: satu variabel berubah — modelnya — sementara sisanya kita kunci. Tabel berikut merinci lima elemen yang wajib identik untuk seluruh kandidat:
| Elemen yang dikunci | Mengapa wajib identik | Akibat bila berbeda |
|---|---|---|
| Baris data dan fitur | Perbedaan skor harus bisa diatribusikan ke model | Kandidat “menang” karena kebetulan menerima data yang lebih mudah |
| Pembagian latih-uji | Skor semua kandidat dihitung pada baris uji yang sama | Selisih skor tidak bermakna: kandidat mengerjakan soal yang berbeda |
| Pra-pemrosesan | Semua kandidat menerima input yang setara | Kandidat tertentu diuntungkan atau dirugikan oleh cara menyiapkan data |
| Metrik dan ambang | Metrik yang berbeda mengukur hal yang berbeda | Perbandingan menjadi apel dengan jeruk |
random_state | Hasil harus bisa direproduksi dan diaudit | Perbandingan tidak bisa diulang oleh orang lain |
Ketentuan ini bukan formalitas: selisih skor yang dihasilkan pada ujian yang berbeda tidak bisa ditafsirkan sebagai keunggulan. Chapter Memilih Model pada Section Dasar Machine Learning telah memperkenalkan perbandingan cepat tiga model dengan satu angka akurasi; dengan aturan main yang penuh di tangan, kita kini memperbaikinya — metriknya lebih dari satu, dan prosedurnya tidak bergantung pada satu undian pembagian.
Mengadu Tiga Kandidat pada Split yang Sama #
Kita kembali memakai data churn pelanggan Toko Kopi Kita: 60 pelanggan dengan fitur frekuensi kunjungan per bulan dan rata-rata belanja (ribu rupiah). Tiga kandidat mewakili tiga tingkat kompleksitas — regresi logistik, pohon keputusan yang dibatasi, dan random forest seratus pohon. Seluruh kandidat dilatih pada data latih yang sama, lalu dinilai pada data uji yang sama dengan empat metrik sekaligus:
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
rng = np.random.default_rng(42)
n = 60
kunjungan = rng.integers(1, 15, n) # frekuensi kunjungan per bulan
belanja = rng.integers(50, 350, n) # rata-rata belanja (ribu rupiah)
skor = 0.8 * (8 - kunjungan) + 0.02 * (belanja - 180) + rng.normal(0, 3, n)
churn = (skor > np.quantile(skor, 0.6)).astype(int)
X = np.column_stack([kunjungan, belanja])
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, churn, test_size=0.3, random_state=42, stratify=churn)
kandidat = {
"LogisticRegression": LogisticRegression(max_iter=1000),
"DecisionTree (max_depth=3)": DecisionTreeClassifier(max_depth=3, random_state=42),
"RandomForest (100 pohon)": RandomForestClassifier(n_estimators=100, random_state=42),
}
hasil = []
for nama, model in kandidat.items():
model.fit(X_latih, y_latih)
y_pred = model.predict(X_uji)
hasil.append({
"model": nama,
"akurasi": round(accuracy_score(y_uji, y_pred), 3),
"presisi": round(precision_score(y_uji, y_pred), 3),
"recall": round(recall_score(y_uji, y_pred), 3),
"f1": round(f1_score(y_uji, y_pred), 3),
})
print("data uji:", len(y_uji), "baris |", int(y_uji.sum()), "berhenti,",
int(len(y_uji) - y_uji.sum()), "lanjut")
print()
print(pd.DataFrame(hasil).to_string(index=False))Output yang Dihasilkan:
data uji: 18 baris | 7 berhenti, 11 lanjut
model akurasi presisi recall f1
LogisticRegression 0.889 0.857 0.857 0.857
DecisionTree (max_depth=3) 0.889 0.778 1.000 0.875
RandomForest (100 pohon) 0.833 0.750 0.857 0.800Mari kita telaah baris-baris kode di atas:
- Baris 11-14 (pembuatan data):
skormerangkum kecenderungan berhenti — pelanggan yang jarang datang condong berhenti, dan belanja bulanan yang besar menaikkan sedikit kecenderungan tersebut. Ambangnp.quantile(..., 0.6)menandai 24 pelanggan teratas sebagai churn. - Baris 17-18 (pembagian):
stratify=churnmenjaga proporsi kedua kelas di kedua sisi pembagian, danrandom_state=42mengunci undiannya — ketiga kandidat mengerjakan soal yang persis sama. - Baris 20-24 (kandidat): satu-satunya hal yang berbeda antar kandidat adalah modelnya, persis variabel eksperimen yang kita izinkan berubah; ketiganya memakai pengaturan standar tanpa tuning tambahan.
- Baris 27-36 (pengukuran): setiap kandidat dilatih, memprediksi data uji, lalu empat metrik dihitung dari prediksi pada
y_ujiyang sama dan dikumpulkan menjadi satu tabel.
Tabel hasilnya tidak memahkotai satu pemenang. LogisticRegression dan DecisionTree seri di akurasi (0,889), tetapi LogisticRegression unggul presisi (0,857) sementara DecisionTree unggul recall (1,000) dan f1 (0,875). RandomForest tidak memimpin di metrik mana pun: akurasi dan f1-nya di bawah dua kandidat lain, presisinya terendah, dan recallnya hanya menyamai LogisticRegression. Tiga kandidat, tiga kisah — dan kisah mana yang dipilih untuk diceritakan menentukan siapa yang tampak menang.
Bahaya Memilih Metrik Setelah Melihat Hasil #
Karena tidak ada pemenang mutlak, pilihan metrik yang dilaporkan menjadi sangat menentukan. Tabel berikut membaca hasil yang sama dari empat sudut pandang:
| Metrik yang dilaporkan | Kandidat yang tampak menang |
|---|---|
| Akurasi saja | LogisticRegression dan DecisionTree (seri 0,889) |
| Presisi saja | LogisticRegression (0,857) |
| Recall saja | DecisionTree (1,000 — tampak sempurna) |
| F1 saja | DecisionTree (0,875) |
Praktik memilih metrik setelah hasil keluar — lalu melaporkan yang paling menguntungkan kandidat favorit — dikenal sebagai cherry-picking, dan inilah cara termudah membalikkan kesimpulan tanpa memalsukan satu angka pun. Recall 1,000 milik DecisionTree juga perlu dibaca kritis: data uji hanya memuat tujuh pelanggan yang berhenti, sehingga “sempurna pada tujuh undian” bukan bukti keunggulan yang kokoh. Jalan yang jujur adalah menetapkan metrik utama sebelum eksperimen dimulai, berdasarkan biaya kesalahan — untuk churn, kegagalan mendeteksi pelanggan yang akan pergi (false negative) jauh lebih mahal daripada salah menandai pelanggan setia (false positive), sehingga recall wajar menjadi metrik penentu. Pembahasan lengkap pertimbangan ini ada pada chapter Precision, Recall, dan F1-Score .
Estimasi Stabil dengan cross_val_score #
Perbandingan pada satu split masih menyisakan kelemahan yang kita bahas pada chapter Cross-Validation
: satu pembagian adalah satu undian, dan skor kandidat mana pun bisa bergeser bila undiannya berganti. Perbandingan yang adil menuntut prosedur yang lebih kokoh untuk semua kandidat sekaligus: validasi silang lima lipatan pada data yang sama. Karena cv=5 memakai StratifiedKFold tanpa pengacakan, lipatan yang terbentuk identik untuk ketiga kandidat — setiap kandidat mengerjakan lima soal yang persis sama, lalu lima skornya kita ringkas menjadi rata-rata dan simpangan baku:
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
rng = np.random.default_rng(42)
n = 60
kunjungan = rng.integers(1, 15, n)
belanja = rng.integers(50, 350, n)
skor = 0.8 * (8 - kunjungan) + 0.02 * (belanja - 180) + rng.normal(0, 3, n)
churn = (skor > np.quantile(skor, 0.6)).astype(int)
X = np.column_stack([kunjungan, belanja])
kandidat = {
"LogisticRegression": LogisticRegression(max_iter=1000),
"DecisionTree (max_depth=3)": DecisionTreeClassifier(max_depth=3, random_state=42),
"RandomForest (100 pohon)": RandomForestClassifier(n_estimators=100, random_state=42),
}
baris = []
for nama, model in kandidat.items():
s = cross_val_score(model, X, churn, cv=5)
print(f"{nama:<26} {s.mean():.3f} ± {s.std():.3f} {np.round(s, 3).tolist()}")
baris.append({"model": nama, "rerata": round(s.mean(), 3),
"simpangan": round(s.std(), 3),
"lipatan_terendah": round(s.min(), 3)})
print()
print(pd.DataFrame(baris).sort_values("rerata", ascending=False).to_string(index=False))Output yang Dihasilkan:
LogisticRegression 0.850 ± 0.062 [0.833, 0.917, 0.917, 0.75, 0.833]
DecisionTree (max_depth=3) 0.767 ± 0.033 [0.833, 0.75, 0.75, 0.75, 0.75]
RandomForest (100 pohon) 0.783 ± 0.113 [0.917, 0.667, 0.917, 0.667, 0.75]
model rerata simpangan lipatan_terendah
LogisticRegression 0.850 0.062 0.750
RandomForest (100 pohon) 0.783 0.113 0.667
DecisionTree (max_depth=3) 0.767 0.033 0.750Mari kita telaah bagian-bagian pentingnya:
- Baris 24 (
cross_val_score(...)): satu panggilan mengembalikan lima skor validasi untuk satu kandidat;cv=5pada masalah klasifikasi otomatis memakai lipatan ter-stratifikasi yang sama bagi semua kandidat karena datanya identik. - Baris 25 (cetak rata-rata ± simpangan): rata-rata menjadi estimasi performa, simpangan baku menjadi ukuran ketidakpastiannya, dan daftar lima skor pada baris yang sama memperlihatkan rentang nyatanya.
- Baris 26-28 (kumpulkan ringkasan): selain rata-rata dan simpangan, kita mencatat lipatan terendah — skor terburuk yang pernah terjadi pada lima undian tersebut.
- Baris 31 (tabel ringkasan):
sort_values("rerata", ascending=False)menyusun tabel laporan dari rata-rata tertinggi ke terendah.
Cara membaca tabel ringkasan sama pentingnya dengan cara membuatnya. Rata-rata LogisticRegression memang tertinggi (0,850), tetapi selisihnya terhadap RandomForest hanya 0,067 — lebih kecil daripada simpangan RandomForest sendiri (0,113) — sehingga rentang kedua kandidat saling tumpang tindih dan keunggulan itu belum meyakinkan. RandomForest juga terbukti paling tidak stabil: skornya melompat dari 0,667 sampai 0,917 antar lipatan, sementara DecisionTree paling konsisten (simpangan 0,033) tetapi rata-ratanya terendah (0,767). Ingat pula recall 1,000 DecisionTree pada satu split tadi: begitu undian digilirkan, kandidat yang sama justru mencetak rata-rata terendah; satu undian yang beruntung mampu menyamarkan kandidat yang lemah.
Rangkuman #
- Perbandingan model yang adil adalah eksperimen terkendali: kunci data, pembagian, pra-pemrosesan, metrik, dan
random_state; hanya model yang berbeda. - Satu metrik tidak pernah cukup: pada kasus kita, pemenang bergantung metrik — LogisticRegression pada presisi, DecisionTree pada recall dan f1, seri di akurasi.
- Metrik utama ditetapkan sebelum eksperimen berdasarkan biaya false positive dan false negative; memilihnya setelah hasil keluar adalah cherry-picking.
cross_val_scorememberi rata-rata ± simpangan baku pada lipatan yang sama untuk semua kandidat; tabel ringkasan (rata-rata, simpangan, lipatan terendah) menampilkan performa sekaligus stabilitas.- Selisih rata-rata yang lebih kecil daripada simpangan bukan bukti keunggulan; keputusan akhir memerlukan kriteria di luar angka performa.
Tabel perbandingan yang jujur kini tersedia, tetapi tabel belum memutuskan apa pun: LogisticRegression unggul tipis dan belum meyakinkan, RandomForest bervariasi paling lebar, dan DecisionTree sempurna hanya pada satu undian. Memilih satu model untuk dipakai di lapangan menuntut penimbangan performa, kesederhanaan, biaya, dan risiko secara berlapis — kerangka itulah yang kita susun pada chapter penutup Section ini, Memilih Model Terbaik .