Episode 7 dari catatan belajar AI saya. Setelah paham cara melatih model, pertanyaan berikutnya muncul begitu model pertama saya selesai dilatih: “bagus” itu ukurannya apa? Angka accuracy 95% terdengar membanggakan — sampai saya belajar bahwa angka tunggal itu bisa menipu total.
Kenapa accuracy saja tidak cukup
Contoh klasik yang langsung mengubah cara saya berpikir: deteksi penyakit yang hanya menyerang 1 dari 100 orang. Model bodoh yang selalu menjawab “sehat” akan mencapai akurasi 99%. Sempurna di atas kertas, useless di dunia nyata — dia tidak pernah menemukan satu pasien pun. Ini disebut imbalanced dataset, dan accuracy adalah metrik pertama yang harus dicurigai kalau kelasnya timpang.
Momen itu mengingatkan saya pada analogi pendekar yang selalu menang berkelahi — tetapi hanya mau bertarung melawan orang yang jelas-jelas kalah. Kemenangannya benar secara statistik, tetapi tidak membuktikan apa-apa. Accuracy memiliki kelemahan struktural yang sama: ia menghitung proporsi prediksi benar tanpa peduli apakah prediksi benar itu berasal dari keberhasilan mendeteksi kasus penting atau sekadar menebak kelas mayoritas.
Confusion matrix: empat kotak kejujuran
Untuk klasifikasi biner, semua metrik lahir dari tabel 2×2 bernama confusion matrix. Ambil kasus deteksi spam (positif = spam):
| Diprediksi spam | Diprediksi bukan | |
|---|---|---|
| Aslinya spam | True Positive (TP) | False Negative (FN) |
| Aslinya bukan | False Positive (FP) | True Negative (TN) |
- FP (false positive) — email penting masuk folder spam. Menyebalkan, bisa fatal.
- FN (false negative) — spam lolos ke inbox. Mengganggu tapi masih bisa dihapus.
Insight pentingnya: dua jenis kesalahan ini tidak setara harganya, dan harganya bergantung konteks. Di deteksi penyakit, FN jauh lebih mahal daripada FP. Di filter spam sebaliknya. Confusion matrix memaksa kita melihat keduanya secara terpisah, bukan dirata-rata.
Contoh kode: confusion matrix dan classification report
Saya tulis kode lengkap ini untuk membangun confusion matrix dari nol. Dengan scikit-learn, dua baris sudah cukup — tetapi mengetahui cara membaca outputnya jauh lebih penting daripada mengetik perintahnya.
from sklearn.metrics import (
classification_report, confusion_matrix, accuracy_score
)
import numpy as np
# Contoh: model klasifikasi spam
y_true = [1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0]
y_pred = [1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0]
# Confusion matrix
cm = confusion_matrix(y_true, y_pred)
print("Confusion Matrix:")
print(cm)
# [[5 1] <- 5 TN, 1 FP
# [2 7]] <- 2 FN, 7 TP
# Classification report — semua metrik sekaligus
print("\nClassification Report:")
print(classification_report(y_true, y_pred,
target_names=["Bukan Spam", "Spam"]))
# Accuracy manual
acc = accuracy_score(y_true, y_pred)
print(f"Accuracy: {acc:.2%}")
# Output: 80.00% — terlihat bagus, tetapi perhatikan
# model masih melewatkan 2 spam (FN) dan 1 email
# penting terjebak di folder spam (FP)Visualisasi confusion matrix dengan heatmap
Angka di terminal memang informatif, tetapi visualisasi heatmap membuat pola kesalahan model langsung terlihat dalam sekejap. Saya sering mencetak heatmap ini sebelum presentasi hasil — audiens langsung paham di mana model lemah tanpa perlu menjelaskan satu per satu angka.
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix
def plot_confusion_matrix(y_true, y_pred, labels=None):
"""Visualisasi confusion matrix sebagai heatmap."""
cm = confusion_matrix(y_true, y_pred)
plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
xticklabels=labels, yticklabels=labels)
plt.ylabel("Aktual")
plt.xlabel("Prediksi")
plt.title("Confusion Matrix")
plt.tight_layout()
plt.savefig("confusion_matrix.png", dpi=150)
plt.show()
# Contoh penggunaan
y_true = [1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0]
y_pred = [1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0]
plot_confusion_matrix(y_true, y_pred,
labels=["Bukan Spam", "Spam"])Sejak saya mulai menggunakan heatmap ini, presentasi model saya jauh lebih mudah dipahami oleh tim non-teknis. Warna gelap di diagonal utama berarti model benar, warna terang di luar diagonal berarti kesalahan — analogi sederhana yang langsung nyampai.
Precision dan recall
Precision menjawab: "dari semua yang diprediksi positif, berapa yang benar-benar positif?" TP / (TP + FP). Tinggi = sedikit alarm palsu. Recall menjawab: "dari semua yang benar-benar positif, berapa yang berhasil ditemukan?" TP / (TP + FN). Tinggi = sedikit kasus lolos.
Dan di sinilah bagian yang paling jujur saya tulis: precision dan recall itu seperti trade-off. Naikkan threshold supaya model cuma menandai spam saat sangat yakin? Precision naik, recall turun. Turunkan threshold biar semua spam tertangkap? Recall naik, precision anjlok. Tidak ada model yang menang di keduanya sekaligus — yang ada adalah pilihan sesuai harga kesalahan di domain kita.
F1 score: harmonik mean yang jujur
F1 score adalah rata-rata harmonik antara precision dan recall — satu angka untuk membandingkan model saat kita belum tahu mana yang lebih penting:
from sklearn.metrics import f1_score
# Contoh: model yang precision tinggi tapi recall rendah
y_true = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
y_pred_1 = [1, 1, 1, 0, 0, 0, 0, 0, 0, 0] # precision=1.0, recall=0.6
y_pred_2 = [1, 1, 1, 1, 0, 0, 0, 1, 0, 0] # precision=0.8, recall=0.8
f1_1 = f1_score(y_true, y_pred_1)
f1_2 = f1_score(y_true, y_pred_2)
print(f"Model 1 — Precision: 1.0, Recall: 0.6, F1: {f1_1:.2f}")
print(f"Model 2 — Precision: 0.8, Recall: 0.8, F1: {f1_2:.2f}")
# Model 2 menang di F1 karena keseimbangan lebih baikMengapa rata-rata harmonik bukan aritmatika? Karena harmonik mean menekan nilai ekstrem. Jika precision 1.0 tetapi recall 0.1, rata-rata aritmatika menghasilkan 0.55 — terdengar lumayan. Harmonik mean menghasilkan 0.18 — jauh lebih jujur. Ini tepat untuk dataset imbalance di mana model bisa saja sangat presisi tetapi hampir tidak pernah memprediksi kelas minoritas.
ROC Curve dan AUC
ROC curve (Receiver Operating Characteristic) memplot True Positive Rate (recall) terhadap False Positive Rate di berbagai threshold prediksi. Kurva yang semakin ke pojok kiri atas menandakan model semakin baik. AUC (Area Under Curve) merangkum kinerja model dalam satu angka: AUC = 1.0 sempurna, AUC = 0.5 setara tebakan acak.
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt
# Asumsi model menghasilkan probabilitas kelas positif
y_true = [1, 1, 1, 0, 0, 1, 0, 0, 0, 1]
y_score = [0.9, 0.8, 0.6, 0.2, 0.1, 0.7, 0.3, 0.05, 0.15, 0.85]
# Hitung ROC curve
fpr, tpr, thresholds = roc_curve(y_true, y_score)
auc = roc_auc_score(y_true, y_score)
# Plot
plt.figure(figsize=(6, 5))
plt.plot(fpr, tpr, "b-", label=f"AUC = {auc:.2f}")
plt.plot([0, 1], [0, 1], "r--", label="Acak (AUC = 0.5)")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate (Recall)")
plt.title("ROC Curve")
plt.legend()
plt.tight_layout()
plt.show()
print(f"AUC: {auc:.2f}")
# AUC > 0.9 = excellent, 0.8-0.9 = good, 0.7-0.8 = fairROC AUC sangat berguna saat membandingkan dua model yang menghasilkan probabilitas, bukan hanya label. Keunggulannya: metrik ini independen dari threshold yang dipilih, sehingga kita bisa membandingkan kinerja model secara utuh tanpa harus memilih threshold lebih dulu.
Precision-Recall Curve: kapan lebih baik dari ROC
Precision-Recall Curve memplot precision terhadap recall di berbagai threshold. Kurva ini jauh lebih informatif daripada ROC saat dataset sangat imbalance. Alasannya: ROC menggunakan False Positive Rate yang melibatkan TN, dan TN sangat besar pada dataset imbalance sehingga FPR tetap rendah meskipun model banyak salah pada kelas minoritas. Precision-Recall Curve tidak menggunakan TN sama sekali — ia hanya fokus pada kelas minoritas.
from sklearn.metrics import precision_recall_curve, average_precision_score
import matplotlib.pyplot as plt
# Kasus imbalance: hanya 10% kelas positif
y_true = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0]
y_score = [0.9, 0.4, 0.3, 0.2, 0.1, 0.35, 0.25, 0.15, 0.1, 0.05,
0.85, 0.3, 0.2, 0.4, 0.7, 0.3, 0.2, 0.1, 0.15, 0.08]
precision, recall, thresholds = precision_recall_curve(y_true, y_score)
ap = average_precision_score(y_true, y_score)
plt.figure(figsize=(6, 5))
plt.plot(recall, precision, "b-", label=f"AP = {ap:.2f}")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.title("Precision-Recall Curve")
plt.legend()
plt.tight_layout()
plt.show()
print(f"Average Precision: {ap:.2f}")
# AP > 0.8 = excellent untuk kasus imbalancePengalaman saya: saat pertama kali membangun model deteksi fraud yang kelas positifnya hanya 2% dari total transaksi, ROC AUC menunjukkan 0.95 — terkesan bagus. Tetapi Precision-Recall Curve menunjukkan average precision hanya 0.35 — artinya ketika model menandai transaksi sebagai fraud, hanya 35% yang benar-benar fraud. ROC menipu karena TN yang sangat besar membangun ilusi performa yang baik.
Multi-class evaluation: macro, micro, dan weighted average
Ketika kelas lebih dari dua (multi-class), metrik precision, recall, dan F1 perlu dirata-ratakan. Tetapi cara merata-ratakannya menghasilkan angka yang sangat berbeda:
from sklearn.metrics import classification_report
import numpy as np
# Tiga kelas: kucing, anjing, burung
# Dataset imbalance: 100 kucing, 30 anjing, 10 burung
y_true = (["kucing"]*80 + ["kucing"]*20 +
["anjing"]*20 + ["anjing"]*10 +
["burung"]*5 + ["burung"]*5)
y_pred = (["kucing"]*75 + ["anjing"]*5 +
["anjing"]*25 + ["kucing"]*5 +
["burung"]*3 + ["kucing"]*2)
print(classification_report(y_true, y_pred))
# Perhatikan tiga baris rata-rata di bawah:
# macro avg: rata-rata tanpa bobot — semua kelas sama
# weighted avg: bobot berdasarkan jumlah sampel per kelas
# micro avg: hitung TP/FP/FN global dulu, baru bagiMacro average memberi bobot sama ke semua kelas — kelas minoritas (burung, 10 sampel) sama pentingnya dengan kelas mayoritas (kucing, 100 sampel). Weighted average mempertimbangkan jumlah sampel, sehingga performa pada kelas mayoritas mendominasi. Micro average menghitung total TP, FP, FN secara global sebelum menghitung metrik — hasilnya identik dengan accuracy pada multi-class. Pilihan saya: gunakan macro saat semua kelas harus diperlakukan sama, weighted saat kelas mayoritas memang lebih relevan.
Cross-validation: tidak percaya satu split
Pembelajaran terpenting dari episode ini: jangan pernah mengevaluasi model hanya pada satu train-test split. Angka bisa beruntung (atau sial) pada split tertentu. Cross-validation memecah data menjadi k lipatan (folds), lalu melatih dan mengevaluasi k kali — setiap kali menggunakan lipatan yang berbeda sebagai test set. Rata-ratanya jauh lebih stabil dan lebih dekat ke performa sebenarnya di data baru.
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
# Dataset sampel
X, y = make_classification(n_samples=500, n_features=20,
weights=[0.9, 0.1], random_state=42)
model = RandomForestClassifier(n_estimators=100, random_state=42)
# 5-fold cross-validation
scores = cross_val_score(model, X, y, cv=5, scoring="f1")
print(f"F1 per fold: {scores}")
print(f"F1 rata-rata: {scores.mean():.3f} ± {scores.std():.3f}")
# Standar deviasi rendah = model konsisten
# Standar deviasi tinggi = model tidak stabil, perlu investigasiCross-validation memberi kepercayaan diri lebih dari sekadar angka pada satu split. Ketika std tinggi, itu sinyal model bergantung pada data spesifik — atau ada masalah lain yang perlu dikejar sebelum produksi.
Pitfall: data leakage saat evaluasi
Kesalahan paling berbahaya dalam evaluasi model: data leakage. Ini terjadi ketika informasi dari test set bocor ke proses training atau preprocessing. Contoh paling umum yang pernah saya alami: melakukan feature scaling pada seluruh dataset sebelum split, bukan hanya pada training set. Statistik normalisasi (mean, std) dihitung dari data yang nantinya jadi test set — sehingga model secara tidak langsung "melihat" data test.
# ❌ SALAH — data leakage!
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # fit pada SELURUH data
X_train, X_test = X_scaled[:400], X_scaled[400:] # leak!
# ✅ BENAR — fit hanya pada training set
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train) # fit di train saja
X_test = scaler.transform(X_test) # transform saja, jangan fitPitfall lain yang perlu diwaspadai: fitur yang merupakan transformasi dari target (misalnya rasio yang melibatkan label), data temporal yang tidak diurutkan dengan benar sebelum split, atau penggunaan test set untuk hyperparameter tuning. Semua ini membuat angka evaluasi terlihat terlalu bagus — tetapi model akan mengecewakan saat dihadapkan data benar-benar baru. Aturan emas saya: split dulu, baru preprocessing. Tidak ada pengecualian.
Tabel ringkasan: kapan pakai metrik mana
| Metrik | Kapan dipakai | Kelemahan |
|---|---|---|
| Accuracy | Kelas seimbang, semua kesalahan sama harganya | Menipu pada dataset imbalance |
| Precision | False positive mahal (spam, rekomendasi hukum) | Ignorance terhadap kasus yang lolos |
| Recall | False negative mahal (penyakit, fraud) | Bisa rendah kalau terlalu banyak alarm palsu |
| F1 Score | Butuh satu angka ringkas, trade-off precision-recall | Tidak menjelaskan precision atau recall secara terpisah |
| ROC AUC | Membandingkan model berdasarkan probabilitas | Misleading pada dataset sangat imbalance |
| PR AUC | Dataset imbalance, fokus pada kelas minoritas | Threshold-dependent, sulit dibandingkan antar-model |
Catatan untuk diri sendiri
Kesalahan terbesar yang saya hindari mulai sekarang: melaporkan satu angka tanpa konteks. Kebiasaan baru saya — selalu tulis confusion matrix-nya, minimal di catatan pribadi. Empat kotak itu memaksa saya jujur soal jenis kesalahan yang model lakukan, dan sering kali justru di situ insight bisnisnya muncul.
Pelajaran personal yang paling membekas: ketika saya pertama kali melihat accuracy 95% pada model deteksi anomali, saya merasa sudah selesai. Ternyata, 5% kesalahan itu sebagian besar adalah false negative — anomali sungguhan yang lolos tanpa deteksi. Sejak saat itu, saya tidak pernah lagi melaporkan angka accuracy tanpa menyertakan confusion matrix di belakangnya. Empat kotak kecil itu jujur — jauh lebih jujur dari satu angka yang terlihat membanggakan.
Komentar Terbaru