Model Evaluation

4 jam13 min baca
Tujuan

Pilih metric tepat, evaluate dengan benar, tune hyperparameter dengan rigor.

06 β€” Model Evaluation

Estimasi: 4 jam Tujuan: Pilih metric tepat, evaluate dengan benar, tune hyperparameter dengan rigor.


Kenapa Materi Ini Penting?

Banyak orang training model tapi tidak tahu bagaimana caranya yakin model mereka bagus. Mereka lihat accuracy 95% lalu deploy, tapi ternyata di production model gagal total karena imbalanced data atau distribution shift. Evaluasi yang baik adalah jaring pengaman antara development dan production.

Untuk bootcamp Dicoding, kemampuan memilih metric yang sesuai dengan bisnis adalah skill yang membedakan engineer matang vs pemula. Spam filter butuh precision tinggi (jangan buang email penting). Kanker test butuh recall tinggi (jangan miss diagnosis). Pemilihan metric salah = kerja keras tuning model jadi sia-sia. Kamu juga akan banyak mendiskusikan ini dengan reviewer expert dan stakeholder.

flowchart TD
    A["πŸ“Š Model Evaluation"] --> B["πŸ”’ Pick Metric"]
    A --> C["βœ‚οΈ Split Strategy"]
    A --> D["🎯 Hyperparameter<br/>Tuning"]
    A --> E["πŸ“ˆ Diagnose<br/>Over/Underfit"]
    B --> B1["Classification:<br/>Acc, Precision, Recall,<br/>F1, AUC"]
    B --> B2["Regression:<br/>MSE, MAE, RΒ²"]
    C --> C1["Train/Val/Test"]
    C --> C2["Cross-Validation"]
    D --> D1["Grid Search"]
    D --> D2["Random Search"]
    D --> D3["Optuna (Bayesian)"]
    E --> E1["Learning Curve"]
    E --> E2["Validation Curve"]

Bagian 1 β€” Confusion Matrix Lengkap

Analogi: Tabel Kebenaran Diagnosa

Bayangkan dokter mendiagnosa pasien kanker. Ada 4 kemungkinan: benar diagnosa kanker (TP), benar diagnosa sehat (TN), salah bilang kanker padahal sehat (FP β€” pasien stress sia-sia), salah bilang sehat padahal kanker (FN β€” paling fatal). Confusion matrix adalah tabel 2x2 yang merangkum 4 kasus ini.

Cara Membaca Diagram:

  • Header: "Predict Positive/Negative" (kolom)
  • Sisi: "Actual Positive/Negative" (baris)
  • 4 cell isi: TP, FN, FP, TN
  • Hijau = benar, kuning/merah = salah

Walkthrough Step-by-Step:

  1. TP (kiri-atas): predict positif, actual positif β†’ benar
  2. FN (kanan-atas): predict negatif, actual positif β†’ MISS (Type II) β€” sangat costly di kanker
  3. FP (kiri-bawah): predict positif, actual negatif β†’ false alarm (Type I) β€” costly di spam filter
  4. TN (kanan-bawah): predict negatif, actual negatif β†’ benar

Analogi Sehari-hari: Tes COVID. TP = positif & sakit (benar). FN = negatif tapi sakit (orang menyebar tanpa tahu β€” bahaya!). FP = positif tapi sehat (isolasi sia-sia). TN = negatif & sehat (benar).

Diagram statis Mermaid sebagai fallback:

flowchart TD
    A["πŸ“‹ Confusion Matrix"] --> B["🟒 TP<br/>Predict Pos<br/>Actual Pos<br/>βœ… Benar"]
    A --> C["🟒 TN<br/>Predict Neg<br/>Actual Neg<br/>βœ… Benar"]
    A --> D["🟑 FP<br/>Predict Pos<br/>Actual Neg<br/>❌ Type I Error"]
    A --> E["πŸ”΄ FN<br/>Predict Neg<br/>Actual Pos<br/>❌ Type II Error"]
    style B fill:#d4f4dd
    style C fill:#d4f4dd
    style D fill:#fff4d4
    style E fill:#ffd4d4
                  Predicted
                  Pos        Neg
Actual    Pos    TP        FN
          Neg    FP        TN
  • TP (True Positive) β€” predict pos, actual pos βœ“
  • TN (True Negative) β€” predict neg, actual neg βœ“
  • FP (False Positive) β€” predict pos, actual neg βœ— (Type I error)
  • FN (False Negative) β€” predict neg, actual pos βœ— (Type II error)
from sklearn.metrics import confusion_matrix
import seaborn as sns

cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues")

Bagian 2 β€” Metrics Klasifikasi

Analogi: Pilih Metric Berdasarkan Konsekuensi

Cara Membaca Diagram:

  • Atas: pertanyaan utama (class balanced?)
  • Branch ke metric sesuai biaya kesalahan
  • Tiap leaf = metric yang cocok

Walkthrough Step-by-Step:

  1. Class balanced? β†’ Accuracy OK
  2. Imbalanced + FP costly (spam filter) β†’ Precision
  3. Imbalanced + FN costly (kanker test) β†’ Recall
  4. Imbalanced + keduanya penting β†’ F1 Score
  5. Threshold-independent β†’ ROC-AUC

Analogi Sehari-hari: Pemilihan KPI di bisnis. Restoran fokus repeat customer (recall pelanggan). Bank fokus tidak pinjam ke fraud (precision). Manajer balance keduanya (F1).

Diagram statis Mermaid sebagai fallback:

flowchart TD
    A["🎯 Pilih Metric"] --> B{"Class<br/>balanced?"}
    B -->|Ya| C["βœ… Accuracy OK"]
    B -->|Tidak| D{"Mana yang<br/>lebih costly?"}
    D -->|False Positive<br/>misal spam filter| E["πŸ“Š Precision"]
    D -->|False Negative<br/>misal kanker| F["πŸ“Š Recall"]
    D -->|Keduanya| G["πŸ“Š F1 Score"]
    D -->|Threshold-independent| H["πŸ“Š ROC-AUC"]
    style E fill:#d4f4dd
    style F fill:#d4f4dd
    style G fill:#d4f4dd

Accuracy

Accuracy = (TP + TN) / Total

❌ Tidak bagus untuk imbalanced data. Kalau 95% class A, predict semua A β†’ 95% accuracy.

Precision

Precision = TP / (TP + FP)

"Dari yang predict positive, berapa benar?"

Penting saat FP costly (e.g., spam filter β€” jangan buang email penting).

Recall (Sensitivity)

Recall = TP / (TP + FN)

"Dari yang aktual positive, berapa terdeteksi?"

Penting saat FN costly (e.g., kanker test β€” jangan miss diagnosis).

F1 Score

Harmonic mean precision-recall:

F1 = 2 Γ— (P Γ— R) / (P + R)

Balance keduanya. Default metric kalau bingung.

F-beta

Bias ke salah satu:

  • F2 β€” recall lebih penting
  • F0.5 β€” precision lebih penting

Specificity

Specificity = TN / (TN + FP)

"Dari yang aktual negative, berapa benar?"


Bagian 3 β€” ROC & AUC

Analogi: Trade-off Threshold

Model klasifikasi probabilistik output skor 0-1. Default threshold = 0.5: > 0.5 = positive. Tapi threshold bisa diubah. Kalau kamu naikkan ke 0.7, model jadi lebih "ketat" (predict positive cuma kalau yakin) β†’ precision naik tapi recall turun. Kalau kamu turunkan ke 0.3, model lebih "longgar" β†’ recall naik tapi precision turun.

ROC curve plot trade-off ini di semua threshold sekaligus. AUC (Area Under Curve) = ringkasan seberapa bagus model membedakan dua kelas, terlepas dari threshold.

Cara Membaca Diagram:

  • Kiri ke kanan: pipeline dari probabilitas β†’ ROC β†’ AUC
  • Kanan: interpretasi nilai AUC
  • Makin tinggi AUC = makin baik model membedakan kelas

Walkthrough Step-by-Step:

  1. Model output probabilitas 0-1 per sample
  2. Sweep threshold dari 0.1 sampai 0.9
  3. Tiap threshold hitung TPR (recall) dan FPR (1 - specificity)
  4. Plot TPR vs FPR di tiap threshold β†’ ROC curve
  5. Area di bawah curve = AUC
  6. AUC: 0.5 random, 0.7-0.8 decent, 0.9+ excellent, 1.0 curiga leakage

Analogi Sehari-hari: Detektor logam di bandara. Threshold ketat = banyak false alarm tapi tidak miss senjata (high recall, low precision). Threshold longgar = sedikit false alarm tapi miss senjata (high precision, low recall). ROC menunjukkan trade-off di semua tingkat sensitivitas.

Diagram statis Mermaid sebagai fallback:

flowchart LR
    A["πŸ“Š Probabilitas<br/>0 - 1"] --> B["🎚️ Threshold:<br/>0.1 β†’ 0.9"]
    B --> C["πŸ“ˆ Tiap threshold<br/>hitung TPR, FPR"]
    C --> D["πŸŒ€ Plot ROC curve"]
    D --> E["πŸ“ Hitung AUC<br/>(area under curve)"]
    E --> F{"AUC?"}
    F -->|0.5| G["🎲 Random"]
    F -->|0.7-0.8| H["βœ… Decent"]
    F -->|0.9+| I["πŸ† Excellent"]
    F -->|1.0| J["⚠️ Curiga leakage"]

ROC Curve

Plot TPR (Recall) vs FPR (1 - Specificity) di berbagai threshold.

from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

y_proba = model.predict_proba(X_test)[:, 1]
fpr, tpr, thresholds = roc_curve(y_test, y_proba)

plt.plot(fpr, tpr, label=f"AUC = {roc_auc_score(y_test, y_proba):.3f}")
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.legend()

AUC (Area Under ROC)

  • 0.5 = random
  • 0.7-0.8 = decent
  • 0.8-0.9 = good
  • 0.9+ = excellent
  • 1.0 = perfect (curiga overfit/leak)

Pros AUC: threshold-independent, robust ke imbalance.

Precision-Recall Curve

Lebih informatif untuk highly imbalanced data:

from sklearn.metrics import precision_recall_curve

precision, recall, _ = precision_recall_curve(y_test, y_proba)
plt.plot(recall, precision)

Bagian 4 β€” Multiclass Metrics

from sklearn.metrics import classification_report

print(classification_report(y_test, y_pred))

# Output:
#              precision    recall  f1-score   support
#       class_0     0.85      0.90      0.87       100
#       class_1     0.78      0.72      0.75        80
#       class_2     0.92      0.88      0.90        50
#
#     accuracy                          0.84       230
#    macro avg     0.85      0.83      0.84       230
# weighted avg     0.84      0.84      0.84       230

Macro vs Weighted

  • Macro β€” average tiap class equal (cocok untuk imbalanced β€” class kecil dapat suara)
  • Weighted β€” average tiap class proporsional (mirip accuracy)
  • Micro β€” global TP/FP/FN (= accuracy untuk multiclass)

Bagian 5 β€” Metrics Regression

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import numpy as np

mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
mape = np.mean(np.abs((y_test - y_pred) / y_test)) * 100
Metric Skala Robust ke outlier? Mudah interpret?
MSE Squared ❌ ❌
RMSE Same as y ❌ βœ…
MAE Same as y βœ… βœ…
RΒ² 0-1 ❌ βœ…
MAPE % βœ… βœ…

Bagian 6 β€” Cross-Validation

Analogi: Ujian Beberapa Kali, Soal Berbeda

Tunggu, ini sudah dijelaskan di file 01? Yes, tapi penting diulang dari sudut evaluasi. Kalau cuma single train/test split, score yang kamu dapat bisa lucky/unlucky tergantung random_state. Cross-validation memberi distribusi score (mean Β± std) yang lebih reliable.

flowchart TD
    A["πŸ“¦ Dataset"] --> B{"Tipe data?"}
    B -->|Tabular biasa| C["πŸ“Š KFold (5-10)"]
    B -->|Imbalanced classification| D["βš–οΈ StratifiedKFold"]
    B -->|Time series| E["⏰ TimeSeriesSplit<br/>(no shuffle!)"]
    B -->|Group data| F["πŸ‘₯ GroupKFold"]
    style C fill:#d4f4dd
    style D fill:#d4f4dd
    style E fill:#fff4d4

k-Fold

from sklearn.model_selection import cross_val_score, KFold

scores = cross_val_score(model, X, y, cv=5, scoring="f1")
print(f"CV: {scores.mean():.4f} Β± {scores.std():.4f}")

Stratified K-Fold (untuk classification)

from sklearn.model_selection import StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=cv)

Cross-Val Predict (untuk inspect)

from sklearn.model_selection import cross_val_predict

y_pred_cv = cross_val_predict(model, X, y, cv=5)
# Setiap titik di-predict pakai fold yang dia bukan training-nya

Time Series Split

Untuk data temporal β€” TIDAK BOLEH random shuffle:

from sklearn.model_selection import TimeSeriesSplit

tss = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tss.split(X):
    # train ada 1, 2, 3, ..., test ada n+1
    pass

Bagian 7 β€” Hyperparameter Tuning

from sklearn.model_selection import GridSearchCV

param_grid = {
    "n_estimators": [100, 200, 500],
    "max_depth": [10, 20, None],
    "min_samples_split": [2, 5],
}

grid = GridSearchCV(
    RandomForestClassifier(random_state=42),
    param_grid,
    cv=5,
    scoring="f1",
    n_jobs=-1,
    verbose=2
)
grid.fit(X_train, y_train)

print(f"Best: {grid.best_params_}")
print(f"Score: {grid.best_score_:.4f}")
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

param_dist = {
    "n_estimators": randint(100, 1000),
    "max_depth": randint(5, 50),
    "min_samples_split": randint(2, 20),
}

random = RandomizedSearchCV(
    RandomForestClassifier(random_state=42),
    param_dist,
    n_iter=50,
    cv=5,
    n_jobs=-1
)
random.fit(X_train, y_train)

Bayesian Optimization (Optuna)

# pip install optuna
import optuna

def objective(trial):
    params = {
        "n_estimators": trial.suggest_int("n_estimators", 50, 500),
        "max_depth": trial.suggest_int("max_depth", 3, 30),
        "learning_rate": trial.suggest_float("lr", 0.01, 0.3),
    }
    model = xgb.XGBClassifier(**params)
    score = cross_val_score(model, X_train, y_train, cv=5).mean()
    return score

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100)
print(study.best_params)

Optuna >> Grid/Random untuk dataset besar atau banyak param.


Bagian 8 β€” Learning Curve

Analogi: Murid Hafal vs Murid Paham

Plot learning curve = lihat bagaimana model belajar saat data ditambah.

  • Murid yang menghafal (overfit): dapat 100 di latihan, tapi 60 di ujian. Gap besar.
  • Murid yang tidak belajar (underfit): dapat 60 di latihan, 60 di ujian. Sama-sama jelek.
  • Murid yang paham (good fit): dapat 90 di latihan, 88 di ujian. Dekat dan tinggi.
flowchart TD
    A["πŸ“ˆ Learning Curve"] --> B{"Train vs<br/>Val score?"}
    B -->|Train tinggi<br/>Val rendah<br/>Gap besar| C["⚠️ Overfit<br/>(murid menghafal)"]
    B -->|Train rendah<br/>Val rendah| D["⚠️ Underfit<br/>(murid tidak belajar)"]
    B -->|Train tinggi<br/>Val tinggi<br/>Dekat| E["βœ… Good Fit<br/>(murid paham)"]
    C --> F["πŸ’‘ Solusi: regularize,<br/>reduce complexity,<br/>more data"]
    D --> G["πŸ’‘ Solusi: model lebih kompleks,<br/>more features,<br/>longer training"]
    style C fill:#ffd4d4
    style D fill:#fff4d4
    style E fill:#d4f4dd

Plot training vs validation score saat dataset besar bertambah:

from sklearn.model_selection import learning_curve

train_sizes, train_scores, val_scores = learning_curve(
    model, X, y, cv=5, train_sizes=np.linspace(0.1, 1.0, 10)
)

plt.plot(train_sizes, train_scores.mean(axis=1), label="Train")
plt.plot(train_sizes, val_scores.mean(axis=1), label="Val")
plt.xlabel("Training samples")
plt.ylabel("Score")
plt.legend()

Diagnose

Pattern Diagnosis
Train high, val rendah, gap besar Overfit β€” kurangi complexity, regularize, more data
Train rendah, val rendah Underfit β€” model lebih kompleks, more features
Train & val tinggi & dekat Good fit
Val masih naik di akhir Bisa lebih banyak data

Bagian 9 β€” Validation Curve (Per Hyperparameter)

from sklearn.model_selection import validation_curve

train_scores, val_scores = validation_curve(
    RandomForestClassifier(),
    X, y,
    param_name="max_depth",
    param_range=[1, 5, 10, 20, 50, None],
    cv=5
)

Plot show optimal value tiap hyperparameter.


Tabel Komparasi Hyperparameter Tuning

Method Speed Accuracy Best For
GridSearchCV 🐌 Slow ⭐⭐⭐⭐ Sedikit hyperparameter, exhaustive
RandomizedSearchCV ⚑⚑ ⭐⭐⭐ Banyak hyperparameter, budget terbatas
Optuna (Bayesian) ⚑⚑⚑ ⭐⭐⭐⭐⭐ Banyak hyperparameter, smart search
Manual Tuning ⚑⚑⚑⚑ ⭐⭐ Eksplorasi awal, debugging

Common Mistakes & FAQ

Common Mistakes

flowchart TD
    A["⚠️ Common Mistakes"] --> B["πŸ“Š Cuma lihat accuracy<br/>untuk imbalanced"]
    A --> C["🎯 Tune di test set"]
    A --> D["πŸ’§ Data leakage di CV"]
    A --> E["🎲 Tidak shuffle<br/>untuk classification"]
    A --> F["⏰ Time series<br/>random shuffle"]
    A --> G["πŸ“ˆ Tidak bandingkan<br/>dengan baseline"]
    style A fill:#ffe0e0

1. Cuma Accuracy untuk Imbalanced

Dataset 99% kelas A, 1% kelas B. Predict semua A β†’ 99% accuracy. Useless.

βœ… Pakai F1, AUC, atau precision/recall sesuai konteks bisnis.

2. Tune di Test Set

Iterasi GridSearchCV pakai test set β†’ kamu fit hyperparameter ke test β†’ leak.

βœ… Tune pakai validation set atau CV. Test set sentuh sekali untuk evaluasi final.

3. CV tanpa Stratify untuk Classification

Imbalanced data + KFold biasa β†’ fold bisa skip kelas minor.

βœ… Pakai StratifiedKFold.

4. Time Series Random Shuffle

❌ Time series dengan KFold biasa β†’ train pakai data masa depan, test pakai data masa lalu. Mustahil di production.

βœ… Pakai TimeSeriesSplit yang menjaga urutan temporal.

5. Tidak Bandingkan dengan Baseline

"Model saya accuracy 80%". Bagus? Tergantung. Kalau baseline (predict majority) sudah 78%, model kamu cuma improve 2%. Selalu compare dengan DummyClassifier.

FAQ

Q: Saya harus pakai metric apa? A: Tergantung bisnis. Diskusi dengan stakeholder dulu. Kalau bingung, F1 atau AUC default yang aman untuk klasifikasi.

Q: Berapa CV fold yang ideal? A: 5-10 standar. Dataset kecil β†’ 10. Dataset besar β†’ 5 (lebih cepat).

Q: Grid Search atau Random Search? A: Random Search hampir selalu lebih efisien. Grid hanya kalau hyperparameter sedikit (≀ 3 dengan opsi sedikit).

Q: Optuna selalu lebih bagus? A: Untuk banyak hyperparameter dan dataset besar, ya. Untuk masalah kecil, overhead-nya tidak terbayar.

Q: Train accuracy 100%, test accuracy 90%. Bagus? A: Curiga overfit. Coba regularize atau cross-validate. Gap kecil bisa OK, gap besar (>10%) suspicious.

Q: ROC-AUC 0.95 tapi PR-AUC 0.4. Kenapa? A: Klasik tanda imbalanced data. ROC-AUC over-optimistic untuk imbalanced. Trust PR-AUC.


Cek Pemahaman

  • Bisa baca confusion matrix?
  • Tahu kapan pakai precision vs recall?
  • Tahu apa itu AUC dan kapan dipakai?
  • Bisa pakai cross-validation?
  • Bisa Grid/Random Search?
  • Bisa diagnose overfit/underfit dari learning curve?

Challenge 5.6

Challenge β€” Full Evaluation Pipeline

Pilih dataset, train model, lakukan:

  1. Confusion matrix + classification report
  2. ROC curve + AUC
  3. PR curve
  4. Learning curve (diagnose fit)
  5. Hyperparameter tuning (Grid + Random)
  6. Final model dengan best params
  7. Test set evaluation

Notebook lengkap, push ke GitHub.


Selanjutnya: 07-kaggle-project.md