Chapter sebelumnya menutup dengan satu keterbatasan Pipeline polos: satu transformer diterapkan ke seluruh kolom, sedangkan tabel nyata mencampur kolom numerik dan kategorikal yang menuntut perlakuan berbeda. StandardScaler menolak teks; OneHotEncoder akan mengubah angka menjadi barisan kolom tanpa makna. Kelas ColumnTransformer menyelesaikan kebuntuan tersebut: satu objek yang merutekan setiap kelompok kolom ke transformer masing-masing, menggabungkan hasilnya, dan — karena ColumnTransformer sendiri adalah transformer — tetap bisa ditaruh di dalam Pipeline. Chapter ini membedah anatomi peruteannya, pemilih kolom otomatis, penamaan fitur keluaran, opsi remainder, hingga perilaku keluaran sparse saat kategori membengkak banyaknya.
Masalah Perutean Kolom #
Tanpa ColumnTransformer, tabel campuran diproses secara manual: skalakan kolom numerik, encode kolom kategorikal, lalu gabungkan kedua hasilnya dengan np.concatenate atau pd.concat. Pola ini berjalan, tetapi menyimpan tiga jebakan. Pertama, urutan kolom hasil penggabungan harus dijaga tangan — tertukar satu posisi pun, model membaca fitur yang keliru tanpa galat apa pun. Kedua, kode preprocessing harus ditulis ulang saat inferensi; satu kolom yang lupa digabung saat predict() menghasilkan dimensi yang tidak cocok, dan yang lebih halus: kolom tersedia tetapi urutannya berbeda. Ketiga, nama fitur hilang — matriks gabungan tidak lagi tahu bahwa kolom ke-5 adalah kota_Jakarta, sehingga interpretasi koefisien dan debugging menjadi sulit.
ColumnTransformer menghapus ketiga jebakan sekaligus karena definisi perutean hidup di satu tempat dan dieksekusi ulang secara identik setiap kali transform() dipanggil — di dalam maupun di luar cross-validation.
Anatomi ColumnTransformer #
ColumnTransformer dibangun dari daftar tripel (nama, transformer, kolom). Parameter kolom menentukan kolom mana yang dialirkan ke transformer tersebut; sisanya diatur parameter remainder. Bentuk spesifikasi kolom sendiri punya beberapa alternatif:
| Bentuk spesifikasi | Contoh | Kolom yang dirutekan |
|---|---|---|
| Daftar nama kolom | ["umur", "pendapatan_juta"] | Tepat dua kolom tersebut |
| Pemilih tipe data | make_column_selector(dtype_include=np.number) | Semua kolom numerik saat fit() |
| Pemilih pengecualian | make_column_selector(dtype_exclude=object) | Semua kolom non-teks |
| Daftar posisi integer | [0, 2] | Kolom indeks 0 dan 2 |
String "passthrough" sebagai transformer | ("asli", "passthrough", ["umur"]) | Kolom dilewatkan tanpa diubah |
Parameter remainder mengurus kolom yang tidak disebut tripel mana pun:
Nilai remainder | Perilaku |
|---|---|
"drop" (bawaan) | Kolom tak dirutekan dibuang dari keluaran |
"passthrough" | Kolom tak dirutekan ikut apa adanya |
| Objek transformer | Transformer tersebut diterapkan ke semua kolom sisanya |
Demonstrasi Terpadu: Tabel Nasabah Campuran #
Demonstrasi berikut memakai tabel 120 nasabah dengan kolom umur, kota, pendapatan_juta, dan pendidikan — dua numerik, dua kategorikal. Pemilih kolom otomatis mengelompokkannya berdasarkan tipe data; StandardScaler menangani jalur numerik, OneHotEncoder jalur kategorikal, lalu LogisticRegression menutup rangkaian. Empat bagian output memeriksa bentuk keluaran, opsi remainder, keluaran sparse, dan prediksi data baru.
# ColumnTransformer: satu tempat untuk merutekan kolom numerik dan
# kategorikal sebelum seluruhnya masuk model. Demonstrasi mencakup
# pemilih kolom otomatis, nama fitur keluaran, remainder passthrough,
# keluaran sparse untuk kategori banyak, dan prediksi data baru.
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# ---------- Data campuran: dua kolom numerik + dua kolom kategorikal ----------
rng = np.random.default_rng(42)
n = 120
umur = np.clip(rng.normal(39, 9, size=n), 18, 65)
pendapatan = np.clip(rng.normal(11, 3, size=n), 3, 25)
kota = rng.choice(["Bandung", "Jakarta", "Surabaya"], size=n)
pendidikan = rng.choice(["SMA", "S1", "S2"], size=n)
# Pola: pendapatan rendah dan pendidikan tinggi mendorong berlangganan.
bonus_sarjana = (pendidikan == "S1") * 0.7 + (pendidikan == "S2") * 1.2
logit = (
0.5 * (umur - 39) / 9
- 0.9 * (pendapatan - 11) / 3
+ bonus_sarjana
+ rng.normal(0, 0.55, size=n)
)
y = pd.Series(
((1 / (1 + np.exp(-logit))) > 0.5).astype(int), name="berlangganan"
)
df = pd.DataFrame({
"umur": umur,
"kota": kota,
"pendapatan_juta": pendapatan,
"pendidikan": pendidikan,
})
# ---------- Pemilih kolom otomatis berbasis tipe data ----------
pilih_angka = make_column_selector(dtype_include=np.number)
pilih_teks = make_column_selector(dtype_include=object)
print("== Bagian 1: perutean kolom otomatis ==")
print("kolom numerik :", pilih_angka(df))
print("kolom kategorikal:", pilih_teks(df))
# ---------- ColumnTransformer di dalam Pipeline ----------
pra = ColumnTransformer([
("skala", StandardScaler(), pilih_angka),
("satu_pan", OneHotEncoder(handle_unknown="ignore"), pilih_teks),
])
pipeline = Pipeline([
("pra", pra),
("model", LogisticRegression(max_iter=1000)),
])
X_transform = pra.fit_transform(df)
print(f"bentuk keluaran : {X_transform.shape} "
f"(tipe {type(X_transform).__name__})")
print("nama fitur :", pra.get_feature_names_out().tolist())
print("baris pertama :", np.round(X_transform[0], 3).tolist())
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
skor = cross_val_score(pipeline, df, y, cv=cv)
print("skor CV :", np.round(skor, 3).tolist(),
f"rerata {skor.mean():.3f}")
# ---------- remainder="passthrough": kolom tak dirutekan tetap ikut ----------
# Hanya "umur" yang diskalakan; tiga kolom lain lewat apa adanya dan
# diberi prefiks "remainder__" pada nama fiturnya.
pra_lewat = ColumnTransformer(
[("skala", StandardScaler(), ["umur"])], remainder="passthrough"
)
keluar_lewat = pra_lewat.fit_transform(df)
print("\n== Bagian 2: remainder passthrough ==")
print("nama fitur :", pra_lewat.get_feature_names_out().tolist())
baris0 = [
round(float(v), 3) if isinstance(v, (int, float, np.floating)) else v
for v in keluar_lewat[0]
]
print("baris pertama :", baris0)
# ---------- Kategori banyak menghasilkan keluaran sparse ----------
# Menambahkan kode_pos (25 kategori) membuat blok satu-panas sangat jarang
# terisi; kepadatan total jatuh di bawah 0.3 sehingga ColumnTransformer
# mengembalikan matriks sparse csr secara otomatis.
df_sparse = df.copy()
df_sparse["kode_pos"] = rng.choice(
[f"10{i:03d}" for i in range(25)], size=n
)
pra_sparse = ColumnTransformer([
("skala", StandardScaler(), pilih_angka),
("satu_pan", OneHotEncoder(handle_unknown="ignore"), pilih_teks),
])
X_sparse = pra_sparse.fit_transform(df_sparse)
kepadatan = X_sparse.nnz / (X_sparse.shape[0] * X_sparse.shape[1])
print("\n== Bagian 3: kategori banyak -> keluaran sparse ==")
print(f"bentuk keluaran : {X_sparse.shape} "
f"(tipe {type(X_sparse).__name__})")
print(f"kepadatan : {kepadatan:.3f}")
pipeline_sparse = Pipeline([
("pra", pra_sparse),
("model", LogisticRegression(max_iter=1000)),
])
skor_sparse = cross_val_score(pipeline_sparse, df_sparse, y, cv=cv)
print("skor CV :", np.round(skor_sparse, 3).tolist(),
f"rerata {skor_sparse.mean():.3f}")
# ---------- Prediksi data baru, termasuk kota yang tak pernah dilihat ----------
pipeline.fit(df, y)
baru = pd.DataFrame({
"umur": [28, 55],
"kota": ["Medan", "Bandung"],
"pendapatan_juta": [6.0, 20.0],
"pendidikan": ["S1", "SMA"],
})
prediksi = pipeline.predict(baru)
peluang = pipeline.predict_proba(baru)[:, 1]
print("\n== Bagian 4: prediksi data baru ==")
for i in range(len(baru)):
print(f"umur={baru['umur'][i]:3d} pendapatan={baru['pendapatan_juta'][i]:5.1f} "
f"kota={baru['kota'][i]:8s} pendidikan={baru['pendidikan'][i]:4s} "
f"-> berlangganan={prediksi[i]} p={peluang[i]:.3f}")Output yang Dihasilkan:
== Bagian 1: perutean kolom otomatis ==
kolom numerik : ['umur', 'pendapatan_juta']
kolom kategorikal: ['kota', 'pendidikan']
bentuk keluaran : (120, 8) (tipe ndarray)
nama fitur : ['skala__umur', 'skala__pendapatan_juta', 'satu_pan__kota_Bandung', 'satu_pan__kota_Jakarta', 'satu_pan__kota_Surabaya', 'satu_pan__pendidikan_S1', 'satu_pan__pendidikan_S2', 'satu_pan__pendidikan_SMA']
baris pertama : [0.467, -0.988, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
skor CV : [0.958, 0.958, 0.833, 0.917, 0.917] rerata 0.917
== Bagian 2: remainder passthrough ==
nama fitur : ['skala__umur', 'remainder__kota', 'remainder__pendapatan_juta', 'remainder__pendidikan']
baris pertama : [0.467, 'Bandung', 7.93, 'SMA']
== Bagian 3: kategori banyak -> keluaran sparse ==
bentuk keluaran : (120, 33) (tipe csr_matrix)
kepadatan : 0.152
skor CV : [0.958, 0.958, 0.833, 0.875, 0.958] rerata 0.917
== Bagian 4: prediksi data baru ==
umur= 28 pendapatan= 6.0 kota=Medan pendidikan=S1 -> berlangganan=1 p=0.988
umur= 55 pendapatan= 20.0 kota=Bandung pendidikan=SMA -> berlangganan=0 p=0.002Mari kita bedah baris-baris yang berperan penting:
- Baris 15–39 (data campuran): dua kolom numerik dan dua kolom kategorikal dibangkitkan dari
rngberseed; label memuat polapendapatandanpendidikansehingga model punya sinyal sungguhan untuk dipelajari. - Baris 42–46 (pemilih kolom):
pilih_angkadanpilih_teksadalah objek pemilih, bukan daftar kolom — pemilihan baru dievaluasi saatfit()melihat DataFrame. Barisprintmembuktikan pengelompokannya tepat. - Baris 49–56 (rangkaian utama):
ColumnTransformermenjadi langkah"pra"di dalamPipeline; dari sudut pandangLogisticRegression, seluruh perutean hanyalah satu transformer biasa. - Baris 58–67 (bentuk, nama, skor): keluaran berbentuk
(120, 8)— dua kolom terstandarkan + enam kolom satu-panas;get_feature_names_out()mencantumkan kedelapan nama fitur dengan prefiks nama tripel; lima lipatan CV menutup Bagian 1 dengan rerata 0,917. - Baris 72–82 (remainder passthrough): hanya
umuryang diskalakan;pendapatan_juta,kota, danpendidikanlewat apa adanya dengan prefiksremainder__— terlihat pada baris pertama[0.467, 'Bandung', 7.93, 'SMA']. - Baris 88–109 (kategori banyak): kolom
kode_posdengan 25 kategori membuat 30 kolom satu-panas; kepadatan total jatuh ke 0,152 — di bawah ambang 0,3 — sehingga keluaran otomatis menjadicsr_matrix. Skor CV pipeline sparse tetap terhitung normal. - Baris 112–125 (prediksi baru): baris berisi kota
Medanyang tidak pernah dilihat encoder tetap diprediksi (peluang 0,988) karenahandle_unknown="ignore"melapisinya dengan vektor nol.
Membaca Nama Fitur Keluaran #
Metode get_feature_names_out() menyusun nama fitur sebagai nama tripel + nama kolom asli (skala__umur), lalu menambahkan kategori untuk transformer yang memekarkannya (satu_pan__kota_Bandung), dan prefiks remainder__ untuk kolom yang dilewatkan. Nama-nama tersebut bukan hiasan: koefisien model dapat dibaca ulang sebagai pengaruh per fitur (materi Section Regresi dan Interpretabilitas), dan nama yang ambigu antar tripel menjadi jelas berkat prefiksnya. Ketika nama seperti itu diperlukan tanpa prefiks, parameter verbose_feature_names_out=False pada ColumnTransformer mematikannya — dengan syarat nama fitur hasilnya tetap unik, karena tabrakan nama antar tripel memicu ValueError saat get_feature_names_out() dipanggil.
Keluaran Sparse dan Ambang 0,3 #
Blok satu-panas berisi hampir seluruh nol: satu baris hanya mengisi satu kolom per kategori asal. ColumnTransformer menghitung kepadatan total keluaran setelah semua tripel digabung; jika kepadatan berada di bawah 0,3 (parameter sparse_threshold), keluaran dikembalikan sebagai matriks sparse csr_matrix — pada Bagian 3, 25 kategori kode pos menjatuhkan kepadatan ke 0,152 sehingga aturan tersebut terpicu. Matriks sparse memori-efisien untuk ratusan hingga ribuan kategori, dan model linear seperti LogisticRegression menerima input tersebut secara langsung. Model berbasis jarak seperti k-NN juga dapat memakainya, tetapi model pohon umumnya lebih suka array padat; parameter sparse_threshold=0.0 memaksa keluaran selalu padat bila diperlukan.
Dengan ColumnTransformer, tabel campuran selesai dirutekan dalam satu objek yang aman dibawa ke cross-validation dan GridSearchCV — parameter preprocessing pun bisa dituning dengan sintaks dua garis bawah, misalnya "pra__satu_pan__min_frequency". Namun katalog transformer bawaan tetap berhenti pada operasi umum: menstandardisasi, meng-encode, mengimputasi. Ketika domain kita menuntut fitur seperti pendapatan per tahun usia atau pemangkasan pencilan pada kuantil terpelajari, kita harus menulis transformer sendiri — dan menuliskannya sedemikian rupa agar tetap menjadi warga kelas satu di dalam Pipeline. Misi tersebut dibuka pada Chapter Transformer Kustom.
Referensi Resmi #
- Column Transformer — scikit-learn User Guide
— bab resmi perutean kolom,
remainder, dan contoh mixed-type table. - sklearn.compose.ColumnTransformer — API
— rujukan parameter
sparse_thresholddanverbose_feature_names_out. - sklearn.compose.make_column_selector — API
— pemilih kolom berbasis
dtype_include/dtype_exclude. - sklearn.preprocessing.OneHotEncoder — API
— opsi
handle_unknown,min_frequency, dansparse_output.