Confusion matrix memberi empat angka; bab ini mengolahnya menjadi tiga metrik yang menjawab pertanyaan bisnis secara langsung. Precision menjawab “dari semua yang model tandai positif, berapa yang benar?” — pertanyaan pemilik filter spam yang tidak boleh menelan surel penting. Recall menjawab “dari semua yang benar-benar positif, berapa yang tertangkap?” — pertanyaan tim skrining medis yang tidak boleh meloloskan pasien berisiko. Keduanya hampir selalu tarik-menarik: menaikkan ambang keputusan menaikkan precision dan menurunkan recall. F1-score merangkum keduanya dalam satu angka yang menghukum ketimpangan. Chapter ini menghitung ketiganya manual lalu membandingkannya dengan API scikit-learn, membaca classification_report baris demi baris, dan memperlihatkan tarik-menarik precision-recall dalam tabel angka.

Precision: Kualitas Alarm #

Precision didefinisikan dari dua sel confusion matrix:

text
precision = TP / (TP + FP)

Pembaginya adalah jumlah seluruh prediksi positif — semua alarm yang model bunyikan. Precision rendah berarti alarm sering palsu. Kasus klasiknya filter spam yang terlalu galak: precision 0,60 berarti 40% dari surel yang dibuang sebenarnya sah, termasuk tawaran kerja, kwitansi pembelian, atau kabar keluarga yang tidak pernah terbaca. Ketika biaya false positive tinggi — surel penting hilang, nasabah jujur terhenti — precision adalah metrik yang dipertaruhkan.

Recall: Cakupan Kasus Positif #

Recall (disebut juga sensitivity atau true positive rate) menghitung dari sisi kebenaran:

text
recall = TP / (TP + FN)

Pembaginya adalah jumlah seluruh kasus positif yang sebenarnya — semua kejadian yang seharusnya tertangkap. Recall rendah berarti banyak kasus lolos. Pada skrining medis, recall 0,60 berarti 40% pasien berisiko tidak terdeteksi dan tidak ditindaklanjuti; pada deteksi penipuan, berarti sebagian kerugian berjalan tanpa peringatan. Ketika biaya false negative tinggi, recall-lah metrik utamanya.

F1-Score dan Tarik-Menarik Precision-Recall #

Precision dan recall saling bersaing karena keduanya dikendalikan oleh satu tombol yang sama: ambang keputusan. predict() pada classifier probabilistik bekerja dengan aturan sederhana — kelas positif bila probabilitas ≥ 0,5. Naikkan ambang menjadi 0,7: hanya kasus yang sangat yakin yang dibunyikan, alarm makin jarang tapi makin tepat — precision naik, recall turun karena kasus meragukan dibiarkan lolos. Turunkan ambang menjadi 0,3: jaring makin lebar, recall naik, tetapi precision turun karena banyak alarm palsu. Tidak ada ambang yang memaksimalkan keduanya sekaligus; posisi terbaik ditentukan oleh rasio biaya FP dan FN di bisnis Anda.

F1-score adalah rata-rata harmonik dari keduanya:

text
F1 = 2 x precision x recall / (precision + recall)

Rata-rata harmonik “menghukum” ketimpangan: precision 1,00 dan recall 0,00 menghasilkan F1 0,00 meskipun rata-rata biasanya 0,50. Model yang mengejar F1 tinggi dipaksa menjaga kedua sisi. Varian F-beta (fbeta_score dengan beta=2) memberi bobot lebih pada recall — berguna ketika FN lebih mahal lagi, seperti skrining penyakit serius yang ditindaklanjuti pemeriksaan lanjutan murah.

Menghitung Ketiganya dari Nol #

Kita pakai kembali 30 transaksi dari kisah deteksi penipuan: 27 sah, 3 penipuan. Model menandai 4 transaksi sebagai penipuan — 2 benar (TP), 2 keliru (FP) — dan meloloskan 1 penipuan (FN). Ketiga metrik dihitung manual dari sel matriks, lalu dibandingkan dengan fungsi scikit-learn; laporan classification_report dicetak untuk melihat format baku industri.

python
# Precision, recall, dan F1: tiga sudut pandang atas kelas positif,
# lalu demo ambang keputusan (threshold) yang menggeser keduanya.

import numpy as np
from sklearn.datasets import make_classification
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

# --- Bagian 1: 30 transaksi tetap, dihitung manual lalu lewat API ---
# 1 = penipuan (positif), 0 = sah. Model menandai 4 transaksi sebagai
# penipuan: 2 benar (TP), 2 keliru (FP); 1 penipuan lolos (FN).
y_true = np.array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0,
                   0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
                   0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
y_pred = np.array([1, 0, 0, 0, 0, 0, 0, 1, 0, 0,
                   0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
                   0, 0, 1, 0, 0, 0, 0, 0, 0, 0])

TP = int(np.sum((y_true == 1) & (y_pred == 1)))
FP = int(np.sum((y_true == 0) & (y_pred == 1)))
FN = int(np.sum((y_true == 1) & (y_pred == 0)))
TN = int(np.sum((y_true == 0) & (y_pred == 0)))
print(f"TP = {TP} , FP = {FP} , FN = {FN} , TN = {TN}")

presisi_manual = TP / (TP + FP)
recall_manual = TP / (TP + FN)
f1_manual = 2 * presisi_manual * recall_manual / (presisi_manual + recall_manual)

print(f"Presisi manual = {presisi_manual:.3f} , "
      f"precision_score = {precision_score(y_true, y_pred):.3f}")
print(f"Recall  manual = {recall_manual:.3f} , "
      f"recall_score    = {recall_score(y_true, y_pred):.3f}")
print(f"F1      manual = {f1_manual:.3f} , "
      f"f1_score        = {f1_score(y_true, y_pred):.3f}")
print(f"Akurasi        = {accuracy_score(y_true, y_pred):.3f} "
      f"({TN + TP} benar dari {TN + TP + FP + FN})")

print()
print(classification_report(y_true, y_pred, target_names=["sah", "penipuan"],
                            digits=3))

# --- Bagian 2: ambang keputusan menggeser presisi vs recall ---
X, y = make_classification(
    n_samples=150,
    n_features=8,
    n_informative=6,
    n_redundant=1,
    weights=[0.85, 0.15],
    class_sep=1.0,
    flip_y=0.0,
    random_state=42,
)
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.3, stratify=y, random_state=42
)
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_latih, y_latih)
proba = model.predict_proba(X_uji)[:, 1]  # probabilitas kelas penipuan

print("Ambang | Presisi | Recall | Akurasi")
for ambang in (0.3, 0.5, 0.7):
    y_pred_ambang = (proba >= ambang).astype(int)
    p = precision_score(y_uji, y_pred_ambang, zero_division=0)
    r = recall_score(y_uji, y_pred_ambang, zero_division=0)
    a = accuracy_score(y_uji, y_pred_ambang)
    print(f"  {ambang:.1f}  |  {p:.3f}  |  {r:.3f}  |  {a:.3f}")

Output yang Dihasilkan:

text
TP = 2 , FP = 2 , FN = 1 , TN = 25
Presisi manual = 0.500 , precision_score = 0.500
Recall  manual = 0.667 , recall_score    = 0.667
F1      manual = 0.571 , f1_score        = 0.571
Akurasi        = 0.900 (27 benar dari 30)

              precision    recall  f1-score   support

         sah      0.962     0.926     0.943        27
    penipuan      0.500     0.667     0.571         3

    accuracy                          0.900        30
   macro avg      0.731     0.796     0.757        30
weighted avg      0.915     0.900     0.906        30

Ambang | Presisi | Recall | Akurasi
  0.3  |  0.600  |  0.857  |  0.889
  0.5  |  0.833  |  0.714  |  0.933
  0.7  |  1.000  |  0.429  |  0.911

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 26–29 (TP, FP, FN, TN): keempat sel dihitung dengan topeng boolean langsung dari pasangan label — fondasi yang sama dengan Chapter Confusion Matrix.
  2. Baris 32–34 (presisi_manual dst.): ketiga rumus diterjemahkan apa adanya ke Python. Hasil 0.500, 0.667, dan 0.571 identik dengan keluaran precision_score, recall_score, dan f1_score — bukti bahwa metrik ini bukan kotak hitam.
  3. Baris 46–47 (classification_report): laporan baku yang merangkum precision, recall, F1, dan support (jumlah sampel aktual per kelas) untuk tiap kelas beserta dua jenis rata-rata.
  4. Baris 50–65 (bagian 2): dataset 150 sampel dengan 15% penipuan dibelah secara stratified, lalu predict_proba(...)[:, 1] mengambil probabilitas kelas penipuan untuk tiap transaksi uji.
  5. Baris 68–73 (for ambang in (0.3, 0.5, 0.7)): alih-alih predict(), prediksi dibuat manual dengan proba >= ambang sehingga kita berkuasa penuh atas ambangnya.

Bacaan output bagian pertama: presisi 0.500 (dari 4 alarm, hanya 2 yang benar), recall 0.667 (dari 3 penipuan, 2 tertangkap), F1 0.571. Perhatikan pula akurasi 0.900 — angka yang terdengar sangat baik untuk model yang melewatkan sepertiga penipuan. Ketimpangan antara akurasi dan F1 inilah alarm pertama bahwa satu metrik saja tidak cukup.

Membaca classification_report Baris demi Baris #

Laporan pada output memuat lima baris yang masing-masing perlu dibaca:

  • Baris sah: kelas negatif diperlakukan sebagai “positif” sementara — precision 0,962 berarti 25 dari 26 transaksi yang diloloskan memang sah. Kelas mayoritas hampir selalu tampak sehat seperti ini.
  • Baris penipuan: baris yang sebenarnya kita perduli. Precision 0,500, recall 0,667, F1 0,571 dengan support hanya 3 — angka pada kelas langka mudah berayun karena satu sampel menggeser nilai jauh.
  • Baris accuracy: 0,900 — tinggi, tetapi seperti dibahas, menyembunyikan kegagalan pada kelas yang penting.
  • Baris macro avg: rata-rata sederhana kedua kelas (0,731 untuk precision) — memperlakukan kelas langka sama penting dengan kelas mayoritas. Gunakan ini ketika kelas langka justru fokus bisnis.
  • Baris weighted avg: rata-rata berbobot support (0,915 untuk precision) — condong ke kelas mayoritas dan cenderung meniru akurasi pada data timpang.

Perbedaan macro dan weighted pada laporan kita (0,731 vs 0,915) adalah indikator ketidakseimbangan kelas yang cepat: makin lebar selisihnya, makin timpang perhatian model antar kelas.

Tarik-Menarik dalam Tabel Angka #

Bagian kedua output menunjukkan see-saw-nya secara kuantitatif. Ambang 0,3 menghasilkan recall 0,857 (6 dari 7 penipuan tertangkap) dengan precision 0,600 — 4 dari 10 alarm palsu. Ambang 0,7 menghasilkan precision 1,000 (semua alarm valid) tetapi recall jatuh ke 0,429 — hanya 3 dari 7 penipuan tertangkap. Perhatikan juga bahwa akurasi tidak mengikuti pola mana pun secara informatif (0,889 → 0,933 → 0,911): ambang terbaik menurut akurasi bukan ambang terbaik menurut biaya bisnis. Pilihan ambang adalah keputusan bisnis yang diterjemahkan ke angka, bukan keputusan teknis yang diambil dari default 0,5.

Satu pertanyaan masih terbuka: bagaimana menilai model pada seluruh ambang sekaligus, tanpa harus memilih salah satu? Jawabannya adalah kurva ROC dan luas AUC — pokok bahasan Chapter ROC Curve dan AUC.

Referensi Resmi #