Feature Engineering

4 jam11 min baca
Tujuan

Skill yang sering **lebih penting dari model selection**. Feature bagus + model sederhana > feature jelek + model complex.

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:

  1. Apakah ada urutan natural? (Low/Medium/High = ya, Bandung/Jakarta/Surabaya = tidak)
  2. Ada urutan โ†’ Label Encoding (0, 1, 2)
  3. Tidak ada urutan โ†’ cek jumlah unique values
  4. < 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:

  1. Tree-based (RF/XGBoost)? โ†’ skip scaling, tidak butuh
  2. Banyak outlier signifikan? โ†’ RobustScaler (median + IQR)
  3. Distribusi normal? โ†’ StandardScaler (mean=0, std=1)
  4. 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