Angka akurasi 0,95 terdengar seperti kabar baik — sampai kita sadar model tersebut bisa meraihnya tanpa belajar apa pun. Bila 95 persen data berada di satu kelas, model yang selalu menebak kelas mayoritas otomatis mencetak akurasi 0,95 sambil mengabaikan kelas minoritas yang justru menjadi alasan model dibuat: transaksi penipuan, kelainan pada hasil pemeriksaan, atau pelanggan yang hendak berhenti berlangganan. Chapter ini membedah mengapa akurasi menipu pada data tak seimbang, lalu membangun tiga metrik yang lebih jujur — presisi, recall, dan F1 — beserta cara membaca trade-off keduanya lewat ambang keputusan. Kita lanjutkan alat baca classification_report dari chapter Evaluasi Model
dengan eksperimen nyata yang dapat diulang pada CPU (scikit-learn 1.5.2).
Empat Kotak Dasar Sebelum Menghitung Metrik #
Setiap prediksi biner jatuh ke salah satu dari empat kotak, dibentuk oleh label aktual dan label prediksi. Empat kotak inilah bahan baku semua metrik klasifikasi:
| Istilah | Label aktual | Prediksi model | Arti |
|---|---|---|---|
| TP (true positive) | positif | positif | kasus positif yang berhasil ditangkap |
| FP (false positive) | negatif | positif | alarm palsu |
| FN (false negative) | positif | negatif | kasus positif yang terlewat |
| TN (true negative) | negatif | negatif | negatif yang benar dilewati |
Akurasi dihitung sebagai (TP + TN) / total. Pada data 95:5, kotak TN membanjiri pembilang — model yang menebak negatif sepanjang waktu memperoleh 0,95 dari TN semata, tanpa satu pun TP. Akurasi hanya aman ketika kelas seimbang dan biaya kedua jenis kesalahan setara; untuk kasus lain kita memerlukan metrik yang membaca TP secara langsung.
Presisi, Recall, dan F1 #
Presisi TP / (TP + FP) menjawab: dari semua sampel yang diprediksi positif, berapa proporsi yang memang positif? Recall TP / (TP + FN) menjawab: dari semua sampel yang benar-benar positif, berapa proporsi yang tertangkap? F1 adalah rata-rata harmonik keduanya, 2PR / (P + R) — rata-rata harmonik menghukum ketimpangan, sehingga F1 hanya tinggi bila presisi dan recall sama-sama tinggi.
Konsekuensi bisnis dari FP dan FN menentukan pilihan. Pada filter spam, FP berarti email penting tersaring ke folder sampah — presisi diutamakan. Pada skrining medis awal, FN berarti penyakit terlewat padahal pemeriksaan lanjutan relatif murah — recall diutamakan dan alarm palsu lebih ditoleransi. Bila kedua jenis kesalahan sama-sama mahal, F1 menjadi kompromi yang wajar.
Eksperimen: Jebakan Akurasi yang Terukur #
Berkas metrik_imbalance.py membangun dataset biner 95:5, lalu membandingkan dua model di atas 800 sampel uji: pembanding yang selalu menebak kelas mayoritas, dan logistic regression yang belajar sungguh-sungguh. Skrip yang sama menggeser ambang probabilitas lalu menguji strategi rata-rata pada tiga kelas:
"""Jebakan akurasi pada kelas tak seimbang, dan metrik penyelamatnya.
Alur: bangun dataset biner 95:5 -> bandingkan model "selalu tebak kelas
mayoritas" dengan logistic regression -> jelajahi trade-off
precision-recall lewat ambang probabilitas -> bandingkan strategi
rata-rata macro/micro/weighted, termasuk pada kasus tiga kelas.
"""
from sklearn.datasets import make_classification
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
accuracy_score,
classification_report,
f1_score,
precision_score,
recall_score,
)
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# ---------- 1. Dataset biner tak seimbang: 95% negatif, 5% positif ----------
X, y = make_classification(
n_samples=2000,
n_features=10,
n_informative=6,
n_redundant=2,
weights=[0.95, 0.05],
flip_y=0.02,
class_sep=1.5,
random_state=42,
)
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, y, test_size=0.4, random_state=42, stratify=y
)
print("== dataset uji: 800 sampel ==")
print("negatif (kelas 0):", int((y_uji == 0).sum()),
"| positif (kelas 1):", int((y_uji == 1).sum()))
# ---------- 2. Model pembanding: selalu menebak kelas mayoritas ----------
mayoritas = DummyClassifier(strategy="most_frequent")
mayoritas.fit(X_latih, y_latih)
pred_mayoritas = mayoritas.predict(X_uji)
# ---------- 3. Model nyata: logistic regression + standardisasi ----------
skala = StandardScaler().fit(X_latih)
model = LogisticRegression(max_iter=1000)
model.fit(skala.transform(X_latih), y_latih)
pred = model.predict(skala.transform(X_uji))
print("\n== akurasi menipu ==")
print(f"{'model':28s} | {'akurasi':>7s} | {'presisi':>7s} | "
f"{'recall':>6s} | {'F1':>5s}")
for nama, p in (("selalu kelas mayoritas", pred_mayoritas), ("logistic regression", pred)):
print(f"{nama:28s} | {accuracy_score(y_uji, p):7.3f} | "
f"{precision_score(y_uji, p, zero_division=0):7.3f} | "
f"{recall_score(y_uji, p, zero_division=0):6.3f} | "
f"{f1_score(y_uji, p, zero_division=0):5.3f}")
# ---------- 4. Trade-off precision-recall lewat ambang probabilitas ----------
proba = model.predict_proba(skala.transform(X_uji))[:, 1]
print("\n== geser ambang, geser keseimbangan precision-recall ==")
print(f"{'ambang':>6s} | {'presisi':>7s} | {'recall':>6s} | {'F1':>5s} | "
f"{'prediksi positif':>16s}")
for ambang in (0.10, 0.30, 0.50, 0.70, 0.90):
pred_a = (proba >= ambang).astype(int)
print(f"{ambang:6.2f} | {precision_score(y_uji, pred_a, zero_division=0):7.3f} | "
f"{recall_score(y_uji, pred_a, zero_division=0):6.3f} | "
f"{f1_score(y_uji, pred_a, zero_division=0):5.3f} | "
f"{int(pred_a.sum()):16d}")
# ---------- 5. Strategi rata-rata pada dataset biner ----------
print("\n== strategi rata-rata F1 (logistic regression) ==")
print("macro :", round(f1_score(y_uji, pred, average="macro"), 3))
print("micro :", round(f1_score(y_uji, pred, average="micro"), 3))
print("weighted:", round(f1_score(y_uji, pred, average="weighted"), 3))
print("akurasi :", round(accuracy_score(y_uji, pred), 3),
"(micro F1 selalu sama dengan akurasi)")
# ---------- 6. Strategi rata-rata pada tiga kelas tak seimbang ----------
X3, y3 = make_classification(
n_samples=1500,
n_features=8,
n_informative=5,
n_classes=3,
weights=[0.80, 0.15, 0.05],
class_sep=1.4,
random_state=42,
)
X3_latih, X3_uji, y3_latih, y3_uji = train_test_split(
X3, y3, test_size=0.4, random_state=42, stratify=y3
)
skala3 = StandardScaler().fit(X3_latih)
model3 = LogisticRegression(max_iter=1000)
model3.fit(skala3.transform(X3_latih), y3_latih)
pred3 = model3.predict(skala3.transform(X3_uji))
print("\n== tiga kelas (80/15/5): F1 per kelas vs rata-rata ==")
print(classification_report(y3_uji, pred3, digits=3, zero_division=0))
print("F1 macro :", round(f1_score(y3_uji, pred3, average="macro"), 3))
print("F1 micro :", round(f1_score(y3_uji, pred3, average="micro"), 3))
print("F1 weighted:", round(f1_score(y3_uji, pred3, average="weighted"), 3))Skrip di atas menghasilkan keluaran berikut saat dijalankan di mesin uji. Amati baris pertama tabel “akurasi menipu” sebelum membaca sisanya.
Output yang Dihasilkan:
== dataset uji: 800 sampel ==
negatif (kelas 0): 751 | positif (kelas 1): 49
== akurasi menipu ==
model | akurasi | presisi | recall | F1
selalu kelas mayoritas | 0.939 | 0.000 | 0.000 | 0.000
logistic regression | 0.966 | 0.923 | 0.490 | 0.640
== geser ambang, geser keseimbangan precision-recall ==
ambang | presisi | recall | F1 | prediksi positif
0.10 | 0.357 | 0.714 | 0.476 | 98
0.30 | 0.769 | 0.612 | 0.682 | 39
0.50 | 0.923 | 0.490 | 0.640 | 26
0.70 | 1.000 | 0.367 | 0.537 | 18
0.90 | 1.000 | 0.245 | 0.393 | 12
== strategi rata-rata F1 (logistic regression) ==
macro : 0.811
micro : 0.966
weighted: 0.961
akurasi : 0.966 (micro F1 selalu sama dengan akurasi)
== tiga kelas (80/15/5): F1 per kelas vs rata-rata ==
precision recall f1-score support
0 0.935 0.966 0.951 477
1 0.728 0.824 0.773 91
2 0.250 0.031 0.056 32
accuracy 0.895 600
macro avg 0.638 0.607 0.593 600
weighted avg 0.867 0.895 0.876 600
F1 macro : 0.593
F1 micro : 0.895
F1 weighted: 0.876Mari kita telaah kode di atas baris per baris:
- Baris 24-36 (dataset tak seimbang):
weights=[0.95, 0.05]membuat kelas positif hanya 5 persen;flip_y=0.02menyisipkan noise label;stratify=ymenjaga rasio 95:5 di data uji (751 berbanding 49). - Baris 42-44 (model pembanding):
DummyClassifier(strategy="most_frequent")tidak melihat fitur sama sekali — hanya menebak kelas terbanyak; baseline inilah yang wajib dikalahkan. - Baris 46-49 (model nyata):
StandardScalerdi-fit pada data latih saja untuk mencegah kebocoran statistik, lalu logistic regression dilatih pada fitur terstandar. - Baris 51-58 (tabel perbandingan): kedua model dievaluasi dengan empat metrik;
zero_division=0menjaga presisi tetap terdefinisi (bernilai 0) ketika model tidak pernah memprediksi positif. - Baris 61-70 (geser ambang):
predict_proba(...)[:, 1]mengambil probabilitas kelas positif; prediksi dibentuk manual lewatproba >= ambangsehingga ambang bebas dipilih di luar bawaan 0,50. - Baris 74-78 (strategi rata-rata): parameter
averagemenyiapkan perbandingan tiga cara merangkum F1 menjadi satu angka. - Baris 81-99 (tiga kelas): dataset kedua dengan rasio 80:15:5 menguji strategi rata-rata ketika kelas lemah bukan sekadar kelas kecil;
classification_reportmencetak metrik per kelas.
Membaca Hasil: Akurasi Nyaris Diam, Metrik Lain Berteriak #
Tabel “akurasi menipu” memperlihatkan inti masalahnya. Model kelas mayoritas mencetak akurasi 0,939 — angka yang lolos rapat evaluasi tanpa pembanding — padahal presisi, recall, dan F1 semuanya 0,000: dari 49 kasus positif, nol tertangkap. Logistic regression “hanya” menaikkan akurasi 0,027 poin ke 0,966, tetapi recall melompat ke 0,490 dan F1 ke 0,640. Selisih akurasi yang sempit itulah alasan akurasi saja tidak cukup untuk menilai kelayakan model.
Tabel ambang memperlihatkan trade-off struktural, bukan kebetulan. Ambang 0,10 menangkap 71,4 persen kasus positif, tetapi hanya 35,7 persen prediksi positif yang benar; ambang 0,90 menghasilkan presisi sempurna 1,000 dengan recall jatuh ke 0,245. Menurunkan ambang selalu menambah TP bersama FP; menaikannya memangkas keduanya. F1 memuncak 0,682 di ambang 0,30 — melampaui bawaan 0,50 — sementara kolom terakhir menghitung beban operasional: banyaknya sampel yang harus ditindaklanjuti.
Macro, Micro, atau Weighted #
Bagian tiga kelas menunjukkan mengapa pilihan strategi rata-rata adalah keputusan. F1 kelas 2 hanya 0,056 dari 32 sampel: rata-rata macro menghitung ketiga kelas setara sehingga tertarik ke 0,593 — kelas lemah terlihat jelas; weighted membobot support sehingga kelas 0 (477 sampel) mendominasi hasil 0,876 — kelas lemah tersembunyi; micro mengagregasi seluruh keputusan sebelum dibagi — pada single-label hasilnya identik dengan akurasi (0,895).
Sebagai rangkuman, tabel berikut memetakan tiap metrik ke rumus dan situasi yang menjadikannya penting:
| Metrik | Rumus | Paling penting saat |
|---|---|---|
| Akurasi | (TP + TN) / total | kelas seimbang, biaya kesalahan setara |
| Presisi | TP / (TP + FP) | alarm palsu mahal — filter spam, moderasi otomatis |
| Recall | TP / (TP + FN) | kasus terlewat mahal — skrining medis, deteksi penipuan |
| F1 | 2PR / (P + R) | kedua jenis kesalahan sama-sama perlu dikawal |
| F1 macro | rata-rata sederhana antar kelas | setiap kelas harus terlihat setara |
| F1 micro | agregat global TP, FP, FN | multi-label; pada single-label identik akurasi |
| F1 weighted | rata-rata berbobot support | ringkasan satu angka yang mencerminkan distribusi data |
Rumus lengkap tiap metrik dirinci pada dokumentasi resmi scikit-learn.org/stable/modules/model_evaluation.html . Regresi memprediksi angka kontinu dan menuntut pengukuran galat yang berbeda — chapter berikutnya, Metrik Regresi , membongkar MAE, MSE, RMSE, dan R², termasuk bagaimana satu pencilan menggeser tiap metrik dengan porsi yang jauh berbeda.