Bagian pertama menutup eksplorasi dengan lima keputusan desain: imputasi median, standardisasi, one-hot encoding, kandidat model pohon, dan kolinearitas yang dibaca hati-hati. Bagian ini menerjemahkan lima keputusan tersebut menjadi satu objek Python yang bisa dilatih, disimpan, dan dipanggil ulang: ColumnTransformer yang membelah fitur numerik dan kategorikal ke jalur masing-masing, dibungkus Pipeline scikit-learn bersama estimator. Fokusnya bukan sekadar “transformasi data berhasil”, melainkan arsitektur yang menjamin transformasi yang sama berlaku saat pelatihan, evaluasi, dan inferensi — jaminan yang kelak menjadi syarat mutlak ketika model dikemas menjadi API di Bagian 4.
Mengapa Preprocessing Harus Dibungkus, Bukan Dicerca #
Cara naif menangani preprocessing adalah menulisnya berbaris-baris: isi nilai hilang, simpan mediannya di variabel, skala fitur, simpan rata-ratanya, encode kategorikal, lalu ulangi seluruh rangkaian itu dengan variabel yang sama saat data baru datang. Pendekatan tercecer ini punya dua penyakit klasik. Pertama, data leakage: jika imputer atau scaler di-fit pada seluruh data (termasuk data uji), informasi dari data uji merembes ke pelatihan dan skor evaluasi menjadi terlalu optimis. Kedua, ketidaksesuaian produksi: transformasi yang ditulis manual di dua tempat berbeda hampir pasti berbeda — satu lupa clipping, satu lagi memakai mean alih-alih median — dan model menerima masukan yang tidak sama dengan yang dilihat saat dilatih.
| Pendekatan | Kebocoran data | Konsistensi inferensi | Bisa disimpan sebagai satu objek |
|---|---|---|---|
| Transformasi manual tercecer | rentan (fit di data penuh) | tidak dijamin | tidak |
| ColumnTransformer tanpa Pipeline | aman jika dipakai disiplin | harus dijalankan terpisah dari model | preprocessing saja |
| ColumnTransformer di dalam Pipeline | aman secara konstruksi | otomatis sama saat fit/predict | ya, satu objek |
Tiga baris tabel tersebut adalah alasan arsitektur proyek ini memilih baris ketiga. Pipeline mengeksekusi langkah-langkah secara berurutan: fit() hanya memanggil fit_transform() pada langkah pra-proses lalu fit() pada estimator, sementara predict() memanggil transform() lalu predict() — data uji dan data baru tidak pernah menyentuh proses pembelajaran transformasi.
Merakit ColumnTransformer Dua Cabang #
Skrip 03_pipeline.py membangun arsitektur tersebut lalu mengujinya dengan tiga uji tarik: bentuk matriks keluaran, nama fitur hasil encoding, dan perilaku terhadap nilai hilang serta kategori yang tidak pernah dilihat saat pelatihan.
# Bangun pipeline preprocessing kampanye: ColumnTransformer dengan dua
# cabang (numerik: imputasi median + StandardScaler; kategorikal: OneHotEncoder
# handle_unknown="ignore") yang dibungkus Pipeline bersama estimator. Objek
# tunggal ini menelan DataFrame mentah dan siap untuk pelatihan, evaluasi,
# maupun inferensi tanpa transformasi manual yang tercecer.
# Jalankan dari root proyek: python3 src/03_pipeline.py
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
FITUR_NUM = ["anggaran_ribu", "durasi_hari", "audiens_ribu", "ctr_persen"]
FITUR_KAT = ["kanal", "segmen", "musim"]
df = pd.read_csv("data/kampanye.csv")
X = df[FITUR_NUM + FITUR_KAT]
y = df["berhasil"].to_numpy()
# ---------- Pembagian latih-uji (dipakai ulang seluruh proyek) ----------
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
print("== Pembagian data ==")
print(f"latih : {len(X_train)} baris, positif {y_train.mean():.1%}")
print(f"uji : {len(X_test)} baris, positif {y_test.mean():.1%}")
# ---------- Arsitektur ColumnTransformer ----------
pra_proses = ColumnTransformer(
[
(
"angka",
Pipeline(
[
("imputasi", SimpleImputer(strategy="median")),
("skala", StandardScaler()),
]
),
FITUR_NUM,
),
(
"kategori",
OneHotEncoder(handle_unknown="ignore", sparse_output=False),
FITUR_KAT,
),
]
)
# ---------- Fit pada data latih, transform kedua subset ----------
X_train_tr = pra_proses.fit_transform(X_train)
X_test_tr = pra_proses.transform(X_test)
print("\n== Hasil fit dan transform ==")
print(f"matriks latih : {X_train_tr.shape}")
print(f"matriks uji : {X_test_tr.shape}")
print(f"nilai NaN : {int(np.isnan(X_train_tr).sum())} (latih), "
f"{int(np.isnan(X_test_tr).sum())} (uji)")
nama_fitur = list(pra_proses.get_feature_names_out())
print(f"jumlah fitur keluaran : {len(nama_fitur)}")
for i, nama in enumerate(nama_fitur):
print(f" [{i:2d}] {nama}")
# ---------- Nilai hilang dan kategori tak dikenal ----------
contoh_mentah = pd.DataFrame(
{
"anggaran_ribu": [np.nan, 430.0],
"durasi_hari": [14, 9],
"audiens_ribu": [1150.0, 980.0],
"ctr_persen": [np.nan, 1.6],
"kanal": ["podcast", "tampilan"],
"segmen": ["akuisisi", "retensi"],
"musim": ["promo", "reguler"],
}
)
contoh_tr = pra_proses.transform(contoh_mentah)
print("\n== Uji nilai hilang dan kategori tak dikenal ==")
print("input mentah:")
print(contoh_mentah.to_string(index=False))
print("hasil transform (13 kolom):")
for baris in contoh_tr:
print(" " + " ".join(f"{v:+7.3f}" for v in baris))
print("kanal 'podcast' tidak dikenal -> seluruh kolom one-hot kanal = 0;"
" nilai hilang diganti median lalu diskalakan")
# ---------- Pipeline utuh: preprocessing + estimator ----------
pipeline_demo = Pipeline(
[
("pra", pra_proses),
("model", LogisticRegression(max_iter=1000)),
]
)
pipeline_demo.fit(X_train, y_train)
peluang = pipeline_demo.predict_proba(contoh_mentah)[:, 1]
print("\n== Pipeline utuh (preprocessing + LogisticRegression) ==")
print(f"baris 0 (podcast, banyak nilai hilang): peluang {peluang[0]:.3f}")
print(f"baris 1 (tampilan, data lengkap) : peluang {peluang[1]:.3f}")
print("pipeline menerima DataFrame mentah langsung - transformasi")
print("yang sama otomatis berlaku saat fit maupun predict")Output yang Dihasilkan:
== Pembagian data ==
latih : 1600 baris, positif 40.1%
uji : 400 baris, positif 40.0%
== Hasil fit dan transform ==
matriks latih : (1600, 13)
matriks uji : (400, 13)
nilai NaN : 0 (latih), 0 (uji)
jumlah fitur keluaran : 13
[ 0] angka__anggaran_ribu
[ 1] angka__durasi_hari
[ 2] angka__audiens_ribu
[ 3] angka__ctr_persen
[ 4] kategori__kanal_email
[ 5] kategori__kanal_medsos
[ 6] kategori__kanal_penelusuran
[ 7] kategori__kanal_tampilan
[ 8] kategori__segmen_akuisisi
[ 9] kategori__segmen_reaktivasi
[10] kategori__segmen_retensi
[11] kategori__musim_promo
[12] kategori__musim_reguler
== Uji nilai hilang dan kategori tak dikenal ==
input mentah:
anggaran_ribu durasi_hari audiens_ribu ctr_persen kanal segmen musim
NaN 14 1150.0 NaN podcast akuisisi promo
430.0 9 980.0 1.6 tampilan retensi reguler
hasil transform (13 kolom):
-0.221 -0.289 +1.131 -0.198 +0.000 +0.000 +0.000 +0.000 +1.000 +0.000 +0.000 +1.000 +0.000
+0.933 -0.911 +0.746 -0.974 +0.000 +0.000 +0.000 +1.000 +0.000 +0.000 +1.000 +0.000 +1.000
kanal 'podcast' tidak dikenal -> seluruh kolom one-hot kanal = 0; nilai hilang diganti median lalu diskalakan
== Pipeline utuh (preprocessing + LogisticRegression) ==
baris 0 (podcast, banyak nilai hilang): peluang 0.640
baris 1 (tampilan, data lengkap) : peluang 0.163
pipeline menerima DataFrame mentah langsung - transformasi
yang sama otomatis berlaku saat fit maupun predictMari kita bedah arsitektur tersebut baris demi baris:
- Baris 17–22 (kolom dan data): empat fitur numerik dan tiga kategorikal dideklarasikan sebagai daftar eksplisit — kolom
id_kampanyedanberhasiltidak pernah masukX, sehingga tidak ada jalan bagi kebocoran penanda ke fitur. - Baris 25–27 (pembagian latih-uji):
test_size=0.2denganstratify=ymenjaga proporsi kelas positif (40,1% latih, 40,0% uji) danrandom_state=42membekukan pembagian yang sama untuk seluruh proyek — Bagian 3 sampai 5 memakai belahan identik. - Baris 33–51 (ColumnTransformer): cabang
"angka"adalahPipelinekecil berisiSimpleImputer(strategy="median")laluStandardScaler()— urutan ini penting: median dihitung dari data latih, lalu skala memakai rata-rata dan simpangan baku data yang sudah terisi. Cabang"kategori"memakaiOneHotEncoder(handle_unknown="ignore")sehingga kategori baru tidak memicu galat. - Baris 54–55 (fit versus transform):
fit_transformhanya padaX_train;X_testcukuptransform— inilah penerapan langsung disiplin anti-kebocoran pada tabel sebelumnya. - Baris 62–65 (get_feature_names_out): tiga belas fitur keluaran bisa dipanggil namanya (
angka__anggaran_ribuhinggakategori__musim_reguler) — nama ini kelak dipakai membaca koefisien model linear dan pentingnya fitur model pohon di Bagian 3. - Baris 67–87 (uji tekanan): dua baris uji sengaja dibuat sulit — satu baris memuat
np.nanpada anggaran dan CTR plus kanal"podcast"yang tidak ada di data latih. Keluarannya tenang: nilai hilang diganti median latih lalu diskalakan, dan keempat kolom one-hot kanal bernilai nol untuk"podcast"alih-alih gagal. - Baris 90–97 (Pipeline utuh): preprocessing dan
LogisticRegressiondirakit jadi satu objek;fit()sekali danpredict_proba()menerima DataFrame mentah langsung. Model demo ini hanya pembuktian mekanisme — pemilihan model sungguhan menunggu kontes Bagian 3.
Hasil Uji dan Maknanya #
Keluaran skrip menjawab ketiga uji tarik secara ringkas. Matriks latih berukuran 1600×13 dan uji 400×13 — empat kolom numerik bertahan, sembilan kolom lahir dari encoding (4 kanal + 3 segmen + 2 musim), dan tidak ada satu pun NaN tersisa. Baris uji "podcast" menghasilkan nol pada seluruh blok kanal, sementara nilai hilangnya diganti median latih sebelum diskalakan. Terakhir, pipeline utuh memberi peluang 0,640 untuk kampanye akuisisi bermusim promo dengan kanal tak dikenal dan 0,163 untuk kampanye retensi reguler di kanal tampilan — dua angka yang menegaskan bahwa seluruh rantai transformasi bekerja tanpa sentuhan manual.
Arsitektur kini berdiri, tetapi masih kosong: cabang estimator diisi model demo tanpa pertimbangan. Bagian 3: Pelatihan dan Pemilihan Model mengisi kursi tersebut lewat kontes terbuka — regresi logistik, LightGBM, dan Random Forest bertarung dalam GridSearchCV dengan cross-validation berstrata, dan hanya pemilik skor CV terbaik yang berhak melangkah ke penyimpanan dan serving.
Referensi Resmi #
- sklearn.compose.ColumnTransformer — API
— pemetaan kolom ke transformator dengan
get_feature_names_out. - sklearn.pipeline.Pipeline — API — rantai fit/transform/predict yang mencegah kebocoran data.
- sklearn.preprocessing.OneHotEncoder — API
— parameter
handle_unknowndansparse_output.