Evaluasi yang Jujur

Strategi CV, holdout akhir, dan narasi metrik.

Daftar isi

Model tertala pada tahap pembangunan kini wajib melewati ujian terakhirnya sebelum angkanya boleh dikutip. Topik ini menuntaskan proyek dengan evaluasi yang jujur, yaitu melaporkan skor cross-validation sebagai estimasi, menyentuh test set tepat satu kali untuk angka final, membaca kesalahan prediksi dalam bahasa biaya pada ranah diagnosis, menempatkan hasil terhadap baseline, serta menyatakan batas klaimnya secara eksplisit. Bagian akhir merangkum seluruh artefak yang dihasilkan proyek dari data hingga skor.

Memuat model terbaik dan estimasinya#

Model final dibangun dari konfigurasi hasil penalaan, yaitu Pipeline berisi ColumnTransformer dengan StandardScaler dan LogisticRegression(max_iter=1000, C=1.0). Objek ini dilatih pada seluruh 426 baris data train untuk memanfaatkan semua data yang diizinkan, kemudian disimpan dengan joblib ke berkas /tmp/model-proyek.joblib dan dimuat kembali sebagai model_akhir. Prediksi model yang dimuat identik dengan model yang disimpan, sebuah pemeriksaan kecil yang membuktikan artefaknya utuh.

Skor CV 0.956 ± 0.025 pada keluaran skrip berikut dihitung ulang dengan protokol yang sama seperti tahap penalaan, yaitu StratifiedKFold lima lipatan pada train dengan scorer recall ganas. Angka ini adalah prediksi terbaik atas recall ganas pada data serupa yang belum pernah dilihat model, lengkap dengan ketidakpastiannya berupa simpangan baku 0.025. Test set belum menyumbang satu bit informasi apa pun hingga titik ini, dan menjaga keadaan itu sampai akhir adalah inti dari evaluasi yang jujur.

Menyentuh test set satu kali#

Satu panggilan evaluasi pada test set seharusnya menjawab semua pertanyaan final sekaligus. Skrip berikut menilai model yang dimuat pada 143 baris test dengan sekali jalan, yaitu menghitung akurasi lewat score, confusion matrix (tabel silang antara kelas sebenarnya dan kelas prediksi) lewat confusion_matrix, laporan klasifikasi lengkap lewat classification_report, serta ROC AUC (Area Under the ROC Curve, ukuran kualitas pemisahan dua kelas yang tidak bergantung pada ambang) lewat roc_auc_score. Peluang kelas ganas diambil dari kolom 0 pada predict_proba, dan pernyataan y_test == 0 menetapkan ganas sebagai kelas positif untuk AUC.

python evaluasi-akhir.py
import numpy as np
import pandas as pd
import joblib
from sklearn.datasets import load_breast_cancer
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.metrics import (confusion_matrix, classification_report, roc_auc_score,
                             recall_score, make_scorer)

cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = cancer.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

# konfigurasi final hasil penalaan pada tahap pembangunan pipeline
praproses = ColumnTransformer([
    ("num", StandardScaler(), make_column_selector(dtype_include=np.number)),
])
pipeline = Pipeline([
    ("prep", praproses),
    ("clf", LogisticRegression(max_iter=1000, C=1.0)),
])

# estimasi performa dari CV pada train, sebelum test disentuh
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
recall_ganas = make_scorer(recall_score, pos_label=0)
skor_cv = cross_val_score(pipeline, X_train, y_train, cv=cv, scoring=recall_ganas)
print(f"Estimasi CV pada train : {skor_cv.mean():.3f} ± {skor_cv.std():.3f}")

# fit pada seluruh train, simpan artefaknya, lalu muat kembali
pipeline.fit(X_train, y_train)
joblib.dump(pipeline, "/tmp/model-proyek.joblib")
model_akhir = joblib.load("/tmp/model-proyek.joblib")

# satu-satunya penyentuhan test set pada seluruh proyek
akurasi = model_akhir.score(X_test, y_test)
pred = model_akhir.predict(X_test)
proba = model_akhir.predict_proba(X_test)
print(f"\nAkurasi test (disentuh sekali): {akurasi:.3f}")
print("Confusion matrix (baris = kelas sebenarnya):")
print(confusion_matrix(y_test, pred, labels=[0, 1]))
print("\n" + classification_report(y_test, pred, target_names=["ganas", "jinak"], digits=3))
# peluang ganas berada di kolom 0, dan y_test == 0 menetapkan ganas sebagai positif
auc = roc_auc_score(y_test == 0, proba[:, 0])
print(f"ROC AUC (ganas sebagai kelas positif): {auc:.3f}")
text Keluaran
Estimasi CV pada train : 0.956 ± 0.025

Akurasi test (disentuh sekali): 0.986
Confusion matrix (baris = kelas sebenarnya):
[[52  1]
 [ 1 89]]

              precision    recall  f1-score   support

       ganas      0.981     0.981     0.981        53
       jinak      0.989     0.989     0.989        90

    accuracy                          0.986       143
   macro avg      0.985     0.985     0.985       143
weighted avg      0.986     0.986     0.986       143

ROC AUC (ganas sebagai kelas positif): 0.998

Isi confusion matrix pada keluaran terbaca jelas. Dari 53 tumor ganas pada test, 52 dikenali dan 1 lolos sebagai jinak, yaitu satu FN dengan recall ganas 0.981. Dari 90 tumor jinak, 89 dinyatakan benar dan 1 menjadi alarm palsu, yaitu satu FP dengan precision ganas 0.981. Akurasi 0.986 dan ROC AUC 0.998 melengkapi gambaran, sedangkan estimasi CV 0.956 ternyata konservatif terhadap recall test 0.981. Selisih semacam ini wajar karena test hanya berisi 143 baris, sehingga satu baris saja sudah menggeser recall sekitar 0.02.

FP dan FN pada ranah diagnosis#

Dua sel di luar diagonal confusion matrix memikul makna yang sangat berbeda pada ranah diagnosis. FP berarti tumor jinak dinyatakan ganas, yang berujung pada pemeriksaan lanjutan, waktu, biaya, dan kecemasan yang tidak perlu. FN berarti tumor ganas dinyatakan jinak, yang berarti tumor lolos dari perhatian dan perlakuannya tertunda. Karena konsekuensi FN jauh lebih berat, recall kelas ganas tetap metrik yang diprioritaskan pada pembacaan akhir ini, dan satu FN pada 143 baris test menjadi angka yang paling patut ditelusuri lebih lanjut.

Precision ganas tidak diabaikan meski recall diprioritaskan. Model yang menandai hampir semua pasien sebagai ganas akan mencetak recall tinggi dengan alarm palsu yang membuat penunjangnya tak berguna, dan angka precision 0.981 menunjukkan alarm palsu masih sangat jarang. Pertukaran kedua metrik ini dapat dikendalikan lewat ambang keputusan, yaitu batas peluang yang memisahkan prediksi ganas dari prediksi jinak (threshold). Nilai default scikit-learn adalah 0.5, dan nilai itu bukan ketentuan yang mengikat.

python Menggeser ambang keputusan pada data train
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_predict, StratifiedKFold
from sklearn.metrics import recall_score, precision_score

cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = cancer.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

praproses = ColumnTransformer([
    ("num", StandardScaler(), make_column_selector(dtype_include=np.number)),
])
pipeline = Pipeline([
    ("prep", praproses),
    ("clf", LogisticRegression(max_iter=1000, C=1.0)),
])

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# tiap baris train dinilai model yang tidak dilatih pada baris tersebut
proba = cross_val_predict(pipeline, X_train, y_train, cv=cv, method="predict_proba")
p_ganas = proba[:, 0]

# geser ambang: menebak jinak bila peluang ganas berada di bawah ambang
for ambang in (0.5, 0.3):
    pred = (p_ganas < ambang).astype(int)
    recall = recall_score(y_train, pred, pos_label=0)
    presisi = precision_score(y_train, pred, pos_label=0, zero_division=0)
    print(f"ambang {ambang:.1f}: recall ganas = {recall:.3f}, precision ganas = {presisi:.3f}")
text Keluaran
ambang 0.5: recall ganas = 0.956, precision ganas = 0.987
ambang 0.3: recall ganas = 0.981, precision ganas = 0.945

Eksperimen ambang dijalankan pada data train melalui cross_val_predict, sehingga test set yang telah disentuh sekali tidak diganggu lagi. Menurunkan ambang dari 0.5 ke 0.3 membuat model lebih berani menebak ganas, sehingga recall pada train naik dari 0.956 ke 0.981 dengan harga precision turun dari 0.987 ke 0.945. Pilihan ambang dengan demikian merupakan keputusan tentang biaya keliru, bukan angka yang ditentukan kode secara otomatis. Pada proyek ini ambang default 0.5 dipertahankan, sementara mekanisme penggeserannya tercatat sebagai alat yang siap digunakan apabila biaya FN dinilai lebih tinggi.

Baseline, batas klaim, dan artefak proyek#

Angka final baru bermakna setelah ditempatkan terhadap baseline dari awal proyek. Baseline DummyClassifier mencetak akurasi 0.627 dengan recall ganas 0.000, artinya gagal mengenali seluruh 212 tumor ganas yang ada pada data. Model akhir mencetak akurasi 0.986 dan recall ganas 0.981 pada test, dengan 52 dari 53 tumor ganas dikenali. Lonjakan recall dari 0.000 ke 0.981 adalah pencapaian inti proyek, sedangkan lonjakan akurasinya menyusul di belakangnya.

PembandingAkurasiRecall kelas ganas
Baseline DummyClassifier (CV lima lipatan)0.6270.000
Model akhir LogisticRegression (test, sekali)0.9860.981

Batas klaimnya perlu dinyatakan sejelas angkanya. Skor 0.986 berlaku untuk distribusi data yang sama dengan data pelatihan, yaitu 569 sampel pengukuran citra aspirasi jarum halus dari satu sumber yang sama. Angka ini bukan jaminan kinerja pada populasi pasien yang berbeda, peralatan pencitraan yang berbeda, atau ketidakseimbangan kelas yang jauh lebih ekstrem, dan model ini tetap alat penunjang keputusan yang tidak menggantikan diagnosis tenaga kesehatan. Yang dapat dipertanggungjawabkan dari proyek ini adalah prosesnya, yaitu audit sejak awal, baseline yang wajib dikalahkan, protokol CV yang konsisten, dan test set yang disentuh sekali.

Ringkasan#

  • Artefak data: load_breast_cancer dengan 569 baris dan 30 fitur, teraudit tanpa nilai NaN dan dengan distribusi 212 ganas berbanding 357 jinak.
  • Artefak model: Pipeline berisi ColumnTransformer dengan StandardScaler dan LogisticRegression(max_iter=1000, C=1.0), tersimpan pada /tmp/model-proyek.joblib.
  • Skor proyek: recall ganas CV 0.956 ± 0.025 sebagai estimasi, lalu test sekali dengan akurasi 0.986, recall ganas 0.981, dan ROC AUC 0.998.
  • Baseline DummyClassifier dengan recall ganas 0.000 terkalahkan jauh, dan batas klaim pada distribusi data yang sama dinyatakan eksplisit.

Referensi#