05 โ Feature Engineering
Estimasi: 4 jam Tujuan: Skill yang sering lebih penting dari model selection. Feature bagus + model sederhana > feature jelek + model complex.
Kenapa Materi Ini Penting?
Pepatah Kaggle: "Feature engineering wins competitions, not algorithms." Andrew Ng pernah bilang 80% waktu data scientist habis di feature engineering. Algoritma terbaik dengan feature jelek akan kalah dari algoritma sederhana dengan feature kaya. Model XGBoost yang fancy tidak bisa "menebak" bahwa hari Sabtu = weekend kalau kamu cuma kasih raw timestamp.
Untuk bootcamp Dicoding, feature engineering yang bagus adalah pembeda antara siswa yang dapet leaderboard top dan yang biasa-biasa. Saat reviewer expert mengevaluasi project kamu, yang mereka lihat bukan "model apa yang dipakai" tapi "feature apa yang kamu buat dan kenapa". Ini skill yang butuh latihan, intuisi, dan domain knowledge.
flowchart TD
A["๐ฅ Raw Data"] --> B["๐งน Cleaning"]
B --> C["๐ข Encoding<br/>Categorical"]
B --> D["โ๏ธ Scaling<br/>Numerical"]
B --> E["๐
Datetime<br/>Features"]
B --> F["๐จ Feature<br/>Creation"]
C --> G["๐ค Model"]
D --> G
E --> G
F --> G
G --> H["๐ Better<br/>Performance"]
style F fill:#fff4d4
style H fill:#d4f4dd
Analogi: Chef Menyiapkan Bahan Sebelum Masak
Chef restoran bintang 5 tidak langsung lempar bahan mentah ke wajan. Dia: cuci, potong, marinate, ukur, susun di tray. Saat masak, semua sudah siap, tinggal kombinasi. Feature engineering itu mise en place โ persiapan bahan. Algoritma ML adalah wajan dan kompor. Bahan jelek + chef hebat = makanan biasa. Bahan premium + chef biasa = makanan enak.
Bagian 1 โ Encoding Categorical
Analogi: Bahasa Kategori โ Bahasa Angka
Model ML cuma bisa baca angka. "Bandung", "Jakarta", "Surabaya" harus dikonversi jadi angka. Tapi cara konversinya penting: salah encode bisa kasih sinyal palsu ke model.
Cara Membaca Diagram:
- Atas: starting point (categorical data)
- Tengah: cek urutan dan cardinality
- Bawah: rekomendasi method
Walkthrough Step-by-Step:
- Apakah ada urutan natural? (Low/Medium/High = ya, Bandung/Jakarta/Surabaya = tidak)
- Ada urutan โ Label Encoding (0, 1, 2)
- Tidak ada urutan โ cek jumlah unique values
- < 10 โ One-Hot. 10-50 โ Target encoding. > 50 โ Frequency / Embedding
Analogi Sehari-hari: Cara nulis nilai pelajaran. Ranking 1-3 = ordinal (Label encoding). Mata pelajaran = nominal (One-Hot). Postcode 50.000 unique = frequency / embedding.
Diagram statis Mermaid sebagai fallback:
flowchart TD
A["๐ท๏ธ Categorical Data"] --> B{"Ada urutan?"}
B -->|Ya, ordinal| C["๐ข Label Encoding<br/>(Low=0, Med=1, High=2)"]
B -->|Tidak, nominal| D{"Berapa unique<br/>values?"}
D -->|< 10| E["๐ฏ One-Hot Encoding"]
D -->|10-50| F["๐ Target Encoding"]
D -->|> 50| G["๐ Frequency Encoding<br/>atau Embedding"]
style E fill:#d4f4dd
style F fill:#d4f4dd
One-Hot Encoding
import pandas as pd
df_encoded = pd.get_dummies(df["kota"], prefix="kota")
# atau sklearn
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
encoded = ohe.fit_transform(df[["kota"]])
Hati-hati: kolom dengan banyak kategori (>50) โ terlalu banyak fitur. Pakai target encoding atau frequency encoding.
Label Encoding
Untuk ordinal (ada urutan):
mapping = {"Low": 0, "Medium": 1, "High": 2}
df["priority_le"] = df["priority"].map(mapping)
โ Jangan pakai untuk nominal (kota) โ model akan asumsi ada urutan.
Frequency Encoding
freq = df["kota"].value_counts().to_dict()
df["kota_freq"] = df["kota"].map(freq)
Target Encoding (Mean Encoding)
target_mean = df.groupby("kota")["target"].mean().to_dict()
df["kota_target"] = df["kota"].map(target_mean)
Hati-hati: target encoding bisa data leakage. Hitung di train saja, apply ke test.
Bagian 2 โ Scaling Numerik
Analogi: Menyamaratakan Skala Berat dan Tinggi
Bayangkan kamu compare 2 orang: tinggi (cm, range 150-200) dan berat (kg, range 40-100). Kalau tidak di-scale, algoritma berbasis jarak akan menganggap perbedaan tinggi 50cm jauh lebih signifikan dari perbedaan berat 60kg karena angka cm absolutnya lebih besar. Padahal secara proporsi, perbedaan berat 60kg justru lebih ekstrem.
Cara Membaca Diagram:
- Atas: numerical data sebagai starting point
- Branch tree-based vs lainnya
- Branch outlier vs distribution
- Bawah: pilihan scaler
Walkthrough Step-by-Step:
- Tree-based (RF/XGBoost)? โ skip scaling, tidak butuh
- Banyak outlier signifikan? โ RobustScaler (median + IQR)
- Distribusi normal? โ StandardScaler (mean=0, std=1)
- Range terbatas / image / neural net? โ MinMaxScaler [0, 1]
Analogi Sehari-hari: Konversi mata uang. Kalau bandingkan harga mobil (jutaan) vs kopi (ribuan), tanpa scaling kopi seakan tidak penting. Scaling = ubah ke z-score atau persen agar fair.
Diagram statis Mermaid sebagai fallback:
flowchart TD
A["๐ Numerical Data"] --> B{"Ada outlier<br/>signifikan?"}
B -->|Ya| C["๐ก๏ธ RobustScaler<br/>(median + IQR)"]
B -->|Tidak| D{"Distribusi<br/>normal?"}
D -->|Ya| E["๐ StandardScaler<br/>(mean=0, std=1)"]
D -->|Range terbatas<br/>misal pixel 0-255| F["๐ MinMaxScaler<br/>(0 to 1)"]
G["๐ฒ Tree-based model"] -.->|skip scaling| H["โ
Tidak perlu"]
style E fill:#d4f4dd
style F fill:#d4f4dd
style C fill:#d4f4dd
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# Standard (z-score): mean=0, std=1
scaler = StandardScaler()
# Min-Max: range [0, 1]
scaler = MinMaxScaler()
# Robust: pakai median + IQR (robust ke outlier)
scaler = RobustScaler()
X_scaled = scaler.fit_transform(X)
Kapan Pakai Apa?
| Situasi | Scaler |
|---|---|
| Distribusi normal | StandardScaler |
| Range dibatasi | MinMaxScaler |
| Banyak outlier | RobustScaler |
| Tree-based model | (tidak perlu) |
Bagian 3 โ Handle Missing
Numerik
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="mean") # atau "median", "most_frequent"
X_imputed = imputer.fit_transform(X)
Kategorik
imputer = SimpleImputer(strategy="most_frequent")
df["kota"] = imputer.fit_transform(df[["kota"]])
Add Indicator Column
Sometimes "is missing" itself adalah sinyal:
df["age_was_missing"] = df["age"].isnull().astype(int)
df["age"] = df["age"].fillna(df["age"].median())
KNN Imputer (Lebih Smart)
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
X_imputed = imputer.fit_transform(X)
Bagian 4 โ Datetime Features
Analogi: Tanggal Mengandung Banyak Sinyal Tersembunyi
Raw timestamp "2026-05-16 14:30:00" cuma string bagi model. Tapi sebenarnya ada banyak sinyal: hari Sabtu (weekend โ mall ramai), bulan Mei (musim ujian โ toko buku ramai), pukul 14:30 (jam istirahat siang โ restoran ramai). Feature engineering datetime = ekstrak semua sinyal ini.
flowchart LR
A["๐
2026-05-16<br/>14:30:00"] --> B["๐ year, month,<br/>day"]
A --> C["๐๏ธ dayofweek,<br/>is_weekend"]
A --> D["๐ hour, minute"]
A --> E["๐ quarter,<br/>week_of_year"]
A --> F["๐ Cyclical:<br/>sin/cos encoding"]
style F fill:#fff4d4
df["date"] = pd.to_datetime(df["date"])
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["day"] = df["date"].dt.day
df["dayofweek"] = df["date"].dt.dayofweek
df["is_weekend"] = df["dayofweek"].isin([5, 6]).astype(int)
df["quarter"] = df["date"].dt.quarter
df["hour"] = df["date"].dt.hour
# Cyclical encoding (untuk dayofweek, hour, month)
import numpy as np
df["dow_sin"] = np.sin(2 * np.pi * df["dayofweek"] / 7)
df["dow_cos"] = np.cos(2 * np.pi * df["dayofweek"] / 7)
Cyclical encoding penting karena hari Minggu (6) dan Senin (0) sebenarnya dekat secara temporal.
Bagian 5 โ Feature Interaction
df["price_per_room"] = df["price"] / df["rooms"]
df["age_x_income"] = df["age"] * df["income"]
Domain knowledge guide kombinasi yang masuk akal.
Polynomial Features
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree=2, interaction_only=True)
X_poly = poly.fit_transform(X)
Bagian 6 โ Binning
Convert numeric ke kategori:
df["age_bin"] = pd.cut(df["age"],
bins=[0, 18, 35, 60, 100],
labels=["child", "young", "adult", "senior"]
)
# Atau equal-frequency
df["income_bin"] = pd.qcut(df["income"], q=4, labels=["Q1", "Q2", "Q3", "Q4"])
Berguna untuk capture non-linear effect tanpa polynomial.
Bagian 7 โ Text Features
Bag of Words
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(max_features=1000, stop_words="english")
X_bow = vectorizer.fit_transform(texts)
TF-IDF
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(max_features=1000, ngram_range=(1, 2))
X_tfidf = vectorizer.fit_transform(texts)
Custom Features
df["text_length"] = df["text"].str.len()
df["word_count"] = df["text"].str.split().str.len()
df["uppercase_ratio"] = df["text"].apply(lambda s: sum(1 for c in s if c.isupper()) / len(s))
Bagian 8 โ Feature Selection
Variance Threshold
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.01) # drop low-variance
X_selected = selector.fit_transform(X)
Correlation
corr_matrix = X.corr().abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
to_drop = [col for col in upper.columns if any(upper[col] > 0.95)]
X_filtered = X.drop(columns=to_drop)
Feature Importance (Tree-based)
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100)
rf.fit(X, y)
importances = pd.Series(rf.feature_importances_, index=X.columns)
top_features = importances.sort_values(ascending=False).head(20).index
X_top = X[top_features]
Recursive Feature Elimination
from sklearn.feature_selection import RFE
selector = RFE(LogisticRegression(), n_features_to_select=10)
selector.fit(X, y)
selected = X.columns[selector.support_]
Bagian 9 โ Handling Imbalance
Analogi: Kelas Sepi vs Kelas Ramai
Bayangkan kamu guru, satu kelas isi 95 murid kelas A dan cuma 5 murid kelas B. Kalau guru cuma fokus di kelas A, dia bisa dapat 95% "akurasi mengajar" tapi 5 murid kelas B terabaikan. Sama dengan ML: model akan bias ke kelas mayoritas. Solusinya: bobot ulang (class_weight), tambahkan murid B (oversample/SMOTE), atau kurangi murid A (undersample).
flowchart TD
A["โ๏ธ Imbalanced<br/>Data"] --> B["๐ 95% Class A<br/>5% Class B"]
B --> C{"Strategi?"}
C -->|Simple, no extra data| D["๐ฏ class_weight='balanced'"]
C -->|Tambah minority| E["๐ SMOTE<br/>(synthetic samples)"]
C -->|Kurangi majority| F["๐ RandomUnderSampler"]
C -->|Kombinasi| G["๐ SMOTEENN /<br/>SMOTETomek"]
style D fill:#d4f4dd
style E fill:#d4f4dd
# Class weight (paling sederhana)
model = RandomForestClassifier(class_weight="balanced")
# SMOTE (oversample minority)
# pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
sm = SMOTE(random_state=42)
X_res, y_res = sm.fit_resample(X_train, y_train)
# Undersample
from imblearn.under_sampling import RandomUnderSampler
us = RandomUnderSampler(random_state=42)
X_res, y_res = us.fit_resample(X_train, y_train)
Aturan: apply imbalance handling hanya di train, bukan val/test.
Tabel Komparasi: Encoding Strategies
| Method | Cocok Untuk | Pros | Cons |
|---|---|---|---|
| One-Hot | Nominal, < 10 kategori | Simple, tidak ada urutan palsu | Banyak kolom kalau cardinality tinggi |
| Label | Ordinal (Low/Med/High) | 1 kolom saja | Salah pakai untuk nominal |
| Frequency | Banyak kategori | Simple, 1 kolom | Asumsi frekuensi = sinyal |
| Target | Banyak kategori, ada target | Powerful sinyal | Risiko data leakage |
| Embedding | Kategori sangat banyak (deep learning) | Capture similarity | Butuh neural network |
Tabel: Scaling Choice
| Scaler | Output Range | Robust ke Outlier? | Pakai Saat |
|---|---|---|---|
| StandardScaler | mean=0, std=1 | โ | Distribusi normal |
| MinMaxScaler | [0, 1] | โ | Range terbatas (image, neural net) |
| RobustScaler | median=0, IQR=1 | โ | Banyak outlier |
| Normalizer | Unit length | โ | Cosine similarity, text |
Common Mistakes & FAQ
Common Mistakes
flowchart TD
A["โ ๏ธ Common Mistakes"] --> B["๐ง Data leakage:<br/>fit di full data"]
A --> C["๐
Datetime tidak<br/>di-extract"]
A --> D["๐ท๏ธ Label encode<br/>untuk nominal"]
A --> E["โ๏ธ Imbalance handling<br/>di test set juga"]
A --> F["๐ข Lupa drop<br/>highly correlated"]
A --> G["๐ Target encoding<br/>tanpa CV"]
style A fill:#ffe0e0
1. Data Leakage di Preprocessing
โ Fit scaler di seluruh data sebelum split โ info test "bocor" ke train.
โ Selalu split dulu, fit di train saja, apply ke test. Atau pakai Pipeline yang otomatis handle.
2. Datetime Tidak Di-Extract
Banyak yang biarkan kolom timestamp sebagai string atau drop. Padahal datetime feature (dayofweek, hour, is_weekend) sering jadi feature paling powerful.
3. Label Encoding untuk Nominal
โ "Bandung"=0, "Jakarta"=1, "Surabaya"=2 โ model anggap Surabaya > Jakarta > Bandung. Tidak ada urutan!
โ Pakai One-Hot untuk nominal.
4. Apply SMOTE di Validation/Test
โ SMOTE di full data sebelum split atau di test set โ metric jadi tidak realistis.
โ Apply SMOTE hanya di train fold, evaluate di test set asli.
5. Target Encoding tanpa CV
โ Hitung target mean dari seluruh data โ leakage. Model tahu target dari "encoded" feature.
โ Pakai out-of-fold target encoding (KFold mean).
FAQ
Q: Saya harus pakai berapa feature? A: Tidak ada angka magic. Pakai feature yang menambah signal, drop yang noise. Cek feature importance setelah model pertama.
Q: One-Hot atau Target Encoding? A: One-Hot untuk cardinality rendah (< 10). Target Encoding untuk cardinality tinggi (50+) tapi hati-hati leakage.
Q: Apakah saya selalu butuh scaling? A: Tergantung model. Linear (Logistic, SVM, KNN, Neural Net) โ ya. Tree-based (RF, XGBoost) โ tidak perlu.
Q: SMOTE atau class_weight? A: class_weight lebih simple, coba dulu. SMOTE bagus kalau imbalance ekstrem (>1:100). Tidak selalu better.
Q: Feature engineering atau model tuning? A: Feature engineering hampir selalu lebih impactful. Tune model setelah feature solid.
Bagian 10 โ Pipeline Lengkap
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
numeric_features = ["age", "income"]
numeric_transformer = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
])
categorical_features = ["city", "education"]
categorical_transformer = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore")),
])
preprocessor = ColumnTransformer([
("num", numeric_transformer, numeric_features),
("cat", categorical_transformer, categorical_features),
])
pipeline = Pipeline([
("preprocessor", preprocessor),
("classifier", RandomForestClassifier(random_state=42)),
])
pipeline.fit(X_train, y_train)
Beauty: semua preprocessing + model dalam 1 object. Saat predict di production, kamu cuma
pipeline.predict(X_new).
Cek Pemahaman
- Bisa one-hot, frequency, target encoding?
- Tahu kapan pakai StandardScaler vs MinMax vs Robust?
- Bisa handle missing dengan imputer?
- Bisa extract datetime features?
- Bisa feature selection (variance, correlation, importance)?
- Bisa bikin Pipeline lengkap?
Challenge 5.5
Challenge 1 โ Real Dataset Engineering
Pilih dataset Kaggle (House Prices). Lakukan feature engineering:
- Encode categorical
- Scale numeric
- Handle missing
- Extract datetime
- Create 5+ interaction features
- Feature selection
Compare model accuracy sebelum dan sesudah feature engineering.
Challenge 2 โ Pipeline End-to-End
Wrap semua di satu Pipeline. Save dengan joblib. Predict di data baru.
Selanjutnya: 06-evaluation.md