ML 2 — Feature engineering
Mở đầu: "garbage in, garbage out"
Có một câu truyền tai trong giới ML đã nhiều thập kỷ: "garbage in, garbage out" — đầu vào rác thì đầu ra cũng rác. Bạn có thể dùng thuật toán tinh vi nhất, tinh chỉnh siêu tham số cả tuần, nhưng nếu đặc trưng đầu vào nghèo nàn thì mô hình vẫn kém. Ngược lại, một bộ đặc trưng (feature) tốt giúp cả mô hình đơn giản như hồi quy logistic vượt trội, và làm model phức tạp hội tụ nhanh, ổn định hơn.
Trong thực tế, phần lớn thời gian của một dự án ML không nằm ở "chọn model" mà ở việc hiểu dữ liệu và tạo đặc trưng. Đây là nơi kiến thức nghiệp vụ (domain knowledge) tạo khác biệt lớn nhất — thứ không thuật toán nào tự nghĩ ra thay bạn.
Bài này đi sâu vào các kỹ thuật feature engineering thực chiến với scikit-learn: xử lý giá trị thiếu, mã hoá biến phân loại, scaling, biến đổi số, đặc trưng thời gian, và chọn lọc đặc trưng — tất cả gói gọn trong pipeline để tránh rò rỉ dữ liệu (data leakage). Nếu bạn chưa nắm quy trình tổng thể, hãy xem trước ML 1 — Quy trình; phần leakage sẽ được nối lại liên tục trong bài này.
Toàn cảnh: từ dữ liệu thô đến feature matrix
Feature engineering là chuỗi biến đổi đưa dữ liệu thô (mỗi cột một kiểu, đầy chỗ trống, đủ định dạng) thành một ma trận số sạch mà model đọc được:
Nguyên tắc vàng xuyên suốt: mọi thống kê dùng để biến đổi (giá trị điền, trung bình để scale, bảng mã hoá) phải được học fit trên tập train, rồi mới transform lên test. Nếu bạn tính trung bình trên toàn bộ dữ liệu trước khi chia, bạn đã "nhìn trộm" test — kết quả offline đẹp nhưng thực tế sụp đổ.
Xử lý giá trị thiếu (missing values)
Dữ liệu thật gần như luôn có ô trống. Có hai hướng xử lý:
1. Xoá (drop). Xoá dòng nếu số dòng thiếu ít và ngẫu nhiên; xoá cột nếu cột thiếu quá nhiều (ví dụ > 60-70%) và ít giá trị. Nhược điểm: mất dữ liệu, và nếu việc thiếu không ngẫu nhiên (khách hàng thu nhập cao ngại điền thu nhập) thì xoá gây thiên lệch.
2. Điền (impute). Thay ô trống bằng một giá trị ước lượng:
- Mean/median cho cột số — median an toàn hơn khi có ngoại lai.
- Mode (giá trị hay gặp nhất) cho cột phân loại.
- KNN imputer: điền dựa trên các dòng "hàng xóm" giống nhất.
- Indicator "was missing": thêm một cột nhị phân đánh dấu ô nào từng bị thiếu. Bản thân việc thiếu đôi khi lại là tín hiệu dự báo.
# (minh hoạ)
import numpy as np
from sklearn.impute import SimpleImputer
# Cột số: điền bằng median (bền với ngoại lai hơn mean)
num_imputer = SimpleImputer(strategy="median")
# Cột phân loại: điền bằng giá trị hay gặp nhất
cat_imputer = SimpleImputer(strategy="most_frequent")
# Giữ cờ "đã từng thiếu" — thông tin việc thiếu có thể có giá trị dự báo
num_imputer_flag = SimpleImputer(strategy="median", add_indicator=True)
Điểm chống leakage cốt lõi: SimpleImputer học median/mode khi gọi fit(X_train). Bạn tuyệt đối không tính median trên cả train + test. Đặt imputer trong pipeline sẽ tự động đảm bảo điều này (xem ML 1 — Quy trình).
Mã hoá biến phân loại (categorical encoding)
Model chỉ ăn số, nên các cột như nghề_nghiệp, tỉnh_thành, loại_thẻ phải chuyển thành số. Cách chọn phụ thuộc số lượng giá trị khác nhau (cardinality) và loại model.
One-hot encoding (cardinality thấp)
Mỗi giá trị thành một cột nhị phân 0/1. Rõ ràng, không áp đặt thứ tự giả. Phù hợp khi số hạng mục nhỏ (vài chục). Với cardinality cao (hàng nghìn mã bưu chính) thì one-hot làm bùng nổ số chiều.
# (minh hoạ)
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(
handle_unknown="ignore", # hạng mục lạ ở test -> vector toàn 0, không lỗi
sparse_output=False,
)
Ordinal encoding
Gán mỗi hạng mục một số nguyên. Chỉ hợp lý khi có thứ tự thật, ví dụ học_vấn: cấp 1 < cấp 2 < cấp 3 < đại học. Áp ordinal cho biến không có thứ tự (mã tỉnh) sẽ tạo ra một thứ hạng giả mà model hiểu nhầm.
Target / mean encoding (cardinality cao — cẩn thận leakage)
Thay mỗi hạng mục bằng trung bình của biến mục tiêu (target) trong hạng mục đó. Ví dụ mã hoá quận_huyện bằng tỷ lệ vỡ nợ trung bình của quận đó. Rất mạnh cho biến cardinality cao, nhưng cực kỳ dễ rò rỉ: bạn đang tiêm chính đáp án vào đặc trưng.
Cách làm đúng là cross-fitted target encoding: chia train thành K fold, mã hoá mỗi fold bằng thống kê tính từ các fold còn lại (out-of-fold), không bao giờ dùng chính dòng đó. Scikit-learn TargetEncoder làm việc này tự động trong fit_transform, cộng thêm smoothing để tránh overfit các hạng mục hiếm (chỉ có 1-2 dòng).
# (minh hoạ)
from sklearn.preprocessing import TargetEncoder
# TargetEncoder tự dùng cross-fitting bên trong fit_transform -> giảm leakage
te = TargetEncoder(smooth="auto", cv=5)
# Lưu ý: cần y khi fit -> te.fit_transform(X_cat, y)
Frequency encoding
Thay hạng mục bằng tần suất xuất hiện của nó. Đơn giản, không cần target nên không rò rỉ target, hữu ích khi mức độ phổ biến của một hạng mục tự nó mang tín hiệu.
Chọn cách nào?
- Model dạng cây (decision tree, random forest, gradient boosting — xem ML 4 — Cây & Ensemble) chịu label/ordinal encoding khá tốt, vì cây tách theo ngưỡng nên "thứ tự giả" ít gây hại; đồng thời tránh được one-hot làm loãng đặc trưng.
- Model tuyến tính (hồi quy tuyến tính/logistic — xem ML 3 — Linear & Logistic) và model dựa khoảng cách (KNN) cần one-hot cho biến không thứ tự, vì chúng diễn giải giá trị số theo độ lớn.
- Cardinality rất cao: cân nhắc target/frequency encoding thay vì one-hot.
Scaling / chuẩn hoá
Nhiều model nhạy cảm với thang đo của đặc trưng. Nếu tuổi chạy 20-70 còn số_dư chạy 0-5.000.000.000, thì các thuật toán dựa khoảng cách (KNN, SVM, K-means) hay dựa gradient (hồi quy có regularization, mạng neural) sẽ bị cột lớn "áp đảo" hoàn toàn. Ba scaler phổ biến:
| Scaler | Công thức trực giác | Khi dùng |
|---|---|---|
| StandardScaler | trừ mean, chia độ lệch chuẩn (mean=0, std=1) | Mặc định tốt, hợp khi dữ liệu gần chuẩn (SVM, hồi quy, NN) |
| MinMaxScaler | co về khoảng [0, 1] | Cần biên rõ ràng, dữ liệu sạch ít ngoại lai |
| RobustScaler | dùng median & IQR (khoảng tứ phân vị) | Dữ liệu nhiều ngoại lai / lệch — không bị vài giá trị cực đoan kéo lệch |
Điểm mấu chốt cần nhớ: StandardScaler và MinMaxScaler đều rất nhạy với ngoại lai (vì mean, min, max đều bị vài giá trị cực đoan kéo lệch). Với dữ liệu thật thường bẩn và lệch, RobustScaler — dựa trên median và IQR nên không bị ảnh hưởng bởi số ít giá trị cực lớn — thường là lựa chọn an toàn.
Model dạng cây thì KHÔNG cần scaling. Cây tách theo ngưỡng trên từng cột một cách độc lập ("số_dư > 10 triệu?"), phép đơn điệu như co giãn không đổi thứ tự nên không ảnh hưởng kết quả. Đây là ưu điểm thực dụng lớn của cây.
# (minh hoạ)
from sklearn.preprocessing import StandardScaler, RobustScaler
scaler_default = StandardScaler()
scaler_outlier = RobustScaler() # khi cột có nhiều ngoại lai
# scaler.fit(X_train) rồi transform(X_test) - KHÔNG fit trên test
Như imputer, scaler cũng học thống kê từ train và áp lên test — đặt trong pipeline để không lỡ tay tính mean trên cả tập.
Biến đổi biến số
- Log / Box-Cox / Yeo-Johnson: cột lệch phải mạnh (thu nhập, doanh thu, số dư) thường nên
log1pđể "kéo" đuôi dài về gần chuẩn, giúp model tuyến tính hoạt động tốt hơn. - Binning (rời rạc hoá): chia biến liên tục thành khoảng, ví dụ
tuổi→ nhóm[<25, 25-40, 40-60, 60+]. Hữu ích khi quan hệ với target là phi tuyến theo bậc thang. - Đa thức & tương tác (interaction): tạo
x1*x2,x1^2... để nắm quan hệ phi tuyến/kết hợp. Ví dụthu_nhập * số_năm_làm_việc. Cẩn thận bùng nổ số chiều. - Tỷ lệ (ratio): thường là loại đặc trưng giá trị nhất, ví dụ
nợ / thu_nhập,chi_tiêu / hạn_mức. Tỷ lệ nén nhiều thông tin nghiệp vụ vào một con số.
# (minh hoạ)
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
X["thu_nhap_log"] = np.log1p(X["thu_nhap"]) # kéo đuôi lệch
X["ty_le_no_thu_nhap"] = X["tong_no"] / (X["thu_nhap"] + 1e-9) # ratio nghiệp vụ
# Sinh tương tác bậc 2 (không thêm cột bias, chỉ lấy tích chéo)
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
Đặc trưng thời gian & chu kỳ
Một cột timestamp thô gần như vô dụng với model. Hãy tách nó ra:
- Thành phần lịch:
year,month,day,day_of_week,hour,is_weekend,is_holiday. - Cyclical encoding (sin/cos): các biến chu kỳ như giờ (0-23) hay tháng (1-12) có tính vòng — 23h và 0h thực ra rất gần nhau, nhưng nếu để số nguyên model lại thấy chúng xa nhất. Mã hoá bằng cặp
sin/cosđể giữ tính liền vòng. - Thời gian từ sự kiện:
số_ngày_kể_từ_giao_dịch_gần_nhất,tuổi_tài_khoản_theo_ngày— cực mạnh trong bài toán churn/tín dụng. - Lag & rolling (cho chuỗi thời gian): giá trị của kỳ trước (
lag), trung bình trượt (rolling mean) 7/30 ngày. Lưu ý: chỉ được dùng dữ liệu quá khứ để tránh nhìn trộm tương lai.
# (minh hoạ)
import numpy as np
df["dow"] = df["ts"].dt.dayofweek
df["hour"] = df["ts"].dt.hour
df["is_weekend"] = (df["dow"] >= 5).astype(int)
# Cyclical encoding cho giờ: 23h và 0h nằm sát nhau trên vòng tròn
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)
# Đặc trưng "thời gian kể từ sự kiện" - rất mạnh cho churn
df["days_since_last_txn"] = (df["snapshot"] - df["last_txn"]).dt.days
Text và các loại khác (nhắc ngắn)
Với dữ liệu văn bản, cách kinh điển là bag-of-words hoặc TF-IDF (đếm/cân trọng số từ). Hiện đại hơn là dùng embedding — biến đoạn văn thành vector ngữ nghĩa dày, nắm được ý nghĩa thay vì chỉ đếm từ. Đây là hướng chủ đạo cho NLP ngày nay; xem LLM 3 — Embeddings & vector để dùng embedding làm đặc trưng cho model xuôi dòng.
Chọn lọc đặc trưng (feature selection)
Nhiều đặc trưng không đồng nghĩa tốt hơn. Đặc trưng thừa/nhiễu gây overfitting, làm chậm huấn luyện, và khó diễn giải. Ba nhóm phương pháp:
- Filter (lọc, độc lập với model): loại cột phương sai ~0 (
VarianceThreshold), xét tương quan (correlation) với target, hoặc mutual information (nắm cả quan hệ phi tuyến). Nhanh, rẻ, dùng để sàng sơ bộ. - Wrapper (bọc quanh model): thử các tập con và đánh giá bằng chính model, ví dụ RFE (Recursive Feature Elimination) — loại dần đặc trưng yếu nhất. Mạnh nhưng tốn tính toán.
- Embedded (nhúng trong huấn luyện): model tự chọn khi học. L1 (Lasso) ép hệ số của đặc trưng vô ích về 0; feature importance từ cây/gradient boosting cũng dùng để cắt tỉa.
# (minh hoạ)
from sklearn.feature_selection import VarianceThreshold, RFE, mutual_info_classif
from sklearn.linear_model import LogisticRegression
# Filter: bỏ cột gần như không đổi
vt = VarianceThreshold(threshold=0.0)
# Wrapper: giữ 20 đặc trưng tốt nhất theo model
rfe = RFE(estimator=LogisticRegression(max_iter=1000), n_features_to_select=20)
Lưu ý leakage: feature selection dựa trên target (correlation, mutual info, L1) cũng phải nằm trong CV, chọn trên train fold — nếu chọn đặc trưng bằng cả dữ liệu rồi mới validate thì đã rò rỉ.
Gói tất cả trong pipeline với ColumnTransformer
Vấn đề thực tế: cột số và cột phân loại cần cách xử lý khác nhau. ColumnTransformer cho phép áp các pipeline khác nhau lên từng nhóm cột, rồi ghép lại thành một feature matrix duy nhất — và toàn bộ chỉ fit trên train.
# (minh hoạ)
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
num_cols = ["tuoi", "thu_nhap", "so_du", "ty_le_dung_han_muc"]
cat_cols = ["tinh_thanh", "loai_the", "kenh_dang_ky"]
# Pipeline cho cột số: điền median -> chuẩn hoá
num_pipe = Pipeline([
("impute", SimpleImputer(strategy="median")),
("scale", StandardScaler()),
])
# Pipeline cho cột phân loại: điền mode -> one-hot
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ộp tiền xử lý + model thành MỘT đối tượng: fit đúng, không leakage
model = Pipeline([("pre", pre), ("clf", LogisticRegression(max_iter=1000))])
# model.fit(X_train, y_train) -> mọi thống kê học từ train, tự áp lên test
Ưu điểm lớn nhất: khi bạn cross_val_score hay GridSearchCV trên pipeline này (xem ML 7 — Mất cân bằng & Tuning), scikit-learn tự động fit tiền xử lý riêng cho từng fold, chống leakage hoàn toàn mà bạn không phải nghĩ.
Ở quy mô lớn, các tổ chức dùng feature store để tính, lưu và tái sử dụng đặc trưng nhất quán giữa train và serving — tránh cảnh "train một kiểu, production một kiểu" (training-serving skew). Đây là chủ đề của MLOps, sẽ nối ở bài sau.
Domain features: nơi kiến thức nghiệp vụ toả sáng
Đặc trưng mạnh nhất thường không đến từ kỹ thuật, mà từ hiểu biết nghiệp vụ. Trong tín dụng ngân hàng, vài đặc trưng "kinh điển" mà chuyên gia rủi ro biết rõ:
- DTI (Debt-to-Income) = tổng nghĩa vụ nợ / thu nhập — thước đo khả năng trả nợ.
- Tỷ lệ sử dụng hạn mức (credit utilization) = dư nợ / hạn mức — sử dụng > 80% là dấu hiệu căng thẳng tài chính.
- Số lần trễ hạn 90 ngày trong 12 tháng gần nhất, tuổi lịch sử tín dụng...
Một model không tự nghĩ ra "chia nợ cho thu nhập". Bạn — người hiểu bài toán — mới làm được, và một đặc trưng như DTI thường có sức dự báo hơn cả chục biến thô cộng lại.
Use case thực tế: đặc trưng cho model churn ngân hàng
Bài toán: dự đoán khách hàng nào sắp rời bỏ (churn) trong 3 tháng tới, dựa trên snapshot cuối tháng. Chiến lược tạo đặc trưng:
- Hành vi giao dịch:
số_giao_dịch_30d,giá_trị_giao_dịch_90d, và tỷ lệgiao_dịch_30d / giao_dịch_90d(đo xu hướng giảm hoạt động — tín hiệu churn rất mạnh). - Recency (thời gian từ sự kiện):
days_since_last_txn,days_since_last_login— càng lâu không hoạt động, nguy cơ rời bỏ càng cao. - Quan hệ sản phẩm:
số_sản_phẩm_đang_dùng, cờcó_khoản_vay,có_thẻ_tín_dụng. Khách gắn bó nhiều sản phẩm ít rời hơn. - Tài chính:
số_dư_trung_bình, xu hướng số dư (rolling mean 3 tháng),tỷ_lệ_dùng_hạn_mức. - Phân loại:
phân_khúc,chi_nhánh,kênh_đăng_ký— one-hot nếu ít hạng mục, target/frequency encoding nếu cardinality cao (nhớ cross-fitting). - Chăm sóc:
số_khiếu_nại_90d,days_since_last_complaint.
Toàn bộ đưa vào một ColumnTransformer: cột số qua median impute + RobustScaler (dữ liệu tài chính lệch mạnh, nhiều ngoại lai), cột phân loại qua mode impute + encode, cột recency giữ nguyên thang ngày. Nối với một classifier như gradient boosting (chịu được thang đo, nắm phi tuyến tốt). Đặc trưng "xu hướng giảm giao dịch" và "recency" gần như luôn nằm top feature importance — và chúng đến từ hiểu nghiệp vụ, không phải từ thuật toán.
Ghi nhớ
- Đặc trưng tốt > model phức tạp. Đầu tư thời gian vào FE cho ROI cao nhất trong đa số dự án.
- Học
fittrên train,transformlên test — với impute, scale, encode, cả feature selection. Đặt tất cả trong pipeline để chống leakage tự động. - Encoding tuỳ cardinality & model: one-hot cho cardinality thấp + model tuyến tính; ordinal chỉ khi có thứ tự thật; target encoding cho cardinality cao nhưng bắt buộc cross-fitting + smoothing; cây chịu label encoding tốt.
- Scaling cần cho model khoảng cách/gradient (KNN, SVM, tuyến tính, NN); cây không cần. Dữ liệu bẩn/lệch → ưu tiên
RobustScaler. - Datetime phải được tách; biến chu kỳ dùng cyclical sin/cos; đặc trưng "thời gian từ sự kiện" và lag/rolling rất mạnh cho chuỗi thời gian.
- Feature selection (filter / RFE / L1) giảm overfitting và tăng tốc — nhưng cũng phải nằm trong CV.
- Domain features (DTI, tỷ lệ dùng hạn mức) là nơi kiến thức nghiệp vụ tạo lợi thế mà thuật toán không thay được.
Bài tiếp theo ta sẽ dùng chính các đặc trưng này để huấn luyện những model nền tảng: ML 3 — Linear & Logistic.
Bài viết liên quan
Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.
Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.
Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.
Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.