Melatih model itu mudah; menilai model dengan jujur itu sulit. Angka akurasi 95 persen bisa berarti model brilian atau model yang tidak pernah memprediksi kelas penting sama sekali. Pada chapter ini kita membedah metrik evaluasi yang benar untuk classification dan regression, membuat evaluasi lebih stabil dengan cross-validation, mencari hyperparameter dengan GridSearchCV, dan menegakkan satu aturan emas: model tidak boleh dievaluasi pada data yang dipakainya belajar.
Ketika Akurasi Berbohong #
Bayangkan sistem deteksi fraud di mana hanya 10 persen transaksi adalah penipuan. Model yang menebak “bukan penipuan” untuk seluruh transaksi langsung meraih akurasi 90 persen tanpa menangkap satu penipuan pun. Kita buktikan dengan data sintetis tak seimbang dan DummyClassifier yang selalu menebak kelas mayoritas:
from sklearn.datasets import make_classification
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import train_test_split
X_imb, y_imb = make_classification(
n_samples=200, n_features=4, n_informative=3, n_redundant=0,
weights=[0.9, 0.1], flip_y=0, class_sep=1.5, random_state=42,
)
X_latih, X_uji, y_latih, y_uji = train_test_split(
X_imb, y_imb, test_size=0.3, random_state=42, stratify=y_imb
)
naif = DummyClassifier(strategy="most_frequent")
naif.fit(X_latih, y_latih)
print(f"Akurasi model naif: {naif.score(X_uji, y_uji):.2%}")
print("Confusion matrix:")
print(confusion_matrix(y_uji, naif.predict(X_uji)))
print(classification_report(y_uji, naif.predict(X_uji), zero_division=0))Output yang Dihasilkan:
Akurasi model naif: 90.00%
Confusion matrix:
[[54 0]
[ 6 0]]
precision recall f1-score support
0 0.90 1.00 0.95 54
1 0.00 0.00 0.00 6
accuracy 0.90 60
macro avg 0.45 0.50 0.47 60
weighted avg 0.81 0.90 0.85 60Confusion matrix dibaca per baris sebagai kelas sebenarnya: 54 transaksi kelas 0 benar diprediksi 0, sedangkan seluruh 6 transaksi kelas 1 salah diprediksi sebagai 0. Baris 1 pada classification report memperlihatkan bencananya: precision 0, recall 0, f1-score 0, meski akurasi keseluruhan 90 persen. Inilah alasan akurasi tidak boleh berdiri sendiri pada data tidak seimbang.
Precision, Recall, dan F1 #
Tiga metrik itu menjawab pertanyaan yang berbeda-beda, semuanya dihitung dari confusion matrix:
- Precision: dari seluruh prediksi positif, berapa yang benar. Penting ketika biaya alarm palsu tinggi (memblokir transaksi sah).
- Recall: dari seluruh kelas positif sebenarnya, berapa yang tertangkap. Penting ketika biaya lolosnya tinggi (penipuan lolos).
- F1-score: rata-rata harmonik precision dan recall, berguna saat keduanya sama-sama penting.
Model logistic regression pada data yang sama memperlihatkan kontras yang sehat:
logreg = LogisticRegression(max_iter=1000)
logreg.fit(X_latih, y_latih)
print(classification_report(y_uji, logreg.predict(X_uji), zero_division=0))Output yang Dihasilkan:
precision recall f1-score support
0 0.98 1.00 0.99 54
1 1.00 0.83 0.91 6
accuracy 0.98 60
macro avg 0.99 0.92 0.95 60
weighted avg 0.98 0.98 0.98 60Akurasi naik dari 90 ke 98 persen, dan yang lebih penting, kelas minoritas kini hidup: recall 0.83 berarti 5 dari 6 transaksi kelas 1 berhasil tertangkap. Argumen zero_division=0 hanya menjaga laporan tetap tercetak ketika suatu kelas tidak pernah diprediksi (pembagian dengan nol); ia tidak mengubah hitungan apa pun.
Cross-Validation: Evaluasi yang Lebih Stabil #
Satu pembagian train/test bergantung pada keberuntungan: bagian mana yang kebetulan jatuh ke data uji. K-fold cross-validation membagi data menjadi k lipatan, lalu melatih model k kali; setiap lipatan bergiliran menjadi data uji:
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
iris = load_iris()
skor = cross_val_score(KNeighborsClassifier(n_neighbors=5), iris.data, iris.target, cv=5)
print("Skor tiap fold:", skor.round(3))
print(f"Rata-rata: {skor.mean():.3f} +/- {skor.std():.3f}")Output yang Dihasilkan:
Skor tiap fold: [0.967 1. 0.933 0.967 1. ]
Rata-rata: 0.973 +/- 0.025Fungsi cross_val_score() mengurus seluruh mekanikanya: membelah data, melatih lima model, lalu mengumpulkan skor tiap lipatan. Rata-rata 0.973 dengan simpangan 0.025 jauh lebih meyakinkan daripada satu angka dari satu split karena seluruh data sempat menjadi data uji. Perhatikan juga bahwa kita melewatkan objek model yang belum di-fit(); fungsi ini memanggil fit() sendiri di tiap lipatan.
Metrik untuk Regression: MAE dan RMSE #
Untuk target angka kontinu, akurasi tidak bermakna karena prediksi “hampir benar” tidak ada. Dua metrik standar:
- MAE (mean absolute error): rata-rata besarnya kesalahan dalam satuan target.
- RMSE (root mean squared error): akar dari rata-rata kuadrat kesalahan; menghukum kesalahan besar lebih keras.
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, root_mean_squared_error
rng = np.random.RandomState(42)
luas = rng.uniform(30, 120, 40)
harga_asli = 5000 * luas + rng.normal(0, 20000, 40)
model = LinearRegression().fit(luas.reshape(-1, 1), harga_asli)
prediksi = model.predict(luas.reshape(-1, 1))
print(f"MAE : {mean_absolute_error(harga_asli, prediksi):,.0f} rupiah")
print(f"RMSE: {root_mean_squared_error(harga_asli, prediksi):,.0f} rupiah")Output yang Dihasilkan:
MAE : 14,754 rupiah
RMSE: 17,594 rupiahDalam bahasa manusia: rata-rata prediksi meleset sekitar 14.754 rupiah, dan kesalahan yang lebih besar mendapat hukuman ekstra sehingga RMSE naik ke 17.594 rupiah. RMSE selalu lebih besar atau sama dengan MAE pada data yang sama; selisih keduanya menandakan adanya beberapa kesalahan ekstrem.
GridSearchCV dan Aturan Emas #
Hyperparameter seperti n_neighbors pada k-NN tidak dipelajari dari data; kita yang memilihnya. Daripada mencoba satu-satu manual, GridSearchCV menguji seluruh kombinasi kandidat dengan cross-validation dan melaporkan yang terbaik:
from sklearn.model_selection import GridSearchCV
param = {"n_neighbors": [3, 5, 7, 9]}
pencarian = GridSearchCV(KNeighborsClassifier(), param, cv=5)
pencarian.fit(iris.data, iris.target)
print(f"Parameter terbaik: {pencarian.best_params_}")
print(f"Skor terbaik: {pencarian.best_score_:.3f}")Output yang Dihasilkan:
Parameter terbaik: {'n_neighbors': 7}
Skor terbaik: 0.980Dari empat kandidat, grid search menemukan n_neighbors=7 dengan skor rata-rata cross-validation 0.980; objek pencarian bahkan sudah berupa model terbaik yang siap dipakai. Terakhir, aturan emas evaluasi. Model decision tree tanpa batas kedalaman hampir selalu bisa menghafal data latihnya, sehingga skor di data latih menyimpang optimistis:
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
X_riuh, y_riuh = make_classification(
n_samples=300, n_features=5, n_informative=3, flip_y=0.15, random_state=42
)
X_l, X_u, y_l, y_u = train_test_split(X_riuh, y_riuh, test_size=0.3, random_state=42)
pohon = DecisionTreeClassifier(random_state=42)
pohon.fit(X_l, y_l)
print(f"Akurasi di data latih: {pohon.score(X_l, y_l):.2%}")
print(f"Akurasi di data uji : {pohon.score(X_u, y_u):.2%}")Output yang Dihasilkan:
Akurasi di data latih: 100.00%
Akurasi di data uji : 72.22%Skor latih 100 persen bukan prestasi; skor itu hanyalah bukti model berhasil menghafal 210 baris beserta labelnya yang mengandung 15 persen noise. Kemampuan sebenarnya adalah 72.22 persen di data uji. Setiap angka yang kita laporkan harus berasal dari mekanisme kedua inilah, dan hyperparameter yang dipilih lewat GridSearchCV pun sudah tunduk pada prinsip yang sama karena pemilihan dilakukan memakai skor cross-validation, bukan skor pada data latih penuh.