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 dikunciMengapa wajib identikAkibat bila berbeda
Baris data dan fiturPerbedaan skor harus bisa diatribusikan ke modelKandidat “menang” karena kebetulan menerima data yang lebih mudah
Pembagian latih-ujiSkor semua kandidat dihitung pada baris uji yang samaSelisih skor tidak bermakna: kandidat mengerjakan soal yang berbeda
Pra-pemrosesanSemua kandidat menerima input yang setaraKandidat tertentu diuntungkan atau dirugikan oleh cara menyiapkan data
Metrik dan ambangMetrik yang berbeda mengukur hal yang berbedaPerbandingan menjadi apel dengan jeruk
random_stateHasil harus bisa direproduksi dan diauditPerbandingan 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:

python
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:

text
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.800

Mari kita telaah baris-baris kode di atas:

  1. Baris 11-14 (pembuatan data): skor merangkum kecenderungan berhenti — pelanggan yang jarang datang condong berhenti, dan belanja bulanan yang besar menaikkan sedikit kecenderungan tersebut. Ambang np.quantile(..., 0.6) menandai 24 pelanggan teratas sebagai churn.
  2. Baris 17-18 (pembagian): stratify=churn menjaga proporsi kedua kelas di kedua sisi pembagian, dan random_state=42 mengunci undiannya — ketiga kandidat mengerjakan soal yang persis sama.
  3. 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.
  4. Baris 27-36 (pengukuran): setiap kandidat dilatih, memprediksi data uji, lalu empat metrik dihitung dari prediksi pada y_uji yang 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 dilaporkanKandidat yang tampak menang
Akurasi sajaLogisticRegression dan DecisionTree (seri 0,889)
Presisi sajaLogisticRegression (0,857)
Recall sajaDecisionTree (1,000 — tampak sempurna)
F1 sajaDecisionTree (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:

python
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:

text
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.750

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 24 (cross_val_score(...)): satu panggilan mengembalikan lima skor validasi untuk satu kandidat; cv=5 pada masalah klasifikasi otomatis memakai lipatan ter-stratifikasi yang sama bagi semua kandidat karena datanya identik.
  2. 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.
  3. Baris 26-28 (kumpulkan ringkasan): selain rata-rata dan simpangan, kita mencatat lipatan terendah — skor terburuk yang pernah terjadi pada lima undian tersebut.
  4. 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_score memberi 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 .

Referensi #

  1. scikit-learn — Metrics and scoring: model evaluation
  2. scikit-learn — Model selection: cross-validation
  3. scikit-learn — train_test_split