Eksplorasi pada Bagian 1 menutup dengan dua fakta yang menuntun bagian ini: prediktor churn sudah terlihat (tenure, komplain, layanan), dan kelasnya timpang 2,7 berbanding 1. Pekerjaan sekarang adalah menyiapkan mesin yang mengubah fakta tersebut menjadi model siap latih — fitur dipisahkan dari label, kolom numerik dan kategorikal diproses pada jalurnya masing-masing di dalam satu Pipeline, dan data dibagi menjadi latih serta uji secara stratified. Satu aturan mutlak berlaku sepanjang proyek: pembagian data dilakukan lebih dulu, sebelum resampling apa pun disentuhkan.
Memisahkan Fitur dan Label #
Kolom churn adalah label; lima kolom sisanya adalah fitur. Pemisahan dilakukan dengan X = df.drop(columns="churn") dan y = df["churn"] agar tidak ada nama kolom yang perlu diketik ulang — daftar fitur mengikuti struktur DataFrame secara otomatis. Dari lima fitur tersebut, empat bersifat numerik (usia, tenure_bulan, tagihan_bulanan_ribu, komplain_12bulan) dan satu kategorikal (layanan); kedua jenis ini membutuhkan perlakuan berbeda sehingga daftarnya dinyatakan eksplisit.
ColumnTransformer di dalam Pipeline #
ColumnTransformer membagi tabel menjadi dua jalur. Jalur angka berisi sub-Pipeline SimpleImputer(strategy="median") lalu StandardScaler; jalur kategori berisi OneHotEncoder(handle_unknown="ignore") yang memecah kolom layanan menjadi tiga kolom biner. Hasil kedua jalur direkat kembali menjadi satu matriks berisi tujuh fitur.
Tiga keputusan desain di dalamnya sengaja diambil untuk dunia nyata, bukan untuk data sintetis yang sudah bersih. Pertama, imputer tetap dipasang meski data saat ini tidak punya nilai hilang: saat pipeline ini menerima data produksi yang bolong, pipeline tetap hidup dan tidak runtuh di tengah jalan. Kedua, handle_unknown="ignore" membuat kategori baru yang tidak ditemui saat latih (misal paket layanan yang baru diluncurkan) diberi nilai nol pada seluruh kolom one-hot, bukan galat runtime. Ketiga, StandardScaler menyetarakan skala fitur sehingga LogisticRegression yang kita latih pada Bagian 3 konvergen cepat dan koefisiennya dapat dibandingkan.
Sebagai model sementara dipasang DummyClassifier(strategy="prior") — model yang selalu menebak kelas mayoritas. Model tiruan ini bukan hiasan: model inilah garis dasar yang harus dikalahkan, dan perilakunya memperlihatkan mengapa akurasi saja tidak cukup pada data timpang.
# Preprocessing dan split: pisahkan fitur-label, bangun ColumnTransformer
# di dalam Pipeline, lalu bagi data secara stratified SEBELUM resampling
# apa pun dilakukan. Model placeholder DummyClassifier menunjukkan mengapa
# akurasi saja menyesatkan pada data imbalance.
# Jalankan dari root proyek: python3 src/03_preprocessing_split.py
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.impute import SimpleImputer
from sklearn.metrics import accuracy_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
df = pd.read_csv("data/churn.csv")
X = df.drop(columns="churn")
y = df["churn"]
fitur_numerik = ["usia", "tenure_bulan", "tagihan_bulanan_ribu",
"komplain_12bulan"]
fitur_kategorikal = ["layanan"]
# ---------- Pra-pemrosesan: ColumnTransformer dalam Pipeline ----------
pra = ColumnTransformer(
transformers=[
(
"angka",
Pipeline(
steps=[
("imputasi", SimpleImputer(strategy="median")),
("skala", StandardScaler()),
]
),
fitur_numerik,
),
(
"kategori",
OneHotEncoder(handle_unknown="ignore"),
fitur_kategorikal,
),
]
)
# ---------- Split stratified: dilakukan SEBELUM SMOTE ----------
X_latih, X_uji, y_latih, y_uji = train_test_split(
X, y, test_size=0.25, stratify=y, random_state=42
)
print("== Pembagian data (stratified, random_state=42) ==")
print(f"total : {len(X)} baris")
print(f"data latih : {len(X_latih)} baris")
print(f"data uji : {len(X_uji)} baris")
print("\n== Proporsi kelas tetap terjaga ==")
print(f"{'bagian':10s} {'bertahan':>9s} {'churn':>9s}")
for nama, deret in [("latih", y_latih), ("uji", y_uji), ("total", y)]:
n0 = int((deret == 0).sum())
n1 = int((deret == 1).sum())
print(f"{nama:10s} {n0:6d} ({n0 / len(deret):4.1%})"
f" {n1:4d} ({n1 / len(deret):4.1%})")
# ---------- Fit pipeline pada data latih saja ----------
pipeline = Pipeline(
steps=[
("pra", pra),
("model", DummyClassifier(strategy="prior")),
]
)
pipeline.fit(X_latih, y_latih)
nama_fitur = pipeline.named_steps["pra"].get_feature_names_out()
print("\n== Fitur hasil pra-pemrosesan ==")
print(f"jumlah fitur : {len(nama_fitur)}")
print(f"nama fitur : {list(nama_fitur)}")
X_latih_tr = pipeline.named_steps["pra"].transform(X_latih)
singkat = []
for n in nama_fitur:
s = n.split("__")[-1].replace("layanan_", "")
s = (s.replace("tagihan_bulanan_ribu", "tagihan")
.replace("komplain_12bulan", "komplain")
.replace("tenure_bulan", "tenure"))
singkat.append(s)
print("\n== Tiga baris pertama data latih setelah transformasi ==")
print(f"{'baris':>5s} " + " ".join(f"{s:>9s}" for s in singkat))
for i in range(3):
nilai = " ".join(f"{X_latih_tr[i, j]:9.3f}" for j in range(len(singkat)))
print(f"{i:5d} {nilai}")
X_uji_tr = pipeline.named_steps["pra"].transform(X_uji)
print(f"\nbentuk matriks latih : {X_latih_tr.shape}")
print(f"bentuk matriks uji : {X_uji_tr.shape}")
# ---------- Baseline tanpa kecerdasan: akurasi menyesatkan ----------
prediksi = pipeline.predict(X_uji)
print("\n== Baseline DummyClassifier(strategy='prior') pada data uji ==")
print(f"akurasi : {accuracy_score(y_uji, prediksi):.3f}")
print(f"recall kelas churn : {recall_score(y_uji, prediksi):.3f}")
print("model menebak 'bertahan' untuk semua pelanggan")Output yang Dihasilkan:
== Pembagian data (stratified, random_state=42) ==
total : 500 baris
data latih : 375 baris
data uji : 125 baris
== Proporsi kelas tetap terjaga ==
bagian bertahan churn
latih 274 (73.1%) 101 (26.9%)
uji 91 (72.8%) 34 (27.2%)
total 365 (73.0%) 135 (27.0%)
== Fitur hasil pra-pemrosesan ==
jumlah fitur : 7
nama fitur : ['angka__usia', 'angka__tenure_bulan', 'angka__tagihan_bulanan_ribu', 'angka__komplain_12bulan', 'kategori__layanan_Bundle', 'kategori__layanan_Internet', 'kategori__layanan_Mobile']
== Tiga baris pertama data latih setelah transformasi ==
baris usia tenure tagihan komplain Bundle Internet Mobile
0 0.762 0.270 0.581 1.286 0.000 1.000 0.000
1 0.467 1.032 -0.509 1.286 0.000 1.000 0.000
2 0.983 -1.635 0.448 -0.872 0.000 0.000 1.000
bentuk matriks latih : (375, 7)
bentuk matriks uji : (125, 7)
== Baseline DummyClassifier(strategy='prior') pada data uji ==
akurasi : 0.728
recall kelas churn : 0.000
model menebak 'bertahan' untuk semua pelangganMari kita bedah skrip preprocessing dan split:
- Baris 18–23 (
X,y, daftar fitur): label dipisahkan dengandrop, lalu fitur dikelompokkan menjadi numerik dan kategorikal. Daftar eksplisit ini menjadi kontrak yang dipakaiColumnTransformer. - Baris 26–44 (
ColumnTransformer): jalurangkaberisi sub-pipeline imputasi median dan penskalaan; jalurkategoriberisiOneHotEncoderdenganhandle_unknown="ignore". Setiap transformer menerima daftar kolomnya sendiri sehingga urutan kolom DataFrame tidak memengaruhi hasil. - Baris 47–49 (
train_test_split): 25% data menjadi uji;stratify=ymenjaga proporsi kelas;random_state=42membekukan pembagian sehingga Bagian 3 dan Bagian 4 memperoleh potongan data yang persis sama. - Baris 58–62 (tabel proporsi kelas): keluaran menunjukkan latih 73,1% berbanding 26,9% dan uji 72,8% berbanding 27,2% — keduanya menyalin proporsi data asli 73,0% berbanding 27,0% dengan selisih kurang dari setengah poin persentase.
- Baris 65–71 (
Pipelinedanfit):pradan model dummy dirakit menjadi satuPipelinelalu dilatih hanya pada data latih. Seluruh status pembelajaran (median imputasi, rata-rata dan simpangan baku scaler, kategori one-hot) dihitung dari 375 baris latih — data uji belum disentuh sama sekali. - Baris 73–76 (
get_feature_names_out): pipeline melaporkan tujuh fitur keluaran: empat kolom numerik berskala ditambah tiga kolomlayanan_Bundle,layanan_Internet,layanan_Mobile. - Baris 78–94 (
transform): tiga baris pertama data latih ditampilkan setelah transformasi — nilai numerik sudah terskala (sekitar nol) dan kolom layanan menjadi indikator biner. Data uji kemudian melewatitransformdengan status yang sama, bukanfit_transform. - Baris 97–101 (baseline):
DummyClassifiermencetak akurasi 0,728 dan recall 0,000 pada data uji.
Stratifikasi Sebelum Resampling #
Urutan operasi pada skrip ini — split lebih dulu, resampling belakangan — bukan gaya-gayaan. SMOTE bekerja dengan mencari tetangga terdekat antar-sampel kelas minoritas lalu menyisipkan sampel sintetis di antara tetangga tersebut. Jika resampling dilakukan sebelum split, pasangan tetangga bisa terbelah: satu masuk data latih, pasangannya yang sintetis masuk data uji. Data uji lalu berisi sampel yang nyaris merupakan salinan data latih, skor evaluasi melonjak, dan model tampak jauh lebih pintar daripada kenyataannya — bentuk data leakage yang dibahas tuntas pada chapter Data Leakage dan Cara Mencegahnya.
Baseline Tanpa Model dan Jebakan Akurasi #
Keluaran akhir skrip memperlihatkan jebakan yang dijanjikan Bagian 1: DummyClassifier(strategy="prior") mencapai akurasi 0,728 pada data uji dengan cara menebak “bertahan” untuk seluruh 125 pelanggan. Angka 0,728 ini nyaris menyamai akurasi model sungguhan yang akan kita latih, padahal recall model tiruan tersebut nol — tidak satu pun dari 34 pelanggan churn tertangkap. Kesimpulannya tegas: sejak Bagian 3, setiap model dibandingkan pada recall dan PR-AUC, karena akurasi tidak lagi mampu membedakan model berguna dari model yang hanya ikut mayoritas.
Mesin preprocessing teruji dan pembagian data terkunci. Pintu berikutnya terbuka pada Bagian 3: Model dan Penanganan Imbalance — di sana tiga konfigurasi LogisticRegression (tanpa penanganan, class_weight="balanced", dan SMOTE di dalam pipeline imblearn) berlomba menangkap pelanggan churn tanpa menjatuhkan presisi.
Referensi Resmi #
- sklearn.model_selection.train_test_split — API
— parameter
stratifydanrandom_state. - sklearn.compose.ColumnTransformer — API — pemetaan kolom ke transformer.
- sklearn.preprocessing.OneHotEncoder — API
— opsi
handle_unknown. - sklearn.dummy.DummyClassifier — API
— baseline
strategy="prior".