LightGBM mempercepat gradient boosting pada data numerik besar, tetapi dua masalah lama masih menunggu jawaban: bagaimana memakai fitur kategorikal tanpa encoding manual yang rentan bocor label, dan bagaimana mencegah boosting menghafal dataset kecil yang berderau. CatBoost — pustaka dari Yandex yang namanya berasal dari categorical boosting — dibangun tepat untuk keduanya: statistik target terurut (ordered target statistics) menggantikan one-hot, ordered boosting menekan bias residual yang menjadi akar overfitting, dan pohon simetris menjaga struktur model tetap sederhana. Mekanisme koreksi berantainya tetap gradient boosting seperti Chapter Gradient Boosting — yang berubah adalah cara data kategorikal masuk dan cara residual dihitung. Chapter ini membedah ketiga gagasan tersebut lalu mengujinya pada data churn pelanggan toko online berisi tiga kolom kategorikal, tanpa satu baris encoding pun.

Masalah Encoding Kategorikal di Boosting #

Dua jalur standar untuk fitur kategorikal sama-sama menyimpan duri. One-hot encoding mengubah satu kolom kota berisi 4 nilai menjadi 4 kolom biner — tidak masalah di sini, tetapi pada kolom bertingkat tinggi seperti kode_pos dengan ribuan nilai, dimensi fitur melonjak cepat sementara tiap kolom hasil encoding menjadi sparse; pemindaian pemecahan menjadi mahal dan pohon individual cenderung mengandalkan satu kolom dummy yang sepi. Target encoding — mengganti tiap kategori dengan rata-rata label kelompoknya — lebih hemat dimensi, tetapi rata-rata itu dihitung dari seluruh data latih termasuk baris yang sedang diprediksi sendiri: baris berlabel 1 menyumbang angka pada fiturnya sendiri, dan model boosting yang tajam akan memburu kebocoran ini hingga akurasi latih menipu.

CatBoost menolak kedua jalur tersebut: kolom kategorikal dikirim apa adanya lewat parameter cat_features, dan konversi ke angka dikerjakan di dalam dengan statistik target terurut.

Ordered Target Statistics dan Ordered Boosting #

Ide statistik target terurut sederhana namun manjur. Data latih diacak menurut suatu permutasi; untuk baris ke-i, kategorinya diganti rata-rata label dari baris-baris sebelum baris ke-i pada permutasi itu (plus penghalang prior agar kategori sepi tidak melonjak). Rata-rata untuk baris pertama hampir kosong, untuk baris terakhir hampir setara rata-rata penuh — informasi label baris itu sendiri tidak pernah masuk ke fiturnya, sehingga pintu kebocoran tertutup. Beberapa permutasi diacak berbeda dan hasilnya dirata-rata agar urutan tunggal tidak meninggalkan artefak.

Ordered boosting menerapkan disiplin yang sama pada residual. Pada gradient boosting biasa, residual baris ke-i di ronde t dihitung dari model yang dilatih pada seluruh data latih — termasuk baris ke-i itu sendiri; residual tersebut dengan demikian terlalu optimistis (prediction shift). CatBoost menghitung residual baris ke-i hanya dengan model yang dilatih pada baris-baris sebelumnya pada permutasi, lalu membangun pohon berikutnya dari residual yang lebih jujur. Biayanya komputasi bertambah, sehingga CatBoost menyediakan mode perkiraan yang lebih cepat — tetapi arah desainnya konsisten: menutup setiap celah label menyusup ke fitur.

Pohon Simetris untuk Regularisasi dan Kecepatan Inferensi #

Pohon CatBoost tumbuh simetris (oblivious tree): satu kondisi pemecahan yang sama dipakai pada seluruh tingkat kedalaman — mirip daftar keputusan bertingkat, bukan pohon bebas. Konsekuensinya, pohon berkedalaman depth selalu memiliki paling banyak 2^depth daun dan prediksinya bisa dibaca sebagai tabel pencarian: beberapa pembandingan bitwise lalu selesai. Untuk data yang berderau, kekakuan struktur ini berfungsi sebagai regularisasi — pohon tidak bisa membentuk cabang istimewa yang menghafal segelintir baris. Parameter kapasitasnya depth (bawaan 6, sehingga maksimum 64 daun), bukan num_leaves seperti LightGBM.

Pendekatan kategorikalCara kerjaRisiko utama
One-hot + estimator biasaTiap kategori menjadi kolom biner di huluDimensi melonjak pada kategori bertingkat tinggi
Target encoding manualKategori diganti rata-rata label kelompoknyaKebocoran label bila dihitung pada seluruh data
categorical_feature LightGBMSplit kategorikal saat pelatihan pohonPerlu penandaan tipe kategori di hulu
cat_features CatBoostStatistik target terurut di dalam pelatihanUkuran dataset sangat kecil dengan kategori sepi

Antarmuka scikit-learn dan Parameternya #

CatBoostClassifier dan CatBoostRegressor mengikuti kontrak fit()/predict() scikit-learn dan menerima pandas DataFrame secara langsung — kolom kategorikal cukup didaftarkan lewat argumen cat_features pada fit() berupa daftar nama kolom. Tiga parameter kebersihan yang wajib melekat di lingkungan kerja kita: random_seed=42 untuk reproduksibilitas, verbose=False agar pelatihan tidak menyapa konsol setiap iterasi, dan allow_writing_files=False supaya CatBoost tidak menaruh direktori catboost_info di sekitar skrip. Parameter kapasitas utamanya depth, iterations, dan learning_rate — maknanya sama dengan n_estimators pada pustaka sebelumnya.

Eksperimen: Data Churn dengan Tiga Kolom Kategorikal #

Skrip berikut membangun data churn pelanggan toko online — 180 baris, tiga kolom kategorikal (kota, perangkat, paket) dan tiga numerik — lalu mempertemukan CatBoost dengan pembanding klasik: pipeline one-hot + GradientBoostingClassifier. Empat bagian skrip memeriksa struktur data, perilaku pelatihan, biaya encoding, dan akhirnya prediksi probabilitas untuk tiga pelanggan baru.

python
# CatBoost: ordered boosting, pohon simetris, dan fitur kategorikal native.
# Bagian 1: data churn pelanggan toko online (pandas DataFrame, 3 kolom
#           kategorikal) + tampilan 5 baris pertama.
# Bagian 2: CatBoostClassifier dengan cat_features -- tanpa encoding manual.
# Bagian 3: pembanding one-hot + GradientBoostingClassifier (pipeline).
# Bagian 4: prediksi probabilitas churn untuk 3 pelanggan baru.

from collections import Counter

import numpy as np
import pandas as pd
from catboost import CatBoostClassifier
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder

# --- Bagian 1: data churn sintetis --------------------------------------
rng = np.random.default_rng(42)
n = 180
kota = rng.choice(["Jakarta", "Bandung", "Surabaya", "Medan"], size=n,
                  p=[0.35, 0.25, 0.25, 0.15])
perangkat = rng.choice(["mobile", "desktop", "tablet"], size=n,
                       p=[0.6, 0.3, 0.1])
paket = rng.choice(["reguler", "premium"], size=n, p=[0.7, 0.3])
umur_akun = rng.integers(1, 37, size=n)              # bulan
jumlah_transaksi = rng.poisson(6, size=n)            # transaksi 90 hari
skor_kepuasan = rng.integers(1, 6, size=n)           # skala 1-5

skor = (3.0
        - 0.075 * umur_akun
        - 0.27 * jumlah_transaksi
        - 0.52 * skor_kepuasan
        + 1.35 * (paket == "reguler")
        + 0.6 * (perangkat == "mobile")
        + 0.3 * (kota == "Medan"))
peluang = 1 / (1 + np.exp(-skor))
churn = rng.binomial(1, peluang)

data = pd.DataFrame({
    "id_pelanggan": [f"P{i:03d}" for i in range(1, n + 1)],
    "kota": kota,
    "perangkat": perangkat,
    "paket": paket,
    "umur_akun_bulan": umur_akun,
    "jumlah_transaksi_90hari": jumlah_transaksi,
    "skor_kepuasan": skor_kepuasan,
    "churn": churn,
})
fitur = ["kota", "perangkat", "paket", "umur_akun_bulan",
         "jumlah_transaksi_90hari", "skor_kepuasan"]
kategorikal = ["kota", "perangkat", "paket"]

print("Bagian 1 - data churn pelanggan toko online:")
print(f"  jumlah baris          : {len(data)}")
print(f"  rasio churn           : {data['churn'].mean():.3f}")
print(f"  5 baris pertama:")
print(data.head().to_string(index=False))

X_latih, X_uji, y_latih, y_uji = train_test_split(
    data[fitur], data["churn"], test_size=54, random_state=42)

# --- Bagian 2: CatBoost dengan cat_features ----------------------------
model = CatBoostClassifier(iterations=200, learning_rate=0.1, depth=6,
                           random_seed=42, verbose=False,
                           allow_writing_files=False, thread_count=1)
model.fit(X_latih, y_latih, cat_features=kategorikal)

daun_per_pohon = model.get_tree_leaf_counts()
print("\nBagian 2 - CatBoostClassifier (cat_features native):")
print(f"  akurasi latih              : {model.score(X_latih, y_latih):.3f}")
print(f"  akurasi uji                : {model.score(X_uji, y_uji):.3f}")
print(f"  jumlah pohon               : {model.tree_count_}")
print(f"  daun per pohon (distribusi): {dict(sorted(Counter(int(d) for d in daun_per_pohon).items()))}")
urutan_penting = np.argsort(model.feature_importances_)[::-1]
print("  kepentingan fitur (semua)  :")
for i in urutan_penting:
    print(f"    {model.feature_names_[i]:<24} "
          f"{model.feature_importances_[i]:5.1f}")

# --- Bagian 3: pembanding one-hot + gradient boosting ------------------
prepros = ColumnTransformer(
    transformers=[("kategorikal", OneHotEncoder(handle_unknown="ignore"),
                   kategorikal)],
    remainder="passthrough")
pembanding = Pipeline([
    ("prepros", prepros),
    ("model", GradientBoostingClassifier(n_estimators=200, learning_rate=0.1,
                                         max_depth=3, random_state=42)),
])
pembanding.fit(X_latih, y_latih)
nama_kolom = (pembanding.named_steps["prepros"]
              .get_feature_names_out().tolist())

print("\nBagian 3 - pembanding one-hot + GradientBoostingClassifier:")
print(f"  kolom sebelum one-hot      : {X_latih.shape[1]}")
print(f"  kolom setelah one-hot      : {len(nama_kolom)}")
print(f"  akurasi latih              : {pembanding.score(X_latih, y_latih):.3f}")
print(f"  akurasi uji                : {pembanding.score(X_uji, y_uji):.3f}")

# --- Bagian 4: probabilitas churn pelanggan baru -----------------------
pelanggan_baru = pd.DataFrame({
    "kota": ["Jakarta", "Bandung", "Surabaya"],
    "perangkat": ["desktop", "mobile", "desktop"],
    "paket": ["premium", "reguler", "premium"],
    "umur_akun_bulan": [30, 4, 14],
    "jumlah_transaksi_90hari": [12, 2, 5],
    "skor_kepuasan": [5, 2, 3],
})
proba = model.predict_proba(pelanggan_baru)[:, 1]
print("\nBagian 4 - probabilitas churn pelanggan baru:")
for nama, p in zip(["Budi", "Siti", "Dewi"], proba):
    print(f"  {nama:<5} : {p:.3f}")

Output yang Dihasilkan:

text
Bagian 1 - data churn pelanggan toko online:
  jumlah baris          : 180
  rasio churn           : 0.461
  5 baris pertama:
id_pelanggan     kota perangkat   paket  umur_akun_bulan  jumlah_transaksi_90hari  skor_kepuasan  churn
        P001 Surabaya   desktop premium               34                        4              3      0
        P002  Bandung   desktop reguler               36                        7              5      0
        P003    Medan    mobile reguler               35                        3              4      1
        P004 Surabaya    mobile reguler               21                        6              5      0
        P005  Jakarta   desktop reguler                8                        8              1      0

Bagian 2 - CatBoostClassifier (cat_features native):
  akurasi latih              : 0.913
  akurasi uji                : 0.741
  jumlah pohon               : 200
  daun per pohon (distribusi): {1: 1, 2: 2, 4: 2, 8: 3, 32: 3, 64: 189}
  kepentingan fitur (semua)  :
    umur_akun_bulan           25.5
    jumlah_transaksi_90hari   19.1
    skor_kepuasan             16.8
    kota                      15.5
    perangkat                 14.5
    paket                      8.7

Bagian 3 - pembanding one-hot + GradientBoostingClassifier:
  kolom sebelum one-hot      : 6
  kolom setelah one-hot      : 12
  akurasi latih              : 1.000
  akurasi uji                : 0.704

Bagian 4 - probabilitas churn pelanggan baru:
  Budi  : 0.004
  Siti  : 0.942
  Dewi  : 0.383

Mari kita telaah bagian-bagian kunci skrip tersebut:

  1. Baris 20–29 (generator data): rng berseed 42 menarik tiga kolom kategorikal dari distribusi eksplisit dan tiga kolom numerik — umur_akun 1–36 bulan, transaksi 90 hari ber-Poisson, skor kepuasan skala 1–5.
  2. Baris 31–39 (mekanisme label): skor laten menjumlahkan kontribusi semua fitur — paket reguler dan perangkat mobile menaikkan kecenderungan berhenti, akun tua dan skor kepuasan tinggi menurunkannya — lalu label diundi dari Bernoulli berpeluang sigmoid. Komponen acak inilah lantai galat dataset: tidak ada model yang berhak menyentuh akurasi 1,000.
  3. Baris 61–62 (split tetap): 126 baris latih dan 54 baris uji, keduanya DataFrame mentah — kolom teks kota/perangkat/paket dikirim tanpa disentuh.
  4. Baris 65–68 (pelatihan): konstruktor memasang random_seed=42, verbose=False, allow_writing_files=False, dan thread_count=1; pemanggilan fit(..., cat_features=kategorikal) adalah satu-satunya penandaan yang dibutuhkan — tidak ada OneHotEncoder, tidak ada get_dummies.
  5. Baris 70–80 (pemeriksaan struktur): get_tree_leaf_counts() melaporkan jumlah daun tiap pohon, dan Counter merangkum distribusinya; feature_importances_ bersama feature_names_ mencetak kepentingan seluruh fitur dalam format tabel.
  6. Baris 83–94 (pembanding): ColumnTransformer meng-encode tiga kolom kategorikal sambil meloloskan kolom numerik (remainder="passthrough"), disusun sebagai Pipeline — arsitektur standar Chapter Encoding Variabel Kategorikal — sebelum GradientBoostingClassifier dengan anggaran pohon yang setara.
  7. Baris 103–114 (pelanggan baru): tiga baris DataFrame dengan profil berlawanan dikirim ke predict_proba, mengambil kolom kelas 1 sebagai probabilitas berhenti berlangganan.

Membaca Hasil Eksperimen #

Bagian 2 memperlihatkan watak CatBoost pada data kecil berderau: akurasi latih berhenti di 0,913 — model menolak menghafal sisa 8,7% label yang memang diundi secara acak — sementara akurasi uji mencatat 0,741. Baris distribusi daun menegaskan klaim pohon simetris: 189 dari 200 pohon tumbuh penuh 64 daun (depth=6), sisanya berhenti di pangkat dua yang lebih kecil (32, 16, …) — tidak ada satu pun pohon dengan jumlah daun ganjil, karena tingkat yang sama selalu berpecah serentak. Kepentingan fitur menempatkan tiga fitur numerik di atas (umur akun 25,5; transaksi 19,1; kepuasan 16,8), dengan dua fitur kategorikal menyusul — masuk akal karena kontribusi aslinya memang lebih landai.

Bagian 3 adalah pembanding yang jujur terhadap kedua dunia. Kolom fitur membengkak dari 6 menjadi 12 akibat one-hot, dan GradientBoostingClassifier memboroskan kapasitasnya menghafal label acak: akurasi latih 1,000, akurasi uji 0,704 — gap latih-uji 0,296 berbanding 0,172 milik CatBoost. Selisih akurasi uji 0,037 pada 54 sampel memang masih dalam wilayah kebetulan sampling — perlakuan serius perlu cross-validation — tetapi arah ceritanya konsisten dengan desain kedua pustaka: regularisasi struktur menahan boosting dari penghafalan.

Bagian 4 menghadirkan inti kegunaan bisnisnya. Budi (premium, desktop, akun 30 bulan, kepuasan 5) diprediksi berpeluang berhenti 0,004; Siti (reguler, mobile, akun 4 bulan, kepuasan 2) melonjak ke 0,942; Dewi yang profilnya di tengah jalan mendarat di 0,383. Ketiganya diproses tanpa satu baris translasi kategori menjadi angka — DataFrame mentah masuk, probabilitas keluar.

Keluarga boosting kini lengkap kita kenal — dari mesin manual scikit-learn, XGBoost yang teregularisasi, LightGBM yang berbasis histogram, hingga CatBoost dengan jalur kategorikalnya. Seluruhnya masih satu algoritme dasar dengan keragaman yang lahir dari konfigurasi. Section ini ditutup dengan jenis keragaman yang paling berani: menggabungkan model yang sepenuhnya berbeda algoritme — topik Chapter Voting dan Stacking.

Referensi Resmi #