TL;DR
Feature engineering adalah proses bikin kolom baru dari data mentah supaya model punya sinyal yang lebih jelas buat belajar. Lima resep yang paling sering ngasih hasil: fitur dari kolom tanggal, rasio antar angka, agregat per pelanggan, encoding kolom kategori, dan binning angka jadi kelompok. Semua langkahnya harus dihitung dari data latih doang biar nggak bocor.
Feature engineering adalah proses bikin kolom baru dari data mentah supaya model punya sinyal yang lebih jelas buat dipelajari.
Di dataset latihan ngulikdata, ganti algoritma dari regresi logistik ke random forest naikin skor F1 dari 0,612 ke 0,647. Nambah 13 kolom turunan naikin skornya ke 0,741, pakai algoritma yang sama persis kayak awal.
Selisihnya jelas. Dan kolom turunan itu bikinnya nggak butuh teori berat, cuma butuh tahu apa yang penting di bisnisnya.
Feature engineering adalah kegiatan mengubah, menggabungkan, atau menurunkan kolom baru dari data mentah supaya pola yang relevan jadi lebih gampang ditangkap model. Contohnya ngubah kolom tanggal jadi jumlah hari sejak transaksi terakhir, atau ngubah dua kolom angka jadi satu kolom rasio.
Datanya nggak nambah. Yang nambah cuma cara nyajiin informasi yang udah ada.
Model nggak punya pengetahuan bisnis. Dia nggak tahu bahwa pelanggan yang belanja tiap 7 hari lalu tiba-tiba absen 40 hari itu tanda bahaya, kecuali kamu bikinin kolomnya.
Karena algoritma cuma bisa belajar dari informasi yang tersedia di kolom. Kalau sinyalnya kesimpan dalam bentuk selisih atau rasio yang belum dihitung, model harus nemuin sendiri lewat kombinasi aturan yang rumit. Bikinin kolomnya langsung, dan pekerjaan model jadi jauh lebih ringan.
Ini kelihatan jelas di model linear. Regresi logistik nggak bisa bikin pembagian antar kolom sendiri, jadi rasio harus kamu siapkan.
Di model berbasis pohon, efeknya lebih halus tapi tetap nyata. Pohon bisa niru rasio pakai banyak potongan aturan, tapi butuh jauh lebih banyak data buat nyampe hasil yang sama.
Kolom tanggal mentah hampir nggak berguna buat model. Pecah jadi komponen yang punya arti bisnis: hari dalam minggu, tanggal dalam bulan, jam, dan yang paling sering ngefek, selisih hari antara dua kejadian.
import pandas as pd
df["tanggal"] = pd.to_datetime(df["tanggal"])
tanggal_acuan = pd.Timestamp("2025-09-30")
df["hari_dalam_minggu"] = df["tanggal"].dt.dayofweek
df["tanggal_dalam_bulan"] = df["tanggal"].dt.day
df["jam_pesan"] = df["tanggal"].dt.hour
df["akhir_pekan"] = (df["tanggal"].dt.dayofweek >= 5).astype(int)
df["hari_sejak_beli"] = (tanggal_acuan - df["tanggal"]).dt.days
Buat konteks Indonesia, dua kolom tambahan sering ngasih lompatan besar. Pertama, penanda tanggal kembar seperti 9.9, 10.10, dan 11.11 karena kampanye marketplace ngumpul di situ. Kedua, penanda periode gajian yaitu tanggal 25 sampai akhir bulan.
df["tanggal_kembar"] = (
df["tanggal"].dt.day == df["tanggal"].dt.month
).astype(int)
df["periode_gajian"] = df["tanggal"].dt.day.between(25, 31).astype(int)
Kolom tanggal_kembar ini contoh fitur yang lahir dari pengetahuan pasar lokal. Nggak ada di tutorial mana pun, dan di data e-commerce Indonesia dampaknya kelihatan.
Detail cara ngolah tanggal di pandas ada di artikel accessor dt di pandas.
Rasio lebih berguna waktu ukuran mutlak nggak sebanding antar baris. Belanja Rp 500.000 dari pelanggan yang biasa belanja Rp 2 juta itu penurunan, sementara angka yang sama dari pelanggan baru itu lonjakan. Rasio yang bikin dua kasus itu kebaca beda.
df["rasio_diskon"] = df["nilai_diskon"] / df["nilai_kotor"]
df["belanja_per_pesanan"] = df["total_belanja"] / df["jumlah_pesanan"]
df["rasio_batal"] = df["pesanan_batal"] / df["jumlah_pesanan"]
df["ongkir_per_belanja"] = df["total_ongkir"] / df["total_belanja"]
Satu hal yang wajib: lindungi dari pembagian nol.
import numpy as np
df["belanja_per_pesanan"] = np.where(
df["jumlah_pesanan"] > 0,
df["total_belanja"] / df["jumlah_pesanan"],
0
)
Rasio juga bikin nilai ekstrem lebih jinak. Pelanggan grosir yang belanja Rp 40 juta bakal jadi outlier di kolom total belanja, tapi rasio diskonnya bisa jadi normal aja.
Kelompokkan data transaksi per pelanggan, hitung ringkasannya, lalu tempelkan balik ke tiap baris. Fitur agregat ngasih konteks perilaku yang nggak kelihatan dari satu baris transaksi.
Tiga angka yang paling sering kepakai di ritel: kapan terakhir belanja, seberapa sering belanja, dan berapa nilainya.
agg = (
trx.groupby("customer_id")
.agg(
jumlah_pesanan=("order_id", "nunique"),
total_belanja=("nilai_bersih", "sum"),
rata_belanja=("nilai_bersih", "mean"),
std_belanja=("nilai_bersih", "std"),
pesanan_pertama=("tanggal", "min"),
pesanan_terakhir=("tanggal", "max"),
jumlah_kategori=("kategori", "nunique"),
)
.reset_index()
)
agg["umur_pelanggan_hari"] = (
agg["pesanan_terakhir"] - agg["pesanan_pertama"]
).dt.days
agg["jeda_rata_hari"] = (
agg["umur_pelanggan_hari"] / agg["jumlah_pesanan"].clip(lower=1)
)
Kolom jeda_rata_hari itu salah satu fitur paling kuat di prediksi pelanggan berhenti belanja. Dia nangkap ritme belanja tiap orang, bukan cuma jumlahnya.
Fitur turunan berikutnya bandingin jeda terakhir sama jeda biasanya.
agg["hari_sejak_terakhir"] = (
tanggal_acuan - agg["pesanan_terakhir"]
).dt.days
agg["rasio_jeda"] = (
agg["hari_sejak_terakhir"] / agg["jeda_rata_hari"].clip(lower=1)
)
Nilai rasio_jeda di atas 3 artinya pelanggan itu udah absen tiga kali lebih lama dari kebiasaannya. Kolom ini yang paling banyak nyumbang di contoh kasus di bawah.
Kalau kamu mau nempelin hasil agregat ke tiap baris tanpa merge, pakai pola di artikel groupby transform.
Pilih metodenya berdasarkan jumlah nilai unik. Di bawah 15 nilai, pakai one hot encoding yang bikin satu kolom nol dan satu per kategori. Di atas itu, pertimbangkan frequency encoding atau target encoding supaya jumlah kolomnya nggak meledak.
| Jumlah nilai unik | Metode | Catatan |
|---|---|---|
| 2 | Ubah jadi 0 dan 1 | Nggak perlu kolom tambahan |
| 3 sampai 15 | One hot encoding | Paling aman dan gampang dijelaskan |
| 15 sampai 50 | Frequency encoding | Ganti kategori dengan jumlah kemunculannya |
| Di atas 50 | Target encoding | Hitung di dalam lipatan cross validation |
| Punya urutan | Ordinal encoding | Contoh: kecil, sedang, besar |
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
kolom_kategori = ["kanal", "metode_bayar", "tipe_pelanggan"]
hasil = enc.fit_transform(X_train[kolom_kategori])
Parameter handle_unknown="ignore" penting banget. Tanpa itu, kode kamu bakal error di produksi begitu ketemu metode bayar baru yang belum ada waktu latihan.
Buat frequency encoding, satu baris cukup.
peta_frekuensi = X_train["kode_pos"].value_counts(normalize=True)
X_train["kode_pos_freq"] = X_train["kode_pos"].map(peta_frekuensi)
X_test["kode_pos_freq"] = X_test["kode_pos"].map(peta_frekuensi).fillna(0)
Perhatikan barisnya. Peta frekuensi dihitung dari data latih, lalu dipakai ke data uji. Kalau kamu hitung dari seluruh data, itu kebocoran.
Cara lain pakai pandas ada di artikel get_dummies.
Kelompokkan angka jadi rentang waktu hubungannya sama target nggak lurus. Contohnya umur pelanggan, di mana kelompok 25-34 tahun bisa berperilaku beda jauh dari kelompok 18-24 dan 35-44 tanpa pola naik turun yang teratur.
df["kelompok_umur"] = pd.cut(
df["umur"],
bins=[0, 24, 34, 44, 54, 100],
labels=["18-24", "25-34", "35-44", "45-54", "55+"]
)
df["tingkat_belanja"] = pd.qcut(
df["total_belanja"], q=5,
labels=["sangat rendah", "rendah", "sedang", "tinggi", "sangat tinggi"]
)
Fungsi cut pakai batas yang kamu tentukan. Fungsi qcut bagi rata berdasarkan jumlah baris, jadi tiap kelompok isinya sama banyak. Bedanya aku jelasin di artikel cut dan qcut.
Hati-hati satu hal: batas qcut harus dihitung dari data latih dan dipakai ulang ke data uji. Kalau dihitung ulang di data uji, kamu bocorin distribusi.
Hasil pengelompokan ini juga sering langsung kepakai buat segmentation di sisi marketing.
Latih model dua kali dengan pembagian data yang sama, sekali tanpa fitur baru dan sekali dengan fitur baru, lalu bandingin skornya pakai cross validation. Kalau kenaikannya lebih kecil dari simpangan baku antar lipatan, fitur itu belum terbukti ngebantu.
from sklearn.model_selection import cross_val_score
skor_lama = cross_val_score(pipe, X_dasar, y, cv=5, scoring="f1")
skor_baru = cross_val_score(pipe, X_lengkap, y, cv=5, scoring="f1")
print(f"tanpa fitur baru: {skor_lama.mean():.3f} +/- {skor_lama.std():.3f}")
print(f"dengan fitur baru: {skor_baru.mean():.3f} +/- {skor_baru.std():.3f}")
Buat lihat kontribusi tiap kolom, pakai permutation importance yang ngukur seberapa turun skor kalau satu kolom diacak.
from sklearn.inspection import permutation_importance
hasil = permutation_importance(
model, X_test, y_test, n_repeats=10, random_state=42
)
urutan = hasil.importances_mean.argsort()[::-1]
for i in urutan[:10]:
print(f"{X_test.columns[i]:25s} {hasil.importances_mean[i]:.4f}")
Daftar teknik transformasi lain yang tersedia ada di dokumentasi preprocessing scikit-learn.
Dataset latihan ngulikdata punya 4.180 pelanggan toko_berkah dengan 11 kolom mentah. Targetnya prediksi pelanggan berhenti belanja dalam 90 hari.
Aku uji tiga skenario pakai cross validation 5 lipatan dan metrik F1.
| Skenario | Jumlah kolom | Skor F1 | Simpangan |
|---|---|---|---|
| Kolom mentah, regresi logistik | 11 | 0,612 | 0,028 |
| Kolom mentah, random forest | 11 | 0,647 | 0,031 |
| Kolom turunan, regresi logistik | 24 | 0,741 | 0,024 |
Ganti algoritma naikin 0,035 poin. Nambah 13 kolom turunan naikin 0,129 poin, hampir empat kali lipatnya.
Tiga kolom yang paling banyak nyumbang menurut permutation importance:
rasio_jeda, yaitu jeda belanja terakhir dibagi jeda rata-rata pelanggan itu.jumlah_kategori, yaitu berapa kategori produk berbeda yang pernah dia beli.rasio_diskon, yaitu porsi diskon dari nilai belanja kotor.Kolom kedua ngasih temuan bisnis yang langsung kepakai. Pelanggan yang pernah beli dari 4 kategori berbeda punya tingkat berhenti belanja 8,3 persen, sementara yang cuma pernah beli 1 kategori mencapai 31,7 persen.
Selisih 23,4 poin itu jadi dasar program rekomendasi lintas kategori buat pembeli baru. Angka itu lahir dari satu kolom turunan yang bikinnya cuma satu baris kode.
Poin nomor 1 dan 2 penyebab paling sering skor lompat mendadak. Selalu split dulu, baru bikin fitur.
Lima resep yang paling sering ngasih hasil: pecah kolom tanggal, bikin rasio, hitung agregat per pelanggan, encode kategori sesuai jumlah nilai uniknya, dan kelompokkan angka yang hubungannya nggak lurus.
Urutan kerjanya selalu sama. Split dulu, bikin fitur dari data latih, ukur kontribusinya pakai cross validation, buang yang nggak terbukti ngebantu.
Kalau skormu masih goyang setelah nambah fitur, cek dulu cara evaluasinya lewat cross validation. Dan buat ngitung agregat awal dari data yang masih di spreadsheet, SUMIFS bisa jadi titik mulai sebelum pindah ke Python.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Kolom datetime nyimpen tanggal, jam, hari, semuanya nempel jadi satu. Accessor .dt di pandas ngeluarin tiap bagian jadi kolom sendiri buat dianalisa.
Nama toko ketik campur huruf besar-kecil, spasi nyasar di ujung, atau kode produk nempel jadi satu. Accessor .str di pandas ngerapiin semua itu tanpa loop.
Model machine learning cuma ngerti angka, bukan teks kayak merah atau biru. get_dummies ngubah kolom kategori jadi kolom 0/1 dalam satu baris kode.