Bab 4: Ensemble Methods dan Evaluasi Model

🎯 Hasil Pembelajaran (Learning Outcomes)

Setelah mempelajari bab ini, Anda akan mampu:

  1. Memahami prinsip ensemble learning dan mengapa banyak model lebih baik dari satu model
  2. Menjelaskan bias-variance tradeoff dan cara ensemble methods mengurangi variance
  3. Mengimplementasi berbagai ensemble techniques: Bagging, Boosting, Stacking
  4. Menghitung metrik evaluasi komprehensif: akurasi, precision, recall, F1-score, ROC-AUC
  5. Menangani class imbalance dan memilih metric yang tepat untuk business context
  6. Optimalkan model menggunakan GridSearchCV dan hyperparameter tuning secara sistematis

4.1 Pengantar: Kekuatan Kolaborasi

“Kebijaksanaan kerumunan” - ide bahwa banyak predictor sederhana yang dikombinasikan menghasilkan predictor yang lebih baik daripada predictor individual.

Ensemble learning adalah teknik yang menggabungkan banyak model untuk mencapai performa superior. Ide sederhananya:

  • Keragaman (Diversity): Model harus berbeda satu sama lain
  • Ketepatan (Accuracy): Setiap model harus lebih baik dari random guessing
  • Kemerdekaan (Independence): Kesalahan model tidak boleh berkorelasi sempurna

\[\text{Ensemble Error} = \text{Bias} + \text{Variance} + \text{Error}\]

Ensemble methods mengurangi variance sambil mempertahankan bias rendah.

4.2 Prinsip Ensemble Learning

4.2.1 Bias-Variance Decomposition

Setiap prediksi memiliki dua sumber error:

  • Bias: Error dari model yang terlalu sederhana (underfitting)
  • Variance: Error dari model yang terlalu kompleks (overfitting)
Code
import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score

# Buat data dengan true function: y = sin(x)
np.random.seed(42)
X_train = np.random.uniform(0, 2*np.pi, 20).reshape(-1, 1)
y_train = np.sin(X_train.ravel()) + np.random.normal(0, 0.1, 20)

X_test = np.linspace(0, 2*np.pi, 200).reshape(-1, 1)
y_test = np.sin(X_test.ravel())

# Models dengan berbagai complexity
degrees = [1, 3, 5, 10, 15]
train_errors = []
test_errors = []

for d in degrees:
    model = Pipeline([
        ('poly_features', PolynomialFeatures(degree=d)),
        ('linear_regression', LinearRegression())
    ])

    model.fit(X_train, y_train)

    train_error = np.mean((model.predict(X_train) - y_train) ** 2)
    test_error = np.mean((model.predict(X_test) - y_test) ** 2)

    train_errors.append(train_error)
    test_errors.append(test_error)

    print(f"Degree {d:2d}: Train Error = {train_error:.4f}, Test Error = {test_error:.4f}")

# Visualisasi Bias-Variance Trade-off
plt.figure(figsize=(10, 5))
plt.plot(degrees, train_errors, 'o-', label='Training Error', linewidth=2, markersize=8)
plt.plot(degrees, test_errors, 's-', label='Test Error (Generalization)', linewidth=2, markersize=8)
plt.xlabel('Model Complexity (Polynomial Degree)', fontsize=12)
plt.ylabel('Mean Squared Error', fontsize=12)
plt.title('Bias-Variance Trade-off', fontsize=14, fontweight='bold')
plt.legend(fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print(f"\nUnderfitting: Low complexity models (degree 1-3)")
print(f"Optimal: Medium complexity models (degree 3-5)")
print(f"Overfitting: High complexity models (degree 10+)")

4.3 Bagging (Bootstrap Aggregating)

Bagging mengurangi variance dengan melatih banyak model pada bootstrap samples dari data.

Algoritma:

  1. Buat B bootstrap samples dari training data
  2. Latih base learner pada setiap sample
  3. Agregasi prediksi semua models

Untuk klasifikasi: voting mayoritas Untuk regresi: rata-rata prediksi

4.3.1 Out-of-Bag (OOB) Error Estimation

Bootstrap sample rata-rata mengandung ~63.2% unique samples. Sisanya (Out-of-Bag) bisa digunakan untuk validasi tanpa perlu test set terpisah.

Code
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import make_classification

# Data
X, y = make_classification(n_samples=300, n_features=10, n_informative=5,
                          n_classes=2, random_state=42)

# Bagging dengan OOB estimation
bagging = BaggingClassifier(
    estimator=DecisionTreeClassifier(),
    n_estimators=50,
    oob_score=True,
    random_state=42
)

bagging.fit(X, y)

print(f"OOB Score (unbiased estimate): {bagging.oob_score_:.4f}")
print(f"Training Score: {bagging.score(X, y):.4f}")
print(f"OOB diestimasi dari ~{int(300 * 0.632)} samples yang tidak digunakan")

4.3.2 Feature Importance dari Random Forest

Random Forest mengukur importance fitur berdasarkan pengurangan impurity rata-rata.

Code
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
import pandas as pd

# Data
cancer = load_breast_cancer()
X = cancer.data
y = cancer.target

# Train Random Forest
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

# Feature Importance
feature_importance = pd.DataFrame({
    'Feature': cancer.feature_names,
    'Importance': rf.feature_importances_
}).sort_values('Importance', ascending=False)

print("Top 10 Most Important Features:")
print(feature_importance.head(10).to_string(index=False))

# Visualisasi top 10
import matplotlib.pyplot as plt
top_10 = feature_importance.head(10)
plt.figure(figsize=(10, 6))
plt.barh(range(len(top_10)), top_10['Importance'].values)
plt.yticks(range(len(top_10)), top_10['Feature'].values)
plt.xlabel('Importance', fontsize=12)
plt.title('Top 10 Feature Importance (Random Forest)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

4.4 Boosting Methods

Boosting melatih models secara sequential, setiap model fokus pada sampel yang sulit dari model sebelumnya.

4.4.1 AdaBoost (Adaptive Boosting)

Adaptive Boosting meningkatkan bobot sampel yang salah klasifikasi.

Algoritma:

  1. Mulai dengan bobot uniform untuk semua samples
  2. Latih weak learner
  3. Tingkatkan bobot sampel yang salah
  4. Ulangi untuk M iterasi
  5. Kombinasi dengan weighted majority voting
Code
from sklearn.ensemble import AdaBoostClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# Data
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.2, random_state=42
)

# AdaBoost dengan Decision Tree sebagai base learner
adaboost = AdaBoostClassifier(
    estimator=DecisionTreeClassifier(max_depth=1),  # Weak learner
    n_estimators=50,
    learning_rate=1.0,
    random_state=42
)

adaboost.fit(X_train, y_train)

train_acc = adaboost.score(X_train, y_train)
test_acc = adaboost.score(X_test, y_test)

print(f"AdaBoost Train Accuracy: {train_acc:.4f}")
print(f"AdaBoost Test Accuracy: {test_acc:.4f}")
print(f"Number of estimators: {len(adaboost.estimators_)}")

4.4.2 Gradient Boosting

Gradient Boosting melatih models secara sequential untuk memprediksi residual dari model sebelumnya.

Intuisi:

  • Model 1: Prediksi awal (error besar)
  • Model 2: Prediksi residual dari Model 1
  • Model 3: Prediksi residual dari Model 1+2
  • …dan seterusnya
Code
from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor
from sklearn.datasets import make_regression

# Regression example
X, y = make_regression(n_samples=200, n_features=10, n_informative=5,
                       noise=10, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Gradient Boosting Regressor
gbr = GradientBoostingRegressor(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,
    random_state=42
)

gbr.fit(X_train, y_train)

train_r2 = gbr.score(X_train, y_train)
test_r2 = gbr.score(X_test, y_test)

print(f"Gradient Boosting Train R²: {train_r2:.4f}")
print(f"Gradient Boosting Test R²: {test_r2:.4f}")

# Feature importance
feature_importance = pd.DataFrame({
    'Feature': [f'Feature {i}' for i in range(10)],
    'Importance': gbr.feature_importances_
}).sort_values('Importance', ascending=False)

print("\nTop 5 Feature Importance:")
print(feature_importance.head(5).to_string(index=False))

4.4.3 XGBoost: Extreme Gradient Boosting

XGBoost adalah implementasi yang dioptimalkan dan lebih cepat dari Gradient Boosting.

Code
# Catatan: Uncomment jika xgboost sudah terinstall
# from xgboost import XGBClassifier
#
# xgb = XGBClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42)
# xgb.fit(X_train, y_train)
#
# xgb_acc = xgb.score(X_test, y_test)
# print(f"XGBoost Test Accuracy: {xgb_acc:.4f}")

print("XGBoost memerlukan instalasi: pip install xgboost")
print("Namun GradientBoosting dari sklearn sudah cukup untuk praktikum")

Kapan menggunakan Boosting:

  • Akurasi maksimal penting
  • Dataset besar
  • Interpretabilitas kurang penting
  • Waktu training bukan masalah
Warning: Boosting dan Overfitting

Boosting bisa overfit jika:

  • Terlalu banyak iterasi (n_estimators)
  • Learning rate terlalu tinggi
  • Max depth terlalu besar

Gunakan early stopping dan validasi set!

4.5 Stacking & Blending

4.5.1 Stacking (Stacked Generalization)

Stacking menggunakan meta-learner untuk mengkombinasikan prediksi dari base learners.

Proses:

  1. Split data menjadi 3 bagian (train, validation, test)
  2. Latih base learners pada bagian train
  3. Prediksi pada bagian validation (features baru)
  4. Latih meta-learner pada prediksi validation
  5. Prediksi final dengan base learners dan meta-learner
Code
from sklearn.model_selection import cross_val_predict
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification

# Data
X, y = make_classification(n_samples=400, n_features=20, n_informative=10,
                          n_classes=2, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Base learners
base_learners = [
    ('rf', RandomForestClassifier(n_estimators=50, random_state=42)),
    ('gb', GradientBoostingClassifier(n_estimators=50, random_state=42))
]

# Prediksi dari base learners menggunakan cross-validation
meta_features_train = []
for name, model in base_learners:
    pred = cross_val_predict(model, X_train, y_train, cv=5, method='predict_proba')
    meta_features_train.append(pred[:, 1])  # Ambil probabilitas kelas 1

X_meta_train = np.column_stack(meta_features_train)

# Train meta-learner
meta_learner = LogisticRegression(random_state=42)
meta_learner.fit(X_meta_train, y_train)

# Prediksi pada test set
meta_features_test = []
for name, model in base_learners:
    model.fit(X_train, y_train)
    pred = model.predict_proba(X_test)
    meta_features_test.append(pred[:, 1])

X_meta_test = np.column_stack(meta_features_test)
y_pred_stacking = meta_learner.predict(X_meta_test)

acc_stacking = np.mean(y_pred_stacking == y_test)
print(f"Stacking Accuracy: {acc_stacking:.4f}")

# Bandingkan dengan base learners individual
for name, model in base_learners:
    model.fit(X_train, y_train)
    acc = model.score(X_test, y_test)
    print(f"{name.upper()} Accuracy: {acc:.4f}")

4.6 Comprehensive Model Evaluation

4.6.1 Metrics untuk Klasifikasi

Classification Metrics Taxonomy

Dari Confusion Matrix:

  • Accuracy: Akurasi overall
  • Precision: Dari prediksi positif, berapa yang benar?
  • Recall: Dari kasus positif, berapa yang terdeteksi?
  • Specificity: Dari kasus negatif, berapa yang terdeteksi?
  • F1-Score: Harmonic mean precision dan recall

Threshold-Independent:

  • ROC-AUC: Area under ROC curve
  • PR-AUC: Area under Precision-Recall curve
  • Log Loss: Cross-entropy loss

Multi-class:

  • Macro F1: F1 rata-rata per kelas
  • Weighted F1: F1 weighted by support
  • One-vs-Rest: Evaluate per kelas
Code
from sklearn.metrics import (classification_report, roc_curve, auc,
                            precision_recall_curve, confusion_matrix)

# Binary classification
y_true = [0, 1, 1, 0, 1, 1, 0, 1, 0, 1]
y_pred_prob = [0.1, 0.8, 0.7, 0.2, 0.9, 0.6, 0.3, 0.85, 0.15, 0.95]
y_pred = [int(p > 0.5) for p in y_pred_prob]

# Comprehensive report
print("Classification Report:")
print(classification_report(y_true, y_pred, target_names=['Negative', 'Positive']))

# ROC Curve
fpr, tpr, thresholds = roc_curve(y_true, y_pred_prob)
roc_auc = auc(fpr, tpr)

print(f"\nROC-AUC Score: {roc_auc:.4f}")

# Precision-Recall Curve
precision, recall, thresholds = precision_recall_curve(y_true, y_pred_prob)
pr_auc = auc(recall, precision)

print(f"PR-AUC Score: {pr_auc:.4f}")

4.6.2 Metrics untuk Regresi

Code
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

# Regression predictions
y_true = [3.0, -0.5, 2.0, 7.0, 4.3, -2.0]
y_pred = [2.5, 0.0, 2.0, 8.0, 4.0, -2.5]

mae = mean_absolute_error(y_true, y_pred)
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_true, y_pred)

print("Regression Metrics:")
print(f"  MAE (Mean Absolute Error): {mae:.4f}")
print(f"  MSE (Mean Squared Error): {mse:.4f}")
print(f"  RMSE (Root Mean Squared Error): {rmse:.4f}")
print(f"  R² Score: {r2:.4f}")

# MAPE (Mean Absolute Percentage Error)
mape = np.mean(np.abs((np.array(y_true) - np.array(y_pred)) / np.array(y_true))) * 100
print(f"  MAPE (Mean Absolute % Error): {mape:.2f}%")

4.7 Menangani Class Imbalance

Ketika kelas tidak seimbang (misal 95% negative, 5% positive), model cenderung memprioritaskan kelas mayoritas.

4.7.1 Strategi Handling Imbalance

1. Resampling:

Code
from sklearn.datasets import make_classification
from collections import Counter

# Data dengan imbalance
X, y = make_classification(n_samples=1000, n_features=20, n_informative=10,
                          weights=[0.9, 0.1], random_state=42)

print(f"Original distribution: {Counter(y)}")

# Oversampling (duplicate minority class)
from sklearn.utils import resample

X_majority = X[y == 0]
X_minority = X[y == 1]
y_majority = y[y == 0]
y_minority = y[y == 1]

X_minority_upsampled, y_minority_upsampled = resample(
    X_minority, y_minority,
    n_samples=len(X_majority),
    random_state=42
)

X_balanced = np.vstack([X_majority, X_minority_upsampled])
y_balanced = np.hstack([y_majority, y_minority_upsampled])

print(f"After oversampling: {Counter(y_balanced)}")

2. SMOTE (Synthetic Minority Over-sampling):

Code
# from imblearn.over_sampling import SMOTE
#
# smote = SMOTE(random_state=42)
# X_smote, y_smote = smote.fit_resample(X, y)
# print(f"After SMOTE: {Counter(y_smote)}")

print("SMOTE tersedia di: pip install imbalanced-learn")

3. Class Weight:

Code
from sklearn.utils.class_weight import compute_class_weight

# Hitung class weights otomatis
class_weights = compute_class_weight('balanced', classes=np.unique(y), y=y)
print(f"Class weights: {class_weights}")

# Gunakan dalam model
rf_balanced = RandomForestClassifier(
    n_estimators=100,
    class_weight='balanced',  # atau dictionary: {0: weight0, 1: weight1}
    random_state=42
)

rf_balanced.fit(X, y)
print(f"Balanced RF Accuracy: {rf_balanced.score(X, y):.4f}")

4. Threshold Adjustment:

Code
from sklearn.metrics import precision_recall_curve

# Ubah decision threshold
y_pred_proba = rf_balanced.predict_proba(X)[:, 1]

# Default threshold = 0.5
y_pred_default = (y_pred_proba > 0.5).astype(int)

# Adjusted threshold = 0.3 (lebih sensitif mendeteksi minority)
y_pred_adjusted = (y_pred_proba > 0.3).astype(int)

from sklearn.metrics import recall_score
print(f"Recall dengan threshold 0.5: {recall_score(y, y_pred_default):.4f}")
print(f"Recall dengan threshold 0.3: {recall_score(y, y_pred_adjusted):.4f}")

4.8 Best Practices untuk Model Selection

Code
from sklearn.model_selection import GridSearchCV

# Data
X, y = make_classification(n_samples=300, n_features=10, n_informative=5,
                          n_classes=2, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Hyperparameter grid untuk Random Forest
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 7, None],
    'min_samples_split': [2, 5, 10]
}

# GridSearchCV dengan stratified k-fold
rf = RandomForestClassifier(random_state=42)
grid_search = GridSearchCV(
    rf, param_grid,
    cv=5,
    scoring='f1',  # atau 'roc_auc' untuk imbalanced data
    n_jobs=-1
)

grid_search.fit(X_train, y_train)

print(f"Best parameters: {grid_search.best_params_}")
print(f"Best CV F1 Score: {grid_search.best_score_:.4f}")
print(f"Test F1 Score: {grid_search.score(X_test, y_test):.4f}")

4.9 Model Evaluation Checklist

10-Point Evaluation Checklist
  1. Data Leakage: Pastikan tidak ada informasi dari test set ke training
  2. Cross-Validation: Gunakan k-fold bukan hanya single train-test split
  3. Stratification: Untuk klasifikasi imbalanced, gunakan stratified k-fold
  4. Baseline Model: Bandingkan dengan simple baseline
  5. Multiple Metrics: Jangan hanya akurasi; gunakan precision, recall, F1, AUC
  6. Threshold Tuning: Cari threshold optimal untuk use case
  7. Feature Importance: Pahami fitur mana yang penting
  8. Error Analysis: Analisis kesalahan model untuk perbaikan
  9. Hyperparameter Tuning: Gunakan GridSearchCV atau RandomizedSearchCV
  10. Final Validation: Validasi dengan truly held-out test set

4.10 Review Questions

Pertanyaan Konseptual:

  1. Jelaskan mengapa ensemble methods biasanya lebih baik dari single models?

  2. Perbedaan antara Bagging dan Boosting dalam hal training strategy?

  3. Kapan precision dan kapan recall lebih penting?

  4. Apa masalah utama dengan imbalanced datasets dan bagaimana mengatasi?

  5. Mengapa class weights lebih baik dari oversampling pada dataset besar?

Pertanyaan Praktis:

  1. Anda membangun model klasifikasi untuk deteksi fraud (hanya 0.1% data adalah fraud). Model Anda mencapai 99.9% accuracy. Apakah ini model yang baik? Metric apa yang lebih informatif?

  2. Jelaskan perbedaan antara precision = 0.9 dan recall = 0.9. Manakah yang lebih penting untuk: (a) email spam detection, (b) cancer diagnosis, (c) bank loan approval?

  3. Anda menjalankan GridSearchCV dengan 100 parameter combinations, 5-fold CV, dan data dengan 10,000 samples. Berapa total model yang akan dilatih? Apakah ini efisien?

  4. ROC-AUC = 0.95 terdengar sempurna, tapi Precision-Recall AUC = 0.3. Bagaimana ini mungkin? Kapan ini bisa terjadi?

  5. Anda telah mengoptimalkan hyperparameters dengan GridSearchCV pada training set, dan mendapatkan akurasi test 95%. Namun di production, akurasi turun menjadi 70%. Apa yang mungkin penyebabnya dan bagaimana mendiagnosis?

4.11 Hands-On Exercise

End-to-End Classification Project:

  1. Dataset: Gunakan dataset imbalanced (misal fraud detection atau loan default)

  2. Exploratory Analysis: Pahami imbalance ratio dan fitur

  3. Preprocessing: Handle missing values, scale features

  4. Baseline Model: Train logistic regression dengan default threshold

  5. Ensemble Models: Train Random Forest, Gradient Boosting, Stacking

  6. Evaluation:

    • 5-fold stratified CV
    • Plot ROC curves untuk semua models
    • Hitung precision, recall, F1, AUC
  7. Optimization:

    • GridSearchCV untuk best hyperparameters
    • Threshold tuning untuk business requirements
    • Handle class imbalance dengan SMOTE atau class_weight
  8. Final Comparison: Pilih model terbaik dengan justifikasi jelas

Tips:

  • Jangan forget data leakage!
  • Dokumentasikan setiap keputusan
  • Visualisasi hasil (confusion matrix, ROC curve, feature importance)

🎯 Key Takeaways

Ensemble methods meningkatkan performa dengan menggabungkan banyak models, mengurangi variance tanpa menambah bias

Bagging (Random Forest) mengurangi variance melalui bootstrap sampling dan agregasi, dengan OOB estimation untuk validasi efisien

Boosting (AdaBoost, Gradient Boosting, XGBoost) melatih models secara sequential untuk fokus pada sampel yang sulit

Stacking menggunakan meta-learner untuk mengoptimalkan kombinasi prediksi dari diverse base learners

Bias-Variance Tradeoff adalah konsep fundamental: underfitting (high bias), overfitting (high variance), ensemble helps balance keduanya

Metrics selection harus sesuai context: accuracy untuk balanced, F1/ROC-AUC untuk imbalanced, precision untuk minimizing false positives, recall untuk minimizing false negatives

Class imbalance memerlukan strategi khusus: resampling, SMOTE, class weights, atau threshold adjustment

Cross-validation dengan stratified k-fold memberikan estimasi performa yang lebih reliable dan mencegah overfitting

Hyperparameter tuning dengan GridSearchCV atau RandomizedSearchCV mengoptimalkan model secara sistematis

Feature importance dari ensemble models memberikan insights untuk feature selection dan interpretability