Feature engineering là nghệ thuật biến dữ liệu thô thành đặc trưng (feature) mà mô hình học được tốt. Đây thường là bước tạo khác biệt lớn nhất về hiệu năng — hơn cả việc chọn thuật toán. Bài này bao quát encoding biến phân loại, scaling, biến đổi số, feature thời gian & text, feature selection, và cách dùng sklearn Pipeline/ColumnTransformer để tránh rò rỉ dữ liệu.
Feature engineering là quá trình dùng hiểu biết về bài toán để tạo, biến đổi và chọn lọc các đặc trưng đầu vào cho mô hình. Một câu nói kinh điển trong ngành: "Applied machine learning is basically feature engineering" (Andrew Ng).
Vì sao quan trọng nhất:
Hầu hết thuật toán chỉ nhận số, nên biến category (text) phải được encode. Chọn cách nào phụ thuộc vào biến có thứ tự không và cardinality (số giá trị duy nhất):
| Kỹ thuật | Cách làm | Khi dùng |
|---|---|---|
| Label encoding | Gán mỗi hạng một số nguyên (0,1,2...). | Cho target, hoặc mô hình cây. Không dùng cho model tuyến tính (áp đặt thứ tự giả). |
| Ordinal encoding | Gán số theo thứ tự có nghĩa (low<med<high → 0,1,2). | Biến thực sự có thứ tự. |
| One-hot | Mỗi hạng thành một cột 0/1. | Không thứ tự, cardinality thấp (< ~15). Chuẩn cho model tuyến tính. |
| Target / mean | Thay hạng bằng trung bình target của hạng đó. | Cardinality cao. Mạnh nhưng dễ leakage & overfit. |
| Frequency / count | Thay hạng bằng tần suất xuất hiện. | Cardinality cao, khi tần suất mang thông tin. |
| Hashing | Băm hạng vào số cột cố định. | Cardinality rất cao / streaming, chấp nhận va chạm. |
import pandas as pd from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder # One-hot: an toàn cho hạng lạ ở test nhờ handle_unknown ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=False) X_city = ohe.fit_transform(train[["city"]]) # fit CHỈ trên train X_city_test = ohe.transform(test[["city"]]) # pandas one-hot nhanh (khi không cần fit/transform tách biệt) train = pd.get_dummies(train, columns=["city"], drop_first=True) # Ordinal có thứ tự chỉ định rõ oe = OrdinalEncoder(categories=[["low", "medium", "high"]]) train["level_enc"] = oe.fit_transform(train[["level"]]) # Frequency encoding (map tần suất học từ train) freq = train["city"].value_counts(normalize=True) train["city_freq"] = train["city"].map(freq) test["city_freq"] = test["city"].map(freq).fillna(0)
"Other".Các cột số thường có thang khác nhau (tuổi 0–100, thu nhập 0–1 tỉ). Nhiều thuật toán bị lệch vì biến giá trị lớn lấn át:
| Scaler | Công thức | Đặc điểm |
|---|---|---|
| StandardScaler | (x − mean) / std | Mean 0, std 1. Mặc định phổ biến; nhạy với outlier. |
| MinMaxScaler | (x − min) / (max − min) | Đưa về [0,1]. Rất nhạy outlier (min/max bị kéo). |
| RobustScaler | (x − median) / IQR | Dùng median & IQR → bền với outlier. |
Khi nào CẦN scaling: các mô hình dựa trên khoảng cách (KNN, K-Means, SVM) và gradient descent (Linear/Logistic Regression, mạng neural), cùng PCA/regularization — vì chúng nhạy với độ lớn.
Khi nào KHÔNG cần: các mô hình cây (Decision Tree, Random Forest, XGBoost, LightGBM) chia theo ngưỡng từng biến độc lập, nên bất biến với scale — scale không hại nhưng vô ích.
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) # học mean/std TỪ train X_test_scaled = scaler.transform(X_test) # áp cùng mean/std lên test
fit scaler trên toàn bộ dữ liệu rồi mới split. Mean/std/min/max phải tính riêng trên train — nếu không, thống kê của test rò vào train.Binning / Discretization — nhóm biến liên tục thành khoảng, giúp bắt quan hệ phi tuyến và giảm nhiễu:
import pandas as pd # Chia theo ngưỡng cố định (business rule) train["age_group"] = pd.cut(train["age"], bins=[0, 18, 35, 60, 120], labels=["teen", "young", "adult", "senior"]) # Chia theo phân vị (mỗi bin số lượng bằng nhau) train["income_q"] = pd.qcut(train["income"], q=4, labels=["Q1", "Q2", "Q3", "Q4"])
Biến đổi phân phối — log / Box-Cox / Yeo-Johnson làm phân phối lệch gần chuẩn hơn (xem Bài 5):
import numpy as np from sklearn.preprocessing import PowerTransformer train["income_log"] = np.log1p(train["income"]) # không có tham số học pt = PowerTransformer(method="yeo-johnson") # CÓ tham số -> fit trên train train[["income_yj"]] = pt.fit_transform(train[["income"]]) test[["income_yj"]] = pt.transform(test[["income"]])
Biến tương tác & đa thức — tạo tích/luỹ thừa để model tuyến tính bắt được quan hệ phi tuyến:
from sklearn.preprocessing import PolynomialFeatures # Tay: tỉ số & tích mang ý nghĩa nghiệp vụ train["price_per_m2"] = train["price"] / train["area"] train["rooms_x_area"] = train["rooms"] * train["area"] # Tự động: bậc 2 + tương tác (x1, x2, x1^2, x1*x2, x2^2) poly = PolynomialFeatures(degree=2, include_bias=False) X_poly = poly.fit_transform(train[["area", "rooms"]])
PolynomialFeatures bậc cao làm số cột bùng nổ và dễ overfit. Feature tạo tay theo hiểu biết nghiệp vụ (tỉ số, hiệu, tích có nghĩa) thường hiệu quả và dễ giải thích hơn feature đa thức máy móc.Một cột datetime chứa rất nhiều tín hiệu. Tách thành phần là bước đầu tiên:
import numpy as np dt = pd.to_datetime(train["order_date"]) train["year"] = dt.dt.year train["month"] = dt.dt.month train["day"] = dt.dt.day train["dow"] = dt.dt.dayofweek # 0=T2 ... 6=CN train["hour"] = dt.dt.hour train["is_weekend"] = (dt.dt.dayofweek >= 5).astype(int)
Cyclical encoding (sin/cos) — với biến tuần hoàn (giờ, tháng, thứ), giá trị 23h và 0h thực ra gần nhau, nhưng số 23 và 0 lại rất xa. Mã hoá sin/cos giữ được tính "vòng tròn":
# hour có chu kỳ 24 train["hour_sin"] = np.sin(2 * np.pi * train["hour"] / 24) train["hour_cos"] = np.cos(2 * np.pi * train["hour"] / 24) # month có chu kỳ 12 train["month_sin"] = np.sin(2 * np.pi * train["month"] / 12) train["month_cos"] = np.cos(2 * np.pi * train["month"] / 12)
Lag & rolling features — cho time series / dữ liệu chuỗi, dùng quá khứ để dự báo:
# Sắp theo thời gian trước đã! train = train.sort_values("order_date") # Lag: giá trị của 1 và 7 kỳ trước train["sales_lag1"] = train["sales"].shift(1) train["sales_lag7"] = train["sales"].shift(7) # Rolling: trung bình/độ lệch trượt 7 kỳ (shift(1) để không dùng giá trị hiện tại) train["sales_ma7"] = train["sales"].shift(1).rolling(7).mean() train["sales_std7"] = train["sales"].shift(1).rolling(7).std()
shift(1) trong rolling để không lỡ tay đưa chính giá trị đang dự báo vào feature.Văn bản cần chuyển thành vector số. Hai cách kinh điển:
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer # Bag-of-Words bow = CountVectorizer(max_features=5000, stop_words="english") X_bow = bow.fit_transform(train["review"]) # fit vocab TỪ train # TF-IDF (thường tốt hơn cho phân loại văn bản) tfidf = TfidfVectorizer(max_features=5000, ngram_range=(1, 2)) X_tfidf = tfidf.fit_transform(train["review"]) X_tfidf_test = tfidf.transform(test["review"]) # dùng lại vocab của train
Bớt feature thừa/nhiễu giúp giảm overfit, tăng tốc, dễ giải thích. Ba nhóm phương pháp:
| Nhóm | Ý tưởng | Ví dụ |
|---|---|---|
| Filter | Chấm điểm từng feature bằng thống kê, độc lập với model. Nhanh. | Correlation, chi-square, mutual information, variance threshold. |
| Wrapper | Thử các tập con feature, đánh giá bằng chính model. Chính xác nhưng tốn kém. | RFE (Recursive Feature Elimination), forward/backward selection. |
| Embedded | Chọn feature ngay trong lúc train model. | L1/Lasso (đưa hệ số về 0), feature_importances_ của cây. |
from sklearn.feature_selection import ( SelectKBest, chi2, mutual_info_classif, RFE) from sklearn.linear_model import LogisticRegression, Lasso from sklearn.ensemble import RandomForestClassifier # FILTER: chọn K feature tốt nhất theo mutual information selector = SelectKBest(score_func=mutual_info_classif, k=20) X_sel = selector.fit_transform(X_train, y_train) # WRAPPER: RFE loại dần feature yếu nhất rfe = RFE(LogisticRegression(max_iter=1000), n_features_to_select=10) rfe.fit(X_train, y_train) print(X_train.columns[rfe.support_]) # EMBEDDED: độ quan trọng từ Random Forest rf = RandomForestClassifier(n_estimators=300, random_state=42) rf.fit(X_train, y_train) importances = pd.Series(rf.feature_importances_, index=X_train.columns) print(importances.sort_values(ascending=False).head(15)) # EMBEDDED: L1/Lasso ép hệ số các feature vô dụng về 0 lasso = Lasso(alpha=0.01).fit(X_train, y_train)
Data leakage là khi thông tin không có ở thời điểm dự báo (đặc biệt từ tập test hoặc từ target) lọt vào quá trình huấn luyện, làm điểm đánh giá cao ảo nhưng sập khi lên production. Nguồn leakage thường gặp: impute/scale/encode trước khi split, target encoding không cross-validate, dùng dữ liệu tương lai trong time series.
Giải pháp gọn gàng: đóng gói mọi biến đổi vào Pipeline + ColumnTransformer. Khi gọi fit, mọi bước chỉ học từ train; khi predict/transform, chúng áp đúng tham số đã học lên dữ liệu mới. Không bao giờ rò rỉ, và tái lập được.
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split, cross_val_score num_cols = ["age", "income", "score"] cat_cols = ["city", "gender"] # Nhánh xử lý riêng cho cột số & cột category num_pipe = Pipeline([ ("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler()), ]) cat_pipe = Pipeline([ ("impute", SimpleImputer(strategy="most_frequent")), ("ohe", OneHotEncoder(handle_unknown="ignore")), ]) pre = ColumnTransformer([ ("num", num_pipe, num_cols), ("cat", cat_pipe, cat_cols), ]) # Gắn preprocessing + model vào MỘT pipeline model = Pipeline([ ("pre", pre), ("clf", RandomForestClassifier(n_estimators=300, random_state=42)), ]) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) # fit: impute/scale/encode học CHỈ từ X_train model.fit(X_train, y_train) print("Test acc:", model.score(X_test, y_test)) # cross_val_score cũng an toàn: preprocessing fit lại trong MỖI fold scores = cross_val_score(model, X_train, y_train, cv=5) print("CV mean:", scores.mean())
category_encoders.TargetEncoder đặt trong Pipeline để an toàn.