Sebagian besar algoritma scikit-learn hanya menerima angka, sehingga kolom kategorikal seperti nama kategori atau kode lokasi wajib diubah menjadi angka terlebih dahulu. Topik ini membahas pengodean (encoding) variabel kategorikal, mulai dari jebakan pemberian angka urut, one-hot encoding, hingga target encoding untuk kategori berjumlah sangat banyak. Praktiknya menggunakan data peminjaman perpustakaan dalam sebuah DataFrame beserta skrip one-hot-vs-target.py yang mengukur setiap pilihan pengodean pada pemecahan data yang sama. Setelah topik ini, pemilihan pengodean dapat disesuaikan dengan jumlah kategori dan risiko kebocorannya.
Kategori sebagai angka urut yang menipu#
Cara termudah mengubah kategori menjadi angka adalah memberi nomor urut, misalnya fiksi menjadi 0, sains menjadi 1, dan sejarah menjadi 2. Nomor itu membawa klaim yang tidak benar, yaitu sejarah lebih besar daripada sains dan sains lebih besar daripada fiksi, serta jarak antarnilainya sama besar. Model linear memperlakukan nomor tersebut sebagai besaran nyata dan hanya dapat memasangkan satu kemiringan terhadapnya. Apabila pengaruh kategori sebenarnya tidak berurutan, model tidak memiliki cara merekonstruksi pola itu dari satu kolom angka.
Kondisi seperti itu mudah terjadi pada data nyata. Pada data peminjaman yang digunakan skrip di bawah, rata-rata lama peminjaman buku sejarah paling lama, fiksi berada di tengah, dan sains paling singkat, sedangkan urutan alfabetisnya adalah fiksi, sains, sejarah. Pemberian nomor urut hanya sah apabila urutannya memang bermakna dan jarak antartingkatnya kira-kira setara, misalnya tingkat pendidikan. Di luar kasus itu, kategori bersifat nominal dan memerlukan pengodean yang tidak mengklaim urutan apa pun.
One-hot encoding dan jaminannya#
One-hot encoding meniadakan klaim urutan dengan mengganti satu kolom kategori menjadi beberapa kolom biner, satu kolom untuk setiap nilai yang muncul. Nilai fiksi menjadi kolom kategori_fiksi berisi 1 atau 0, dan begitu pula nilai lainnya, sehingga setiap kategori memperoleh koefisiennya sendiri tanpa dihubungkan oleh angka. Kelas OneHotEncoder dari scikit-learn melakukan transformasi itu dan menyimpan daftar nilainya agar data uji diubah dengan aturan yang sama seperti data latih.
Jaminan itu memiliki batas yang perlu diwaspadai. Encoder hanya membuat kolom untuk nilai yang ditemuinya pada data latih, dan nilai yang belum pernah dilihat diperlakukan sesuai pilihan handle_unknown. Pilihan "ignore" mengubah nilai asing menjadi baris tanpa tanda apa pun, yakni seluruh kolom bernilai 0, sehingga model membacanya mirip kategori acuan yang tidak diberi kolom sendiri. Karena daftar nilai itu tersimpan di dalam encoder, transformasi yang sama dapat diterapkan ulang pada data baru dengan urutan kolom yang konsisten. Perilaku tersebut wajar untuk kategori yang memang baru muncul, tetapi menjadi masalah apabila nilai asingnya banyak, sebagaimana terjadi pada kategori berjumlah ratusan di bagian berikutnya.
Mengukur pengodean pada satu skrip#
Skrip berikut membangun 400 baris data peminjaman dengan tiga kolom, yaitu kategori berisi tiga nilai, kode_rak berisi 120 nilai unik, dan halaman sebagai fitur numerik. Target lama_pinjam dipengaruhi kategori, sedikit oleh kode rak, dan oleh jumlah halaman, ditambah derau. Bagian A membandingkan angka urut alfabetis dengan one-hot encoding pada kolom kategori, sedangkan bagian B membandingkan one-hot penuh dengan target encoding pada kolom kode rak. Semua konfigurasi menggunakan pemecahan indeks yang sama, dan encoder dipasang hanya pada data latih.
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression, Ridge
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
# data peminjaman perpustakaan: lama peminjaman tergantung kategori dan kode rak
rng = np.random.default_rng(42)
n = 400
kategori = rng.choice(["fiksi", "sains", "sejarah"], size=n)
kode_rak = rng.integers(0, 120, size=n)
halaman = rng.integers(120, 500, size=n)
dasar = {"fiksi": 10.0, "sains": 7.0, "sejarah": 14.0}
efek_rak = rng.uniform(-1.5, 1.5, 120)
df = pd.DataFrame({
"kategori": kategori,
"kode_rak": kode_rak,
"halaman": halaman,
})
df["lama_pinjam"] = np.round(
[dasar[k] for k in kategori]
+ efek_rak[kode_rak]
+ 0.008 * halaman
+ rng.normal(0.0, 1.5, n),
1,
)
print(df.head())
y = df["lama_pinjam"]
# satu pemecahan indeks yang sama untuk semua konfigurasi agar perbandingannya adil
idx = np.arange(n)
idx_latih, idx_uji = train_test_split(idx, test_size=0.2, random_state=42)
y_latih, y_uji = y.iloc[idx_latih], y.iloc[idx_uji]
# bagian A: variabel kategorikal tiga nilai diberi angka urut secara alfabetis
urutan = [["fiksi", "sains", "sejarah"]]
angka = np.array([urutan[0].index(k) for k in kategori])
X_angka = np.column_stack([angka, halaman])
model_angka = LinearRegression().fit(X_angka[idx_latih], y_latih)
print(f"\nA. R2 kategori sebagai angka urut : {model_angka.score(X_angka[idx_uji], y_uji):.4f}")
# bagian A: variabel yang sama melalui one-hot encoding
pengode = OneHotEncoder(sparse_output=False, categories=urutan)
X_onehot_a = np.column_stack([
pengode.fit_transform(df[["kategori"]].iloc[idx_latih]),
halaman[idx_latih],
])
model_onehot = LinearRegression().fit(X_onehot_a, y_latih)
X_uji_a = np.column_stack([
pengode.transform(df[["kategori"]].iloc[idx_uji]),
halaman[idx_uji],
])
print(f"A. R2 one-hot encoding : {model_onehot.score(X_uji_a, y_uji):.4f}")
# bagian B: kardinalitas tinggi, kode rak memiliki 120 nilai unik
# handle_unknown="ignore": kategori yang tidak ada di data latih menjadi baris nol
pengode_rak = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
X_rak_latih = pengode_rak.fit_transform(df[["kode_rak"]].iloc[idx_latih])
X_rak_uji = pengode_rak.transform(df[["kode_rak"]].iloc[idx_uji])
jumlah_asing = int((X_rak_uji.sum(axis=1) == 0).sum())
# matriks one-hot yang lebar selalu ditemani regularisasi ridge
def latih_ridge(fitur_latih, fitur_uji):
model = Ridge(alpha=1.0).fit(fitur_latih, y_latih)
return model.score(fitur_latih, y_latih), model.score(fitur_uji, y_uji)
X_lengkap_latih = np.column_stack([
pengode.transform(df[["kategori"]].iloc[idx_latih]), X_rak_latih, halaman[idx_latih],
])
X_lengkap_uji = np.column_stack([
pengode.transform(df[["kategori"]].iloc[idx_uji]), X_rak_uji, halaman[idx_uji],
])
skor_lengkap = latih_ridge(X_lengkap_latih, X_lengkap_uji)
print(f"\nB. Jumlah kolom one-hot rak : {X_rak_latih.shape[1]}")
print(f"B. Rak tanpa data latih : {jumlah_asing} dari {len(idx_uji)} baris uji")
print(f"B. one-hot penuh + ridge : "
f"R2 latih {skor_lengkap[0]:.4f} / R2 uji {skor_lengkap[1]:.4f}")
# target encoding: rata-rata target per rak, dihitung dari data latih saja
def target_encode(indeks_rata, indeks_peta):
rata_global = y.iloc[indeks_rata].mean()
tabel = pd.DataFrame({"rak": kode_rak[indeks_rata], "y": y.iloc[indeks_rata].values})
rata_rak = tabel.groupby("rak")["y"].mean()
return rata_rak.reindex(kode_rak[indeks_peta]).fillna(rata_global).to_numpy()
X_jujur_latih = np.column_stack([
pengode.transform(df[["kategori"]].iloc[idx_latih]),
target_encode(idx_latih, idx_latih), halaman[idx_latih],
])
X_jujur_uji = np.column_stack([
pengode.transform(df[["kategori"]].iloc[idx_uji]),
target_encode(idx_latih, idx_uji), halaman[idx_uji],
])
skor_jujur = latih_ridge(X_jujur_latih, X_jujur_uji)
print("B. target encoding jujur (latih) : "
f"R2 latih {skor_jujur[0]:.4f} / R2 uji {skor_jujur[1]:.4f}")
# target encoding bocor: rata-rata dihitung dari seluruh data termasuk data uji
X_bocor_latih = np.column_stack([
pengode.transform(df[["kategori"]].iloc[idx_latih]),
target_encode(idx, idx_latih), halaman[idx_latih],
])
X_bocor_uji = np.column_stack([
pengode.transform(df[["kategori"]].iloc[idx_uji]),
target_encode(idx, idx_uji), halaman[idx_uji],
])
skor_bocor = latih_ridge(X_bocor_latih, X_bocor_uji)
print("B. target encoding bocor : "
f"R2 latih {skor_bocor[0]:.4f} / R2 uji {skor_bocor[1]:.4f}") kategori kode_rak halaman lama_pinjam
0 fiksi 119 124 12.8
1 sejarah 93 234 15.0
2 sains 38 315 11.8
3 sains 116 469 7.6
4 sains 59 400 8.7
A. R2 kategori sebagai angka urut : 0.2189
A. R2 one-hot encoding : 0.7112
B. Jumlah kolom one-hot rak : 109
B. Rak tanpa data latih : 7 dari 80 baris uji
B. one-hot penuh + ridge : R2 latih 0.8674 / R2 uji 0.7444
B. target encoding jujur (latih) : R2 latih 0.8047 / R2 uji 0.7179
B. target encoding bocor : R2 latih 0.8023 / R2 uji 0.8092Hasil bagian A mengukur kerugian urutan semu pada data uji. Angka urut alfabetis hanya mencapai 0.2189, sedangkan one-hot encoding pada kolom yang sama mencapai 0.7112, dan selisihnya berasal dari bentuk hubungan yang dipaksakan, bukan dari informasi tambahan. Hasil bagian B memperlihatkan harga kardinalitas tinggi. Encoder membentuk 109 kolom biner dari 320 baris latih, dan tujuh dari 80 baris uji jatuh sebagai nilai asing tanpa tanda. Matriks selebar itu wajib ditemani regularisasi ridge agar pelatihannya stabil.
Target encoding memampatkan 109 kolom rak menjadi satu kolom angka berisi rata-rata target per rak yang dihitung dari data latih, dengan nilai asing jatuh ke rata-rata global. Pada data yang sama, one-hot penuh dengan regularisasi mencapai uji 0.7444, sedangkan target encoding yang jujur mencapai 0.7179. Keduanya setara dalam kekuatan pada data ini. Keunggulan praktis target encoding terletak pada fitur yang ringkas dan kelancarannya pada model pohon. Harganya adalah kehati-hatian ekstra terhadap kebocoran, yang dibahas pada bagian berikutnya.
Kebocoran pada target encoding#
Target encoding menghitung rata-rata target, sehingga sumber datanya menentukan kejujurannya. Apabila rata-rata dihitung dari seluruh dataset sebelum pemecahan, setiap baris ikut menyumbang targetnya sendiri ke dalam fiturnya. Jawaban yang seharusnya ditebak dengan demikian sudah tertanam di dalam fitur. Pada skrip, versi bocor ini mencapai uji 0.8092 tanpa ada perbaikan model apa pun, jauh di atas versi jujur 0.7179. Kenaikan yang menggembirakan itulah gejala kebocoran data (data leakage), yaitu angka evaluasi yang naik karena informasi masa depan ikut digunakan, bukan karena kemampuan model benar-benar meningkat.
Kejelian dibutuhkan karena kebocoran jenis ini tidak memunculkan error apa pun. Skrip berjalan mulus, skor terlihat membaik, dan model tetap dapat disimpan serta dijalankan. Satu-satunya petunjuk adalah skor yang terlalu indah dibandingkan ekspektasi domain, atau selisih yang aneh terhadap pengodean alternatif. Karena itu rata-rata target hanya boleh dihitung dari data latih, lalu diterapkan ke data uji melalui langkah transformasi yang terikat pada pemecahan data yang sama.
Kesalahan umum#
- Memberi nomor urut pada kategori tak berurutan. Penyebabnya adalah anggapan bahwa semua teks cukup diterjemahkan menjadi angka, padahal angka membawa struktur urutan yang dipercaya model. Perbaikannya menggunakan one-hot encoding untuk kategori nominal dengan jumlah nilai yang wajar.
- Mengodekan sebelum memecah data. Encoder yang melihat seluruh dataset, baik daftar nilainya maupun rata-rata targetnya, membawa informasi data uji ke dalam data latih. Pasang encoder hanya pada data latih, lalu gunakan
transformuntuk data uji. - Membiarkan kardinalitas tinggi tanpa strategi. Ratusan kolom biner membesarkan memori, memperlambat pelatihan, dan menyisakan banyak baris asing pada data uji. Gunakan target encoding yang dihitung dari data latih, kelompokkan nilai langka menjadi kategori lain, atau pertahankan hanya nilai yang paling sering muncul.
- Mengandalkan
handle_unknown="ignore"tanpa menghitung dampaknya. Pilihan itu hanya meniadakan tanda kategori, sehingga kategori asing yang berbeda-beda diperlakukan sama persis, dan efeknya membesar apabila nilai asing banyak. Pada skrip topik ini, tujuh dari 80 baris uji jatuh ke dalam kondisi tersebut. Amati jumlah baris asing pada data uji sebelum memutuskan strategi pengodean.
Ringkasan#
- Kategori tidak boleh diberi nomor urut kecuali urutannya bermakna, karena nomor memaksa model percaya pada hubungan yang tidak ada.
- One-hot encoding memberi setiap nilai kolom binernya sendiri dan aman untuk kardinalitas rendah, tetapi meledak menjadi ratusan kolom pada kardinalitas tinggi.
- Target encoding memampatkan banyak kategori menjadi satu kolom rata-rata target dan wajib dihitung dari data latih saja.
- Selisih skor yang terlalu menggembirakan setelah pengodean berbasis target adalah gejala kebocoran data yang harus diselidiki, bukan keberhasilan yang dirayakan.
Referensi#
- Preprocessing data — scikit-learn — Bagian resmi tentang pengodean fitur kategorikal beserta pilihan penanganan nilai asing.
- sklearn.preprocessing.OneHotEncoder — scikit-learn
— Dokumentasi encoder biner, termasuk parameter
handle_unknowndandrop. - sklearn.preprocessing.TargetEncoder — scikit-learn — Dokumentasi target encoding resmi beserta mekanisme perataan dan validasi silangnya.