Chương 2·Bài 2.11

Feature engineering

Feature engineering là tạo thêm cột đầu vào từ dữ liệu có sẵn để model đoán tốt hơn. Bài dùng bài toán dự đoán số đơn hàng mỗi giờ của các cửa hàng: từ thời điểm đặt hàng tạo ra giờ trong ngày, ngày trong tuần, số đơn của giờ trước. Quy tắc quan trọng nhất là chỉ dùng thông tin đã có tại lúc dự đoán. Nếu lỡ dùng số đơn của chính giờ cần dự đoán, điểm validation sẽ rất đẹp nhưng model không dùng được thật.

Mỗi mô hình cần một kiểu đặc trưng

Ví dụ đơn giản nhất của tạo đặc trưng là tính tuổi từ ngày sinh. Học máy cổ điển cần người làm việc này; mạng học sâu ở các chương sau tự học đặc trưng từ ảnh, chữ, âm thanh. Với dữ liệu bảng cỡ vừa, gradient boosting trên cây kèm vài đặc trưng tốt thường là lựa chọn mạnh nhất.

Cùng một dữ liệu, cách biến đổi đúng phụ thuộc mô hình sẽ dùng:

Thao tác Mô hình tuyến tính, kNN, SVM Mô hình cây
Chuẩn hóa thang đo Cần Không cần
Log cho biến lệch Thường có ích Gần như không đổi, vì log giữ thứ tự
One-hot cột phân loại Cần Thường kém ordinal hoặc hỗ trợ danh mục sẵn có
Tương tác, tỉ số Phải tự tạo Học được phần nào, nhưng tạo sẵn vẫn giúp
Giá trị thiếu Phải điền XGBoost, LightGBM, HistGB tự xử lý

Hỏi nhanh

Áp log⁡(1+x)\log(1 + x) cho cột thu nhập, rồi học lại một cây quyết định trên cùng dữ liệu. Chuyện gì xảy ra?

Đặc trưng số

  • Biến đổi. Biến lệch phải (thu nhập, số lượt xem) qua log⁡(1+x)\log(1 + x) trở nên cân đối hơn, có ích cho mô hình tuyến tính và kNN. Với target lệch như giá nhà, học log⁡(giaˊ)\log(\text{giá}) biến sai số thành sai số theo tỉ lệ: lệch 10% ở căn 3 tỉ hay căn 300 tỉ tính như nhau, nên vài căn rất đắt không lấn át loss.
  • Tỉ số và hiệu. Tổng tiền chia số món ra giá trung bình mỗi món; số đơn trừ số đơn hủy ra số đơn thành công. Những phép tính con người thấy “có nghĩa” thường là đặc trưng tốt, vì cây phải dùng rất nhiều ngưỡng mới xấp xỉ được một phép chia số học. Điều kiện duy nhất: mọi thành phần của phép tính phải có sẵn lúc dự đoán.
  • Rời rạc hóa (binning) thành khoảng, ví dụ nhóm tuổi: đôi khi giúp mô hình tuyến tính bắt được quan hệ không đơn điệu.
Thu nhập dồn sát 0 với đuôi dài; sau log(1 + x) gần cân đối.thu nhập (lệch phải)log(1 + x)
Thu nhập dồn sát 0 với đuôi dài; sau log(1 + x) gần cân đối.

Hỏi nhanh

Cần dự đoán giá nhà trước khi bán. Cột price/area được tạo từ giá bán thực tế. Lúc dự đoán, area đã biết nhưng price thì chưa. Feature này có hợp lệ không?

Đặc trưng phân loại

Cách mã hóa Ý tưởng Dùng khi
One-hot Mỗi giá trị một cột 0/1 Ít giá trị khác nhau; mô hình tuyến tính
Ordinal Mỗi giá trị một số nguyên Có thứ tự thật (S < M < L), hoặc cho mô hình cây
Tần suất Thay giá trị bằng số lần xuất hiện Nhiều giá trị; độ phổ biến có ý nghĩa
Target encoding Thay giá trị bằng trung bình yy của nó Rất nhiều giá trị (mã quận, mã sản phẩm)

Target encoding mạnh nhưng nguy hiểm: dùng yy để tạo đặc trưng chính là cửa ngõ rò rỉ dữ liệu. Hai quy tắc:

  1. Tính out-of-fold. Giá trị mã hóa của một mẫu chỉ được tính từ các fold khác, không bao giờ dùng chính yy của nó.
  2. Làm mượt (smoothing) cho giá trị hiếm. Với giá trị cc xuất hiện ncn_c lần, trung bình nhãn yˉc\bar y_c, trung bình toàn cục yˉ\bar y và hệ số mm:
TE(c)=nc yˉc+m yˉnc+m.\text{TE}(c) = \frac{n_c\,\bar y_c + m\,\bar y}{n_c + m}.

Ví dụ: quận A chỉ có 3 mẫu và cả 3 đều có nhãn 1, trong khi tỉ lệ chung là 0.3. Với m=10m = 10, TE(A)=(3⋅1+10⋅0.3)/13≈0.46\text{TE}(A) = (3 \cdot 1 + 10 \cdot 0.3)/13 \approx 0.46, không phải 1.0. Ba mẫu chưa đủ để tin rằng quận A “chắc chắn” mang nhãn 1.

Giá trị ít mẫu bị kéo về tỉ lệ chung; nhiều mẫu thì gần trung bình riêng.00.30.510310204060số mẫu n_c của giá trịtrung bình riêng 1.0trung bình riêng 0.5A: 0.46B: 0.43tỉ lệ chung 0.3
Giá trị ít mẫu bị kéo về tỉ lệ chung; nhiều mẫu thì gần trung bình riêng.

Hỏi nhanh

Vẫn m=10m = 10 và tỉ lệ chung là 0.30.3. Quận B có 20 mẫu, một nửa mang nhãn 1. Giá trị mã hóa của B bằng bao nhiêu?

CatBoost cài sẵn một biến thể an toàn của target encoding; LightGBM cũng nhận trực tiếp cột phân loại.

Khi một mẫu có thể mang nhiều giá trị cùng lúc, như một phim có nhiều diễn viên trong danh sách 200 người, dùng multi-hot: 200 cột 0/1, cho phép nhiều số 1 trên cùng một hàng.

Giá trị thiếu

Trước khi xử lý, hỏi vì sao ô bị trống. Xóa mọi dòng có ô trống thường đắt hơn tưởng: 20 cột, mỗi ô thiếu ngẫu nhiên 15% thì chỉ 0.8520≈4%0.85^{20} \approx 4\% số dòng còn đủ.

  • Điền bằng median (số) hoặc giá trị phổ biến nhất (phân loại), học trên train rồi áp lên mọi tập.
  • Thêm cột chỉ báo “có bị thiếu không”. Việc thiếu cũng là thông tin: khách bỏ trống ô thu nhập có thể khác hẳn khách điền đầy đủ.
  • Với các thư viện boosting, thường để nguyên NaN cho mô hình tự học hướng đi tốt nhất.

Điền trung bình còn làm cột trông ít biến động hơn thực tế: cột 2, 4, 8 cùng hai ô trống điền bằng trung bình 14/314/3 thì phương sai giảm từ 6.226.22 xuống 3.733.73.

Hỏi nhanh

Khách bỏ trống ô thu nhập có tỉ lệ vỡ nợ 40%, khách điền đầy đủ chỉ 10%. Ta điền median vào ô trống và không thêm cột nào khác. Hệ quả chính là gì?

Thời gian

  • Trước hết đưa mọi cách viết ngày (“11/10/2026”, “2026-10-11”) về một định dạng, và biết chắc “11/10” là ngày-tháng hay tháng-ngày.
  • Tách ngày giờ thành năm, tháng, ngày trong tuần, giờ, ngày lễ, cuối tuần.
  • Đặc trưng có chu kỳ như giờ trong ngày nên mã hóa trên đường tròn, để 23 giờ và 0 giờ nằm gần nhau:
xsin⁡=sin⁡2πtT,xcos⁡=cos⁡2πtT,T=24.x_{\sin} = \sin\frac{2\pi t}{T}, \qquad x_{\cos} = \cos\frac{2\pi t}{T}, \qquad T = 24.

Với cách này, khoảng cách giữa 23 giờ và 0 giờ là 0.260.26, còn giữa 12 giờ và 0 giờ là 22, đúng như trực giác. Nếu chỉ dùng số giờ thì 23 và 0 cách nhau xa nhất.

Mã hóa (sin, cos) đặt 23 giờ cạnh 0 giờ trên vòng tròn.06121823|23 − 0| = 23số giờ thô612180230-23: 0.260-12: 2(sin, cos) trên vòng tròn
Mã hóa (sin, cos) đặt 23 giờ cạnh 0 giờ trên vòng tròn.
Giờ trong ngày chạy từ 18 giờ qua nửa đêm: trên trục số 23 và 0 cách nhau xa nhất; mã hóa (sin, cos) đặt chúng cạnh nhau trên vòng tròn.

Hỏi nhanh

Sau khi mã hóa giờ bằng sin và cos với T=24T = 24, khoảng cách Euclid giữa 22 giờ và 2 giờ bằng bao nhiêu?

Chuỗi thời gian

Dự đoán yty_t từ quá khứ, các đặc trưng hay dùng:

  • Lag: yt−1,yt−7,yt−365y_{t-1}, y_{t-7}, y_{t-365}, tùy chu kỳ của dữ liệu.
  • Thống kê cửa sổ trượt (rolling): trung bình, độ lệch chuẩn của kk bước gần nhất.
  • Sai phân quá khứ: yt−1−yt−2y_{t-1} - y_{t-2} để dự đoán yty_t, đo xu hướng từ những giá trị đã có.

Quy tắc vàng: mọi đặc trưng tại thời điểm tt chỉ được dùng thông tin trước tt. Rolling mean phải dịch lùi một bước (shift(1)) trước khi lấy cửa sổ; nếu không, giá trị cần dự đoán đã nằm sẵn trong đặc trưng. Validation cũng phải chia theo thời gian (bài 1.3).

Hỏi nhanh

Dùng sai phân yt−yt−1y_t - y_{t-1} làm feature để dự đoán yty_t. Có hợp lệ không?

Lag và rolling bằng pandas

Cửa hàng A có doanh số 5, 7, 6 ở ba giờ liên tiếp. Khi dự đoán giờ kế tiếp, lag_1 bằng 6 và trung bình hai giá trị gần nhất là (7+6)/2=6.5(7+6)/2=6.5. Nhưng nếu đang dự đoán chính giờ có số 6 mà rolling lại gồm số 6, ta đã dùng đáp án. shift(1) đẩy mỗi giá trị xuống một hàng: doanh số của một giờ chỉ được dùng từ giờ sau.

Python
import numpy as np
import pandas as pd

history = pd.DataFrame({"shop": ["A", "A", "A", "B", "B"],
                        "time": [1, 2, 3, 1, 2],
                        "sales": [5., 7., 6., 3., 4.]})
history = history.sort_values(["shop", "time"])
history["lag_1"] = history.groupby("shop")["sales"].shift(1)
history["mean_prev_2"] = history.groupby("shop")["sales"].transform(
    lambda s: s.shift(1).rolling(2, min_periods=2).mean()
)
print(history.to_string(index=False))
assert history.loc[2, "mean_prev_2"] == 6.

Ở giờ 3 của A, mean_prev_2 là 6 vì hai giờ trước có 5 và 7. Đầu chuỗi thiếu lịch sử nên NaN là đúng, không phải code hỏng. Có thể điền bằng giá trị học trên train, hoặc để model tự xử lý NaN; điền bằng dữ liệu tương lai cho “đỡ mất hàng” là leakage. Nhớ sort theo thời gian trong từng nhóm trước khi shift.

Sau shift(1), lag và rolling chỉ dùng giờ trước giờ cần dự đoán.giờ 1giờ 2giờ 3 (cần dự đoán)576lag_1 = 7đúngshift(1).rolling(2) = 6đúngrolling(2) = 6.5sai: chứa target
Sau shift(1), lag và rolling chỉ dùng giờ trước giờ cần dự đoán.

Hỏi nhanh

Quên shift(1), viết thẳng rolling(2).mean() trên doanh số 5, 7, 6 của cửa hàng A. Giá trị ở giờ 3 bằng bao nhiêu?

Dự báo nhiều giờ một lúc. Nếu phải dự đoán cả khối giờ tương lai một lần, doanh số của những giờ đầu khối chưa có để làm lag cho giờ sau. Khi đó hoặc dự đoán đệ quy (lấy dự đoán của giờ trước làm lag), hoặc chỉ dùng lag đủ xa, ví dụ đoán 24 giờ tới thì chỉ dùng lag từ 24 trở lên. Không được lấy target thật của validation làm lag như thể đã biết.

Đặc trưng gộp nhóm

Thống kê theo nhóm thường là đặc trưng mạnh nhất trong dữ liệu bảng: số đơn trung bình của mỗi khách, giá trung bình của mỗi danh mục, độ lệch của một giao dịch so với thói quen của chính người đó. Với pandas, đó là một dòng groupby(...).transform("mean"). Nếu thống kê có dùng yy, quy tắc out-of-fold ở phần target encoding vẫn áp dụng.

Hỏi nhanh

Thêm cột shop_mean = groupby("shop")["sales"].transform("mean"), tính trên toàn bảng train theo giờ, rồi dùng nó để dự đoán sales. Có vấn đề gì?

Giá trị mới ở test

Mã cửa hàng là danh mục, không phải số để so lớn nhỏ. One-hot dành một cột cho mỗi giá trị đã thấy; ở test có mã mới thì handle_unknown="ignore" cho toàn số 0 trong nhóm cột đó. Code chạy được, nhưng mô hình không biết gì về cửa hàng mới. Muốn đoán tốt cửa hàng mới, cần đặc trưng chung như khu vực hay diện tích, nếu thông tin đó có sẵn.

Code dưới ghép điền thiếu, chuẩn hóa và one-hot vào một ColumnTransformer, fit trên train rồi transform một cửa hàng mới.

Python
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

train_features = pd.DataFrame({"size": [10., np.nan, 30.], "shop": ["A", "B", "A"]})
new_features = pd.DataFrame({"size": [20.], "shop": ["C"]})
numeric = make_pipeline(SimpleImputer(strategy="median"), StandardScaler())
preprocessor = ColumnTransformer([
    ("number", numeric, ["size"]),
    ("category", OneHotEncoder(handle_unknown="ignore", sparse_output=False), ["shop"]),
])
encoded_train = preprocessor.fit_transform(train_features)
encoded_new = preprocessor.transform(new_features)
print("shape:", encoded_train.shape, encoded_new.shape)
print("mẫu mới:", encoded_new.round(3).tolist())

Hàng mới có cột số chuẩn hóa bằng 0 vì 20 bằng median/mean của train sau điền thiếu, còn hai cột A/B đều bằng 0 vì C chưa xuất hiện. Quan trọng là không fit_transform lại trên từng tập: như vậy số cột và ý nghĩa cột có thể đổi, chưa kể thống kê validation bị dùng để học phép biến đổi.

Hỏi nhanh

Trong code trên, nếu lỡ gọi preprocessor.fit_transform(new_features) thay vì transform, đầu ra có shape gì?

Văn bản, ảnh, âm thanh

Dữ liệu không có cấu trúc cũng cần thành vector:

  • Văn bản: đếm từ, TF-IDF (bài 6.1), hoặc embedding câu (bài 6.2).
  • Ảnh, âm thanh: embedding từ model pretrained (Chương 4).

Sau đó, mọi kỹ thuật của chương này dùng lại được: PCA cho embedding, kNN trên embedding, gradient boosting trên embedding ghép với đặc trưng bảng.

Làm có kiểm chứng

Thêm đặc trưng là thêm giả thuyết; giả thuyết cần được kiểm. Quy trình đơn giản mà hiệu quả:

  1. Dựng baseline và ghi điểm cross-validation.
  2. Thêm một nhóm đặc trưng, đo lại bằng đúng cùng các fold. Giữ nếu tăng rõ, bỏ nếu không.
  3. Ghi lại mọi thử nghiệm: nhóm đặc trưng, điểm từng fold, thời gian chạy.

Làm ngược lại, tức bỏ từng nhóm khỏi mô hình đầy đủ (ablation), cho biết nhóm nào thật sự gánh điểm. Feature importance của mô hình cây chỉ là gợi ý; permutation importance trên validation đáng tin hơn.

Đừng lọc đặc trưng chỉ bằng điểm của từng cột riêng lẻ: với y=x1 XOR x2y = x_1 \text{ XOR } x_2, mỗi cột một mình không nói gì về yy (thông tin tương hỗ bằng 0), nhưng hai cột cùng nhau xác định yy hoàn toàn.

Hỏi nhanh

Mô hình đầy đủ có MAE 2.0 trên các fold cố định. Bỏ nhóm lag thì MAE là 2.9, bỏ nhóm thời gian là 2.02, bỏ nhóm thống kê cửa hàng là 2.3. Nhóm nào đóng góp nhiều nhất?

Mỗi đặc trưng nên trả lời được hai câu: nó được biết lúc nào, và vì sao có thể liên quan target. Ví dụ “giờ cuối tuần” có thể đổi nhu cầu thật; còn “mã đơn” có thể chỉ vô tình tăng theo thời gian.

Hỏi nhanh

Với mô hình cây, cột order_id đứng đầu bảng feature importance. Phản ứng hợp lý nhất là gì?

Danh sách kiểm tra rò rỉ

Trước khi vui mừng vì điểm validation tăng vọt, tự hỏi:

  • Đặc trưng này có dùng yy của chính mẫu đó không?
  • Có dùng thông tin từ tương lai không?
  • Có được tính trên cả train lẫn test cùng lúc, kiểu chuẩn hóa hay đếm tần suất trên toàn bộ dữ liệu, theo cách mà lúc triển khai thật không làm được không?
  • Lúc dự đoán thật, đặc trưng này có sẵn không?

Một đặc trưng tốt đến mức khó tin thì thường đúng là không nên tin.

Hỏi nhanh

Đếm tần suất của mã sản phẩm trên train và test gộp lại, không dùng nhãn. Cách này hợp lệ khi nào?

Dùng thư viện

Python
import numpy as np
import pandas as pd

df = pd.DataFrame({
    "time": pd.date_range("2026-10-01", periods=6, freq="h"),
    "shop": ["A", "B", "A", "A", "B", "A"],
    "sales": [5, 3, 7, 6, 4, 9],
})
df["hour_sin"] = np.sin(2 * np.pi * df["time"].dt.hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["time"].dt.hour / 24)
df["lag_1"] = df.groupby("shop")["sales"].shift(1)
df["roll_mean_2"] = df.groupby("shop")["sales"].transform(lambda s: s.shift(1).rolling(2).mean())
df["shop_count"] = df.groupby("shop").cumcount()  # số lần đã xuất hiện trước hàng hiện tại
print(df.drop(columns="time").round(2).to_string())

Hỏi nhanh

Hàng 4, thuộc cửa hàng B, có lag_1 = 3 nhưng roll_mean_2 lại là NaN. Vì sao?

Khi đi thi

  • Với mỗi cột, hỏi: sinh ra lúc nào, lúc dự đoán đã có chưa.
  • Target encoding và mọi thống kê dùng nhãn phải tính out-of-fold.
  • Mỗi lần thêm một nhóm feature, so trên cùng các fold.

Tóm tắt

  • Đặc trưng là nơi kiến thức về bài toán biến thành điểm số; cách biến đổi phụ thuộc mô hình.
  • Cột phân loại: one-hot, ordinal, tần suất, target encoding (out-of-fold, có làm mượt).
  • Thời gian: tách thành phần, mã hóa chu kỳ bằng sin/cos; chuỗi thời gian dùng lag và rolling chỉ từ quá khứ.
  • Thêm đặc trưng có kiểm chứng bằng cross-validation; luôn soát rò rỉ.

Kiểm tra cuối bài

Mười câu kiểm cả cách hiểu, phép tính và quy trình. Câu sai chỉ ra phần cần đọc lại; hãy làm trước khi mở lời giải.

Câu 1

Cần dự đoán giá bán của ngày mai. Nếu các dữ kiện được mô tả đúng, feature nào tạo được ngay lúc dự đoán?

Câu 2

Dự đoán số khách của một buổi hòa nhạc. Một feature là “số khách của buổi trước, nếu biết”. Nên mã hóa thế nào?

Phỏng theoMIT 6.036 · Final, Spring 2022 · Q5(a)

Câu 3

Doanh số các giờ đã biết là 4, 8, 6. Ta dự báo giờ kế tiếp, dùng trung bình trượt của hai giờ gần nhất. Feature hợp lệ bằng bao nhiêu?

Câu 4

Bảng gồm nhiều cửa hàng, các hàng chưa xếp theo thời gian. Muốn tạo lag 1 bằng groupby("store").shift(1), ta phải làm gì trước?

Câu 5

OneHotEncoder fit trên hai loại A và B, với drop=None và handle_unknown="ignore". Validation có hai loại B và C. Sau transform, hai vector là gì?

Câu 6

Dữ liệu có D=10D = 10 feature. Mở rộng đa thức bậc hai gồm mọi số hạng bậc một, mọi bình phương và mọi tích chéo xixjx_ix_j với i<ji < j, không tính hệ số tự do. Có tất cả bao nhiêu feature mới?

Phỏng theoCMU 10-601 · Exam 2 Practice · Q3.1(a)

Câu 7

Một loại hiếm chỉ có một mẫu train, với y=100y = 100. Target encoding lấy trung bình theo loại trên toàn bộ train, tính cả nhãn của chính mẫu ấy. Rủi ro cụ thể là gì?

Câu 8

Mô hình A trên bộ fold cũ có MAE 3.0. Thêm một feature mới và đổi sang bộ fold khác thì MAE còn 2.6. Muốn biết riêng tác động của feature, ta chạy gì tiếp?

Câu 9

Lúc 0 giờ, ta phải dự báo cùng lúc cả 24 giờ tới, và không nhận được nhãn nào trong khối ấy. Lag của giờ 2 có được dùng doanh số thật của giờ 1 không?

Câu 10

Đầu vào (x1,x2)(x_1, x_2) được thay bằng vector feature (1,x12,x22)(1, x_1^2, x_2^2), rồi học logistic regression với tham số (b,w1,w2)(b, w_1, w_2). Ranh giới quyết định, nơi P(Y=1)=P(Y=0)P(Y=1) = P(Y=0), là tập nào?

Phỏng theoCMU 10-601 · Exam 2 Practice · Q2.5(a)

Đọc thêm

  • Kuhn, Johnson - Feature Engineering and Selection, bản đọc trực tuyến miễn phí. Biểu diễn dữ liệu, lựa chọn đặc trưng và kiểm nghiệm.
  • Zheng, Casari - Feature Engineering for Machine Learning. Từ biến đổi số, mã hóa danh mục đến văn bản.
  • Géron - Hands-On Machine Learning, chương 2. Một dự án trọn vẹn với ColumnTransformer và pipeline.

  • scikit-learn - Preprocessing data. Scaler, encoder, imputer và cách ghép chúng vào pipeline.

  • scikit-learn - TargetEncoder. Đọc khác biệt fit_transform với fit rồi transform và cơ chế cross-fitting.