Beranda Blog Store
Machine Learning

Membangun Pipeline Machine Learning dengan scikit-learn: Preprocessing, Validasi Silang, dan Penyimpanan Model

23 Agu 2026 Hartono 5 menit baca 4 Dilihat

Halo, Rekan ArtonLabs. Tulisan ini membahas kebiasaan kecil yang membedakan model yang bagus di laptop dari model yang bagus di produksi.

Kesalahan paling umum yang kami lihat di proyek machine learning pemula bukan model yang salah, melainkan preprocessing yang bocor: data uji ikut dipakai saat menghitung rata-rata untuk normalisasi, atau encoder kategori "melihat" label yang tidak ada di data latih. Hasilnya skor yang terlalu bagus di laptop dan mengecewakan di produksi. Pipeline scikit-learn dirancang persis untuk mencegah itu. Tulisan ini membangun satu pipeline tabular lengkap: preprocessing, model, validasi silang, pencarian parameter, dan penyimpanan untuk dipakai aplikasi.

Daftar Isi

Kenapa Pipeline

Pipeline menggabungkan langkah preprocessing dan model menjadi satu objek dengan antarmuka fit dan predict. Saat fit, setiap langkah hanya belajar dari data yang diberikan; saat predict, langkah yang sama diterapkan dengan parameter yang sudah dipelajari. Dengan validasi silang, pipeline memastikan preprocessing di-fit ulang pada tiap lipatan, sehingga tidak ada informasi dari lipatan uji yang bocor. Dan objek tunggal itu yang disimpan dan dimuat di produksi, jadi tidak ada lagi "lupa menormalisasi" di aplikasi.

Data dan Pembagian

import pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_csv("pengajuan.csv")
X = df.drop(columns=["disetujui"])
y = df["disetujui"]
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42)

Data uji disisihkan sekarang dan tidak disentuh sampai akhir. Semua eksperimen, termasuk pencarian parameter, dilakukan pada data latih dengan validasi silang.

Preprocessing dengan ColumnTransformer

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer

numerik = ["usia", "penghasilan", "lama_kerja"]
kategori = ["kota", "pekerjaan", "status"]

pra_numerik = Pipeline([("isi", SimpleImputer(strategy="median")),
                        ("skala", StandardScaler())])
pra_kategori = Pipeline([("isi", SimpleImputer(strategy="most_frequent")),
                         ("onehot", OneHotEncoder(handle_unknown="ignore"))])

pra = ColumnTransformer([("num", pra_numerik, numerik),
                         ("kat", pra_kategori, kategori)])

handle_unknown="ignore" penting untuk produksi: kota baru yang tidak ada saat pelatihan tidak akan membuat prediksi gagal. Imputer menangani nilai kosong yang pasti muncul di data sungguhan. Semua parameter ini (median, kategori yang dikenal) dipelajari hanya dari data latih.

Menyusun Pipeline dan Melatih

from sklearn.ensemble import GradientBoostingClassifier

pipa = Pipeline([("pra", pra),
                 ("model", GradientBoostingClassifier(random_state=42))])
pipa.fit(X_latih, y_latih)
print("akurasi uji:", pipa.score(X_uji, y_uji))

Satu pemeriksaan kecil yang layak dibiasakan: cetak pipa.named_steps["pra"].get_feature_names_out()[:20] setelah fit untuk melihat kolom apa saja yang benar-benar masuk ke model. Kolom id, nomor urut, atau stempel waktu yang ikut terbawa adalah sumber kebocoran yang paling umum, dan ia terlihat jelas di daftar itu.

Gradient boosting adalah titik awal yang kuat untuk data tabular. Untuk baseline yang bisa dijelaskan, ganti dengan LogisticRegression; selisih skornya memberi tahu seberapa besar nilai tambah model yang lebih rumit.

Validasi Silang

from sklearn.model_selection import cross_val_score, StratifiedKFold
lipat = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
skor = cross_val_score(pipa, X_latih, y_latih, cv=lipat, scoring="f1")
print(skor.round(3), skor.mean().round(3), "+/-", skor.std().round(3))

Gunakan shuffle=True dengan random_state tetap agar lipatannya acak tetapi dapat diulang; data yang terurut menurut waktu atau kelas tanpa pengocokan menghasilkan lipatan yang tidak mewakili.

Lima lipatan memberi rata-rata dan sebaran, dan sebaran itu yang sering dilupakan: F1 0,82 ± 0,01 dan 0,82 ± 0,08 adalah dua model yang sangat berbeda keandalannya. Pilih metrik sesuai masalah; untuk kelas tidak seimbang, f1, roc_auc, atau average_precision lebih jujur daripada akurasi.

Mencari Parameter

from sklearn.model_selection import GridSearchCV
param = {"model__n_estimators": [100, 300],
         "model__learning_rate": [0.05, 0.1],
         "model__max_depth": [2, 3]}
cari = GridSearchCV(pipa, param, cv=lipat, scoring="f1", n_jobs=-1)
cari.fit(X_latih, y_latih)
print(cari.best_params_, cari.best_score_.round(3))
terbaik = cari.best_estimator_

Nama parameter memakai awalan nama langkah (model__), jadi parameter preprocessing pun bisa ikut dicari, misalnya strategi imputasi. Untuk ruang parameter yang besar, RandomizedSearchCV dengan beberapa puluh iterasi biasanya menemukan hasil yang hampir sama dalam waktu jauh lebih singkat. Baru setelah ini, sekali saja, evaluasi terbaik pada X_uji.

Menyimpan dan Memuat Model

import joblib, sklearn
joblib.dump({"pipeline": terbaik, "versi_sklearn": sklearn.__version__,
             "kolom": list(X.columns)}, "model-v1.joblib")

bundel = joblib.load("model-v1.joblib")
model = bundel["pipeline"]

Simpan versi scikit-learn dan daftar kolom bersama modelnya. Model yang disimpan dengan satu versi tidak dijamin bisa dimuat di versi lain, dan daftar kolom mencegah aplikasi mengirim data dengan urutan yang salah. Beri nama berkas dengan versi, dan simpan juga skor validasi dan tanggal data; saat model v3 berperilaku aneh, Anda bisa kembali ke v2 dengan percaya diri.

Menjelaskan Prediksi

from sklearn.inspection import permutation_importance
nama = terbaik.named_steps["pra"].get_feature_names_out()
pi = permutation_importance(terbaik, X_uji, y_uji, n_repeats=10, random_state=42, scoring="f1")
for i in pi.importances_mean.argsort()[::-1][:10]:
    print(f"{X_uji.columns[i] if i < len(X_uji.columns) else nama[i]}: {pi.importances_mean[i]:.3f}")

Permutation importance mengacak satu kolom dan mengukur seberapa jatuh skornya; kolom yang penting membuat skor jatuh banyak. Ini bekerja pada pipeline utuh dan model apa pun. Kalau kolom yang paling penting ternyata sesuatu yang tidak masuk akal (misalnya nomor urut), itu tanda kebocoran: kolom tersebut mengandung informasi yang tidak akan tersedia saat prediksi sungguhan. Untuk penjelasan per prediksi, pustaka SHAP bekerja dengan model pohon dan pipeline ini.

Memantau Pergeseran Data

Model dilatih pada data masa lalu, dan dunia berubah. Simpan statistik sederhana kolom masukan saat pelatihan (rata-rata, sebaran, proporsi kategori), lalu bandingkan dengan data yang datang tiap bulan. Kalau proporsi "kota" baru melewati beberapa persen, atau rata-rata "penghasilan" bergeser jauh, model perlu dilatih ulang meski skornya belum terlihat turun, karena skor sebenarnya baru diketahui setelah kenyataannya terjadi. Catatan prediksi dari bagian berikut adalah bahan untuk pemeriksaan ini.

Memakai di Produksi

baris = pd.DataFrame([{"usia": 34, "penghasilan": 7500000, "lama_kerja": 6,
                       "kota": "Bandar Lampung", "pekerjaan": "swasta", "status": "menikah"}])
print(model.predict(baris), model.predict_proba(baris).round(3))

Muat model sekali saat aplikasi mulai. Kirim data sebagai DataFrame dengan kolom yang sama seperti saat pelatihan; pipeline yang mengurus sisanya. Catat setiap prediksi bersama masukannya, karena dalam beberapa bulan Anda akan ingin membandingkan prediksi dengan kenyataan, dan itulah data latih untuk versi berikutnya. Kunci versi scikit-learn di lock file proyek, seperti dibahas di artikel dependensi, supaya pembaruan tidak diam-diam mengubah perilaku model yang sudah dimuat.

Semoga membantu, Rekan ArtonLabs. Kalau pipeline Anda menemukan kebocoran yang tidak terduga, kami ingin mendengar ceritanya.