Data nyata jarang seimbang: hanya sebagian kecil transaksi yang benar-benar penipuan, hanya beberapa persen pelanggan yang berhenti berlangganan tiap bulan, dan sebagian besar hasil pemeriksaan medis bersifat normal. Pada situasi 90% lawan 10%, model yang belajar dari data mentah menemukan jalan pintas yang menguntungkan secara angka: selalu tebak kelas mayoritas. Akurasi tampak tinggi (90%!), padahal model tersebut gagal total pada kelas yang justru menjadi alasan proyek dibuat. Chapter ini membahas cara mendiagnosis ketidakseimbangan, tiga penanganan utama — class_weight="balanced", resampling, dan SMOTE dari pustaka imbalanced-learn — serta satu aturan mutlak: data uji tidak pernah boleh di-resample.

Diagnosis Sebelum Penanganan #

Langkah pertama adalah mengukur, bukan menduga. np.bincount(y) atau value_counts() pada target langsung memperlihatkan komposisi kelas; angka 180 berbanding 20 pada contoh kita berarti ketimpangan 9:1. Pemisahan data latih dan uji wajib memakai stratify=y agar proporsi ketimpangan tetap terjaga di kedua sisi — tanpa stratifikasi, data uji berisi 60 baris bisa saja kehilangan seluruh kelas minoritas karena kebetulan acak. Patokan berikutnya adalah baseline: DummyClassifier(strategy="most_frequent") yang selalu menebak kelas mayoritas. Model sungguhan apa pun harus dibandingkan terhadap baseline ini; model yang akurasinya 0,92 sementara baseline mencapai 0,90 hanya memberi nilai tambah dua persen — dan bila nilai tambahnya justru berasal dari menekan kelas minoritas, model tersebut lebih buruk daripada baseline.

Inti masalahnya adalah sinyal belajar yang timpang: fungsi loss menjumlahkan kontribusi seluruh sampel, sehingga 126 sampel mayoritas berbicara sembilan kali lebih keras daripada 14 sampel minoritas. Seluruh penanganan pada chapter ini bekerja dengan menyeimbangkan suara tersebut — entah memberi bobot lebih pada minoritas, atau menambah jumlah sampelnya.

class_weight=“balanced”: Bobot Tanpa Mengubah Data #

Cara termurah dan pertama yang layak dicoba: LogisticRegression(class_weight="balanced"). Mekanismenya sederhana — setiap kelas diberi bobot berbanding terbalik dengan frekuensinya (n / (jumlah kelas × jumlah sampel kelas tersebut)), lalu bobot tersebut masuk ke perhitungan loss. Kesalahan pada satu sampel minoritas dihukum sembilan kali lebih berat daripada kesalahan pada satu sampel mayoritas, sehingga gradien pembelajaran berhenti berpihak pada mayoritas. Tidak ada data yang dibuang atau digandakan; data latih tetap utuh, dan penanganan ini tersedia pada sebagian besar classifier scikit-learn.

Resampling: Undersampling dan Oversampling SMOTE #

Jika pembobotan belum cukup, data latih sendiri bisa diseimbangkan. Undersampling membuang sebagian sampel mayoritas secara acak hingga jumlahnya menyamai minoritas (126 menjadi 14 pada contoh kita). Harganya jelas: 112 sampel mayoritas yang membawa informasi ikut dibuang — pendekatan ini layak hanya jika data mayoritas sangat berlimpah dan redundan. Oversampling bekerja ke arah sebaliknya, dan versi paling populer adalah SMOTE (Synthetic Minority Oversampling Technique) dari pustaka imbalanced-learn. Alih-alih menduplikasi sampel minoritas yang sama (yang membuat model menghafal baris tertentu), SMOTE mensintesis sampel baru: pilih satu sampel minoritas, cari tetangga minoritas terdekatnya (k_neighbors), lalu buat titik baru pada ruas garis di antara keduanya — interpolasi ber atribut demi atribut. Sampel sintetis mengisi wilayah kelas minoritas sehingga batas keputusan tidak lagi menempel pada segelintir titik. Risiko yang perlu diwaspadai: bila kelas minoritas dan mayoritas tumpang tindih di ruang fitur, interpolasi bisa menempatkan sampel sintetis di wilayah mayoritas — kualitas tetangga menentukan kualitas SMOTE.

PenangananCara kerjaKelebihanRisiko
class_weight="balanced"Bobot kelas pada lossTanpa mengubah data; satu baris kodeRecall naik, presisi biasanya turun
UndersamplingBuang sampel mayoritasCepat; batas kelas lebih jelasMembuang informasi mayoritas
SMOTESintesis sampel minoritasVariasi baru, bukan duplikatInterpolasi bisa menabrak wilayah mayoritas
Penyesuaian thresholdGeser ambang keputusanTanpa pelatihan ulangDibahas pada Section evaluasi

Contoh Kode: Empat Penanganan pada Data 90:10 #

Contoh berikut membangun 200 sampel berlabel 180:20, membandingkan lima model pada data uji yang sama — dummy, regresi logistik biasa, class_weight, undersampling, dan SMOTE — dengan recall kelas minoritas sebagai metrik utama.

python
# Dampak ketidakseimbangan kelas (90% vs 10%) dan empat penanganan:
# baseline dummy, class_weight="balanced", undersampling, dan SMOTE.
# Evaluasi memakai recall kelas minoritas agar jebakan akurasi terlihat.
# Resampling HANYA diterapkan pada data latih, tidak pernah data uji.

import numpy as np
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler
from sklearn.datasets import make_classification
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score
from sklearn.model_selection import train_test_split

X, y = make_classification(
    n_samples=200,
    n_features=4,
    n_informative=3,
    n_redundant=1,
    weights=[0.9, 0.1],
    flip_y=0,
    class_sep=0.8,
    random_state=42,
)

print("== Diagnosis: jumlah data per kelas ==")
print("seluruh data :", np.bincount(y).tolist(), "(0 = bertahan, 1 = berhenti)")

X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.3, stratify=y, random_state=42
)
print("data latih   :", np.bincount(y_latih).tolist())
print("data uji     :", np.bincount(y_uji).tolist())


def evaluasi(nama, model):
    prediksi = model.predict(X_uji)
    akurasi = accuracy_score(y_uji, prediksi)
    recall = recall_score(y_uji, prediksi)
    print(f"{nama:28s} akurasi {akurasi:.3f}   recall kelas 1 {recall:.3f}")


print("\n== Perbandingan model (data uji tidak pernah di-resample) ==")
dummy = DummyClassifier(strategy="most_frequent").fit(X_latih, y_latih)
evaluasi("DummyClassifier (mayoritas)", dummy)

model_biasa = LogisticRegression(random_state=42).fit(X_latih, y_latih)
evaluasi("LogisticRegression biasa", model_biasa)

model_balanced = LogisticRegression(
    class_weight="balanced", random_state=42
).fit(X_latih, y_latih)
evaluasi("LogisticRegression balanced", model_balanced)

# --- Undersampling mayoritas (hanya pada data latih) ---
under = RandomUnderSampler(random_state=42)
X_under, y_under = under.fit_resample(X_latih, y_latih)
model_under = LogisticRegression(random_state=42).fit(X_under, y_under)
evaluasi("LR + undersampling (latih)", model_under)

# --- SMOTE: sintesis sampel minoritas lewat interpolasi tetangga ---
smote = SMOTE(random_state=42, k_neighbors=3)
X_smote, y_smote = smote.fit_resample(X_latih, y_latih)
model_smote = LogisticRegression(random_state=42).fit(X_smote, y_smote)
evaluasi("LR + SMOTE (latih)", model_smote)

print("\n== Jumlah data latih per kelas: sebelum -> sesudah resampling ==")
print(
    "undersampling :",
    np.bincount(y_latih).tolist(),
    "->",
    np.bincount(y_under).tolist(),
)
print(
    "SMOTE         :",
    np.bincount(y_latih).tolist(),
    "->",
    np.bincount(y_smote).tolist(),
)

Output yang Dihasilkan:

text
== Diagnosis: jumlah data per kelas ==
seluruh data : [180, 20] (0 = bertahan, 1 = berhenti)
data latih   : [126, 14]
data uji     : [54, 6]

== Perbandingan model (data uji tidak pernah di-resample) ==
DummyClassifier (mayoritas)  akurasi 0.900   recall kelas 1 0.000
LogisticRegression biasa     akurasi 0.950   recall kelas 1 0.500
LogisticRegression balanced  akurasi 0.900   recall kelas 1 0.833
LR + undersampling (latih)   akurasi 0.800   recall kelas 1 0.667
LR + SMOTE (latih)           akurasi 0.933   recall kelas 1 0.833

== Jumlah data latih per kelas: sebelum -> sesudah resampling ==
undersampling : [126, 14] -> [14, 14]
SMOTE         : [126, 14] -> [126, 126]

Bedah Kode Baris-per-Baris #

  1. Baris 15–24 (make_classification): data sintetis 200 sampel dibangun dengan weights=[0.9, 0.1] — sembilan berbanding satu — dan random_state=42 agar contoh dapat direproduksi persis. Argumen flip_y=0 mematikan kebisingan label agar perbandingan model bersih.
  2. Baris 29–31 (train_test_split dengan stratify=y): proporsi kelas dijaga di kedua sisi — hasilnya data latih 126:14 dan data uji 54:6, keduanya tetap 9:1.
  3. Baris 36–40 (fungsi evaluasi): setiap model dievaluasi dengan dua angka — akurasi dan recall kelas 1 (proporsi kelas minoritas yang berhasil ditemukan). Fungsi ini hanya membaca data uji; tidak ada model yang dilatih di dalamnya.
  4. Baris 44–45 (baseline dummy): DummyClassifier(strategy="most_frequent") tidak belajar apa pun; classifier ini hanya menghitung kelas terbanyak lalu selalu menebak kelas tersebut.
  5. Baris 50–53 (class_weight="balanced"): satu-satunya perbedaan dengan model biasa ada pada argumen ini — data latih yang dipakai tetap sama, yang berubah hanyalah bobot kelas di dalam loss.
  6. Baris 56–65 (resampling): RandomUnderSampler dan SMOTE memakai pola fit_resample(X_latih, y_latih) — keduanya hanya menyentuh data latih. Model kemudian dilatih pada hasil resampling, tetapi evaluasi tetap memakai data uji asli.
  7. Baris 67–79 (hitungan sebelum-sesudah): np.bincount mencetak jumlah data per kelas sebelum dan sesudah resampling, memperlihatkan bahwa undersampling memangkas mayoritas ke 14 dan SMOTE menumbuhkan minoritas hingga 126.

Membaca Hasil: Akurasi Menipu, Recall Berbicara #

Baris pertama tabel adalah bukti jebakan akurasi: dummy mencatat akurasi 0,900 — angka yang terlihat bagus — dengan recall minoritas 0,000 karena classifier tersebut tidak pernah menebak satu pun kelas 1. Regresi logistik biasa naik ke akurasi 0,950 dengan recall 0,500: setengah pelanggan yang benar-benar berhenti luput. Versi class_weight="balanced" menulis kisah berbeda — akurasi turun ke 0,900 tetapi recall melompat ke 0,833; model sekarang menemukan lima dari enam kasus berhenti dengan menukar sebagian presisi. SMOTE mencapai recall 0,833 dengan akurasi lebih tinggi (0,933), dan undersampling tertinggal di recall 0,667 — konsisten dengan informasi mayoritas yang dibuang. Perlu digarisbawahi bahwa hasil ini berasal dari satu pemisahan data dengan 6 sampel minoritas di data uji; angka recall bergerak lebar antar pemisahan, sehingga keputusan final selalu perlu cross-validation — materi Section Evaluasi dan Validasi Model.

Semua teknik pada chapter ini tetap mematuhi satu disiplin: data latih boleh dimodifikasi, data uji hanya boleh dibaca. Pelanggaran disiplin tersebut — dalam bentuk yang paling halus sekalipun — adalah pokok bahasan chapter penutup Section ini: Data Leakage dan Cara Mencegahnya.

Referensi Resmi #