Harga Baju dengan Stacking Ensemble : Tiga atau Satu
oleh Naufal Arkan Wahib
Harga Baju dengan Stacking Ensemble : Tiga atau Satu
oleh Naufal Arkan Wahib
Prediksi harga pakaian terlihat sederhana di permukaan, segelintir atribut kategorikal seperti merek, material, ukuran, dan warna dipetakan ke sebuah harga. Namun membangun model yang dapat menggeneralisasi dengan baik di ruang kombinatorial ini menyimpan beberapa tantangan yang tidak trivial: kategori dengan kardinalitas tinggi, interaksi antar fitur yang tidak terlihat oleh model individual, dan ruang hyperparameter yang terlalu besar untuk dicari secara exhaustif.
Artikel ini membahas bagaimana kami menyelesaikan masalah ini dengan sebuah pipeline yang menggabungkan Leave-One-Out Target Encoding, optimasi hyperparameter berbasis Bayesian melalui Optuna, stacking ensemble dari tiga model berbasis pohon, serta explainability berbasis SHAP. Fokus pembahasan ada pada keputusan engineering di balik setiap komponen dan alasan mengapa mereka dipilih dibanding alternatif yang lebih sederhana.
Latar Belakang
Dataset terdiri dari item pakaian yang dideskripsikan oleh lima atribut kategorikal: Brand, Category, Color, Size, dan Material. Tujuannya adalah memprediksi Price sebagai target kontinu.
Sekilas, ini terlihat seperti pekerjaan untuk pipeline regresi sederhana dengan one-hot encoding dan satu model gradient boosting. Dalam praktiknya, kami menemukan tiga masalah yang saling memperparah satu sama lain:
Pertama, encoding kategorikal yang naif membuang sinyal penting. Label encoding memperkenalkan relasi ordinal palsu. One-hot encoding pada kardinalitas tinggi membuat dimensi meledak dan mengabaikan hubungan statistik antara kategori dan harga target.
Kedua, model individual melewatkan interaksi antar fitur. Merek dan material bersama-sama mungkin membawa sinyal harga yang jauh lebih kuat dibanding keduanya secara terpisah, jaket bulu angsa Nike dan kaos katun Nike memiliki harga yang sangat berbeda. Model tunggal dengan fitur independen tidak dapat menangkap pola ini tanpa rekayasa fitur yang eksplisit.
Ketiga, hyperparameter default berkinerja jauh di bawah potensi model. Model gradient boosting sangat sensitif terhadap konfigurasi hyperparameter-nya, sementara grid search atau random search tidak efisien di ruang yang sebesar ini.
Pipeline yang diuraikan di sini menangani ketiga masalah tersebut secara sistematis.

Feature Encoding: Leave-One-Out Target Encoding
Keputusan desain paling berpengaruh dalam pipeline ini adalah cara variabel kategorikal diencoding.
Target encoding standar mengganti nilai kategori dengan rata-rata nilai target untuk kategori tersebut. Misalnya, Brand=Nike menjadi rata-rata harga semua item Nike dalam dataset. Pendekatan ini efektif, tetapi memperkenalkan kebocoran data (data leakage): ketika nilai target suatu baris berkontribusi pada encoding-nya sendiri, model mempelajari relasi sirkular yang menggelembungkan metrik validasi.
Leave-One-Out (LOO) Encoding menyelesaikan ini dengan mengecualikan baris saat ini dari perhitungan rata-rata selama training. Encoding kategori setiap baris dihitung dari semua baris lain dalam kategori yang sama:
from category_encoders import LeaveOneOutEncoder
cat_features = ['Brand', 'Category', 'Color', 'Size', 'Material']
loo_encoder = LeaveOneOutEncoder(cols=cat_features, random_state=42)
X_train_enc = loo_encoder.fit_transform(X_train, y_train)
X_test_enc = loo_encoder.transform(X_test)
Detail implementasi yang krusial: fit_transform hanya dipanggil pada split training. Test set ditransformasi menggunakan encoder yang sudah di-fit pada data training, artinya encoding untuk test set mencerminkan statistik dari training set saja, bukan dari datanya sendiri. Ini adalah perilaku yang benar untuk evaluasi held-out.
Pada saat inferensi untuk data produksi baru, encoder tidak memiliki “baris lain” sebagai referensi, sehingga ia jatuh kembali ke rata-rata global untuk kategori tersebut. Ini merupakan aproksimasi yang dapat diterima untuk kategori yang terwakili dengan baik, dan terdegradasi secara graceful untuk kategori yang belum pernah dilihat sebelumnya.
Menambahkan Fitur Interaksi

LOO encoding mengonversi setiap kolom kategorikal menjadi satu angka. Untuk memunculkan efek interaksi antar atribut, kami menambahkan fitur produk berpasangan dari semua kombinasi:
from itertools import combinations
def add_interaction_features(df_enc):
df_new = df_enc.copy()
for c1, c2 in combinations(cat_features, 2):
df_new[f'{c1}_x_{c2}'] = df_enc[c1] * df_enc[c2]
return df_new
Ini mengambil representasi encoded dari, katakanlah, Branddan Material, lalu mengalikannya dan menciptakan fitur yang tinggi ketika merek dan material secara individual sama-sama cenderung ke harga tinggi, sekaligus menangkap sinyal gabungan keduanya. Set fitur yang dihasilkan bertumbuh dari 5 menjadi 15 kolom (5 asli + 10 interaksi berpasangan).
Optimasi Hyperparameter: Bayesian Search dengan Optuna
Baik LightGBM maupun XGBoost mengekspos puluhan hyperparameter seperti learning rate, jumlah estimator, kedalaman pohon, regularisasi, rasio subsampling dari masing-masing dengan rentang kontinu atau diskret yang besar. Grid search di ruang ini tidak traktabel secara komputasi. Random search mengeksplorasi ruang secara seragam tetapi membuang banyak trial di area yang jelas-jelas suboptimal.
Optuna menggunakan Tree-structured Parzen Estimator (TPE) untuk memodelkan distribusi probabilitas konfigurasi hyperparameter yang bagus berdasarkan trial-trial sebelumnya. Setiap trial baru diambil dari region yang diestimasi surrogate model sebagai kandidat yang paling menjanjikan dan membuat pencarian semakin terarah seiring bertambahnya trial.
import optuna
def objective_lgb(trial):
params = {
'n_estimators' : trial.suggest_int('n_estimators', 200, 1000),
'learning_rate' : trial.suggest_float('learning_rate', 0.01, 0.2, log=True),
'num_leaves' : trial.suggest_int('num_leaves', 15, 127),
'max_depth' : trial.suggest_int('max_depth', 3, 10),
'min_child_samples': trial.suggest_int('min_child_samples', 5, 50),
'subsample' : trial.suggest_float('subsample', 0.6, 1.0),
'colsample_bytree' : trial.suggest_float('colsample_bytree', 0.6, 1.0),
'reg_alpha' : trial.suggest_float('reg_alpha', 1e-4, 10.0, log=True),
'reg_lambda' : trial.suggest_float('reg_lambda', 1e-4, 10.0, log=True),
'random_state' : 42,
'verbose' : -1,
}
model = lgb.LGBMRegressor(**params)
scores = cross_val_score(
model, X_train_feat, y_train,
cv=KFold(n_splits=5, shuffle=True, random_state=42),
scoring='neg_root_mean_squared_error'
)
return -scores.mean()
study = optuna.create_study(
direction='minimize',
sampler=optuna.samplers.TPESampler(seed=42)
)
study.optimize(objective_lgb, n_trials=50)
Kami menjalankan 50 trial per model, secara empiris cukup untuk ukuran masalah ini. Fungsi objektif mengevaluasi RMSE secara cross-validated pada training set, memastikan bahwa tuning tidak terekspos ke data test di titik mana pun.
Proses tuning yang sama diterapkan secara independen pada XGBoost. Extra Trees, sebaliknya, relatif tidak sensitif terhadap pilihan hyperparameter dan berkinerja memadai dengan konfigurasi tetap (n_estimators=500, max_features='sqrt').
Arsitektur Model: Stacking Ensemble
Dengan model individual yang sudah di-tuning, kami menggabungkannya melalui stacking, sebuah strategi meta-learning di mana prediksi dari base model menjadi fitur untuk learner tingkat kedua.
from sklearn.ensemble import StackingRegressor
from sklearn.linear_model import Ridge
estimators = [
('lgbm', lgb.LGBMRegressor(**best_lgb_params)),
('xgboost', xgb.XGBRegressor(**best_xgb_params)),
('extratrees', ExtraTreesRegressor(n_estimators=500, max_features='sqrt',
min_samples_leaf=2, random_state=42)),
]
stacking_model = StackingRegressor(
estimators=estimators,
final_estimator=Ridge(alpha=1.0),
cv=5,
passthrough=True,
)
stacking_model.fit(X_train_feat, y_train)
Ada dua keputusan desain di sini yang perlu dijelaskan.
Ridge sebagai meta-learner. Model linear dengan regularisasi adalah pilihan yang disengaja untuk final estimator. Kesederhanaannya mencegah meta-learner overfitting pada prediksi out-of-fold dari base model, dan koefisiennya tetap dapat diinterpretasi dan kita dapat mengamati seberapa besar bobot yang diberikannya pada sinyal dari masing-masing base model.
**passthrough=True. **Flag ini meneruskan matriks fitur asli bersama prediksi base model ke meta-learner. Ini memberi Ridge kemampuan untuk mengoreksi kesalahan sistematis dalam prediksi base model dengan merujuk langsung ke input mentah, alih-alih terbatas pada apa yang dipilih base model untuk ditampilkan.
Ketiga base model dipilih karena propertinya yang saling melengkapi. LightGBM menangani fitur sparse berkardinalitas tinggi secara efisien menggunakan konstruksi pohon berbasis histogram. Regularisasi L1/L2 dari XGBoost membuatnya robust terhadap fitur yang berisik. Extra Trees menggunakan randomisasi ekstrem dalam pemilihan split dan ia memiliki bias yang lebih tinggi dari dua model lainnya tetapi variance yang jauh lebih rendah, yang menstabilkan ensemble secara keseluruhan.
Evaluasi
Performa model dinilai dari empat metrik pada data test yang disisihkan: R², RMSE, MAE, dan MAPE. Kami melatih setiap base model secara independen sebagai baseline, lalu membandingkannya dengan stacking ensemble penuh.
def evaluate(name, model, X_tr, y_tr, X_te, y_te):
pred_tr = model.predict(X_tr)
pred_te = model.predict(X_te)
def mape(y_true, y_pred):
mask = y_true != 0
return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100
return {
'Model' : name,
'R² (Test)' : r2_score(y_te, pred_te),
'RMSE (Test)' : np.sqrt(mean_squared_error(y_te, pred_te)),
'MAE (Test)' : mean_absolute_error(y_te, pred_te),
'MAPE (Test)' : mape(y_te.values, pred_te),
}
Stacking ensemble secara konsisten mengungguli setiap model individual di keempat metrik. Ini adalah perilaku yang diharapkan ketika base model cukup beragam dan mereka membuat kesalahan yang berbeda pada subset data yang berbeda, dan meta-learner belajar untuk mengompensasi blind spot dari masing-masing model.
Residual plot mengonfirmasi bahwa error prediksi terdistribusi secara acak di seluruh rentang prediksi, tanpa over- atau under-prediction yang sistematis di segmen harga mana pun dengan tanda bahwa model tidak overfit pada segmen distribusi tertentu.
Explainability: SHAP Values
Akurasi prediksi yang tinggi saja tidak cukup dalam banyak konteks terapan. Memahami fitur mana yang mendorong prediksi penting untuk validasi model, debugging, dan mengomunikasikan hasil kepada pemangku kepentingan non-teknis.

Kami menggunakan SHAP (SHapley Additive exPlanations) dengan Tree Explainer yang diterapkan pada model LightGBM, yang mendukung komputasi Shapley value eksak yang efisien untuk model berbasis pohon:
import shap
explainer = shap.TreeExplainer(lgb_model)
shap_values = explainer.shap_values(X_test_feat)
shap.summary_plot(shap_values, X_test_feat, plot_type='dot', max_display=15)
Beeswarm plot yang dihasilkan menampilkan tiga properti secara bersamaan: pentingnya fitur secara global (urutan vertikal), arah efek masing-masing fitur (posisi horizontal titik relatif terhadap nol), dan korelasi antara nilai fitur dan dampaknya (warna titik). Fitur seperti Brand, Material, dan interaksi keduanya cenderung mendominasi serta konsisten dengan temuan EDA bahwa merek dan material adalah pembeda terkuat dalam distribusi harga.
Untuk penjelasan prediksi individual, force plot menguraikan satu prediksi menjadi kontribusi aditif dari setiap fitur dan berguna untuk mengaudit keputusan model tertentu atau membangun kepercayaan pengguna akhir.
Hasil

[embed]
Pipeline inferensi untuk data baru bersifat langsung: terapkan LOO encoder yang sudah di-fit, hitung fitur interaksi, dan masukkan ke stacking model. Inferensi cepat dan bottleneck ada pada encoder lookup, bukan pada komputasi model.
sample = pd.DataFrame([
{'Brand': 'Nike', 'Category': 'Jacket', 'Color': 'Black', 'Size': 'L', 'Material': 'Wool'},
{'Brand': 'Adidas', 'Category': 'Jeans', 'Color': 'Blue', 'Size': 'M', 'Material': 'Denim'},
])
sample_enc = loo_encoder.transform(sample)
sample_feat = add_interaction_features(sample_enc)
predictions = stacking_model.predict(sample_feat)
Kesimpulan
Hasil evaluasi dari pendekatan stacking ensemble memiliki nilai metrik yang kurang baik, ini disebabkan karena pemanfaatan fitur yang serta merta hanya berdasarkan fitur kategorikal saja, tanpa adanya fitur numerik asli yang tersedia sebelum proses encoding.
메타데이터
- post_id
- d79db0d3b3c9
- slug
- harga-baju-dengan-stacking-ensemble-tiga-atau-satu-d79db0d3b3c9
- url
- https://medium.com/@naufalarkanwahib/harga-baju-dengan-stacking-ensemble-tiga-atau-satu-d79db0d3b3c9
- canonical_url
- https://medium.com/@naufalarkanwahib/harga-baju-dengan-stacking-ensemble-tiga-atau-satu-d79db0d3b3c9
- author_url
- https://medium.com/@naufalarkanwahib
- status
- ok
- fetched_at
- 2026-07-13 06:23:13