Mục lục
- Mục tiêu bài học
- Recap Bài 10 và vị trí của bài này
- Ordinal data — khi thứ tự có nghĩa
- OrdinalEncoder của sklearn
- LabelEncoder — chỉ cho target y
- OrdinalEncoder vs LabelEncoder
- Pitfall — dùng Label cho nominal
- Bảng quyết định chọn encoder
- pandas Categorical với ordered=True
- handle_unknown trong production
- Khi ordinal vẫn nên one-hot
- Tree-based với categorical native
- Demo end-to-end
- Bài tập thực hành
- Bài tiếp theo
Mục tiêu bài học
Sau bài này, bạn sẽ:
- Phân biệt ordinal feature với nominal feature qua ví dụ cụ thể.
- Dùng
OrdinalEncodervới tham sốcategoriesđể giữ đúng thứ tự ngữ nghĩa. - Dùng
LabelEncodercho target y và biết vì sao không dùng cho feature X. - Nhận diện và tránh pitfall lớn nhất: dùng label encoding cho nominal feature.
- Cấu hình
handle_unknownđể encoder không vỡ ở inference. - Có bảng quyết định nhanh cho từng loại cột.
Recap Bài 10 và vị trí của bài này
Bài 10 đã chốt: nominal feature (không có thứ tự ngầm) → OneHotEncoder. Lý do: gán số trực tiếp cho nominal tạo ra thứ tự và khoảng cách giả mà model linear/SVM/KNN/NN sẽ dùng để học sai.
Bài này xử lý hai trường hợp còn lại:
- Ordinal feature (có thứ tự ngầm) →
OrdinalEncoder: gán số nguyên theo đúng thứ tự ngữ nghĩa, model tận dụng được thứ tự đó. - Target y là string/category →
LabelEncoder: convert nhãn thành integer trước khifit().
Hai class trên dễ nhầm vì tên gần nhau. Mục 6 sẽ phân định rõ ranh giới.
Ordinal data — khi thứ tự có nghĩa
Ordinal = giá trị thuộc tập hữu hạn nhãn nhưng có thứ tự rõ ràng giữa các nhãn. Vài ví dụ thường gặp:
size∈ {S, M, L, XL} — S nhỏ nhất, XL lớn nhất.education∈ {Tiểu học, Cấp 2, Cấp 3, Đại học, Sau đại học} — trình độ tăng dần.rating∈ {Poor, Average, Good, Excellent} — chất lượng tăng dần.satisfaction∈ {1 sao, 2 sao, 3 sao, 4 sao, 5 sao} — điểm tăng dần.priority∈ {low, medium, high, critical} — mức độ ưu tiên tăng dần.
Với loại này, gán số nguyên có nghĩa: S=0, M=1, L=2, XL=3 phản ánh đúng thứ tự thực tế. Model linear thấy L > M > S đúng với ngữ nghĩa; KNN tính khoảng cách |XL - S| > |M - S| cũng hợp lý.
Điều kiện cần kiểm tra trước khi encode ordinal: thứ tự phải đến từ domain knowledge, không phải tự bịa. Nếu không chắc {Poor, Average, Good, Excellent} có thật sự tăng dần đều hay không, mục 11 sẽ bàn về việc khi nào nên one-hot thay vì ordinal.
OrdinalEncoder của sklearn
API trong sklearn.preprocessing:
import numpy as np
from sklearn.preprocessing import OrdinalEncoder
X = np.array([["M"], ["S"], ["XL"], ["L"], ["M"]])
encoder = OrdinalEncoder(categories=[["S", "M", "L", "XL"]])
X_encoded = encoder.fit_transform(X)
print(X_encoded)
# [[1.]
# [0.]
# [3.]
# [2.]
# [1.]]
print(encoder.categories_)
# [array(['S', 'M', 'L', 'XL'], dtype=object)]
Bắt buộc truyền tham số categories=[...] với thứ tự đúng. Nếu bỏ tham số này, sklearn sort alphabet — kết quả sẽ là L=0, M=1, S=2, XL=3, sai hoàn toàn thứ tự ngữ nghĩa.
categories là list of list: mỗi list con là thứ tự cho một cột input. Với nhiều cột:
X = np.array([
["S", "Poor"],
["L", "Good"],
["XL", "Excellent"],
["M", "Average"],
])
encoder = OrdinalEncoder(categories=[
["S", "M", "L", "XL"], # cột 0: size
["Poor", "Average", "Good", "Excellent"], # cột 1: rating
])
print(encoder.fit_transform(X))
# [[0. 0.]
# [2. 2.]
# [3. 3.]
# [1. 1.]]
Input phải 2 chiều, shape (n_samples, n_features) — giống OneHotEncoder. Một cột vẫn cần reshape (-1, 1).
LabelEncoder — chỉ cho target y
LabelEncoder convert array 1D các nhãn thành integer:
from sklearn.preprocessing import LabelEncoder
y = ["spam", "ham", "spam", "ham", "spam"]
le = LabelEncoder()
y_encoded = le.fit_transform(y)
print(y_encoded)
# [1 0 1 0 1]
print(le.classes_)
# ['ham' 'spam'] <- sort alphabet, ham=0, spam=1
Đặc điểm quan trọng:
- Input là array 1D — không reshape, không 2D. Nếu truyền 2D sẽ raise lỗi.
- Class sort alphabet, không control được thứ tự. Không có tham số
categories. - Lưu lại tập class qua
le.classes_để inverse_transform sau:
le.inverse_transform([0, 1, 1, 0])
# array(['ham', 'spam', 'spam', 'ham'], dtype='<U4')
Quy tắc duy nhất cần nhớ: LabelEncoder chỉ dùng cho target y. Không dùng cho feature X — kể cả khi X là ordinal, vì bạn không control được thứ tự.
Use case điển hình: classification có target string (spam/ham, malignant/benign, cat/dog/bird) và model sklearn cần y kiểu int.
OrdinalEncoder vs LabelEncoder
Hai class này dễ lẫn vì cùng "biến nhãn thành số nguyên". Khác biệt cụ thể:
| Tiêu chí | OrdinalEncoder | LabelEncoder |
|---|---|---|
| Dùng cho | Feature X | Target y |
| Input shape | 2D (n_samples, n_features) | 1D (n_samples,) |
| Nhiều cột | Có — list of categories | Không, chỉ 1 vector |
| Control thứ tự | Có, qua categories=[...] | Không, luôn sort alphabet |
| handle_unknown | Có | Không |
| Tích hợp Pipeline | Có, vào ColumnTransformer | Không nên — chỉ cho y |
Vì sao tách thành hai class? Convention sklearn: transformer cho X nhận 2D, transformer cho y nhận 1D. Bỏ vào cùng pipeline với X sẽ vướng shape. Sklearn 0.20+ đã thêm OrdinalEncoder đúng cho feature, để lại LabelEncoder chuyên cho y.
Pitfall — dùng Label cho nominal
Đây là lỗi encoding hay gặp nhất trong notebook người mới: dùng LabelEncoder (hoặc OrdinalEncoder không truyền categories) cho feature nominal.
Ví dụ: city ∈ {HN, HCM, DN} — nominal, không có thứ tự thực tế.
from sklearn.preprocessing import LabelEncoder
cities = ["HN", "HCM", "DN", "HN", "HCM"]
le = LabelEncoder()
encoded = le.fit_transform(cities)
print(le.classes_) # ['DN' 'HCM' 'HN']
print(encoded) # [2 1 0 2 1]
Sau encode, model thấy HN(2) > HCM(1) > DN(0) — một thứ tự hoàn toàn bịa do alphabet, không liên quan gì đến thực tế. Hệ quả theo loại model:
- Linear / Logistic Regression: học coefficient cho cột
city_encoded, áp dụng cùng một hệ số tuyến tính cho mọi giá trị. Mô hình bị ép giả định "thay đổi 1 unit của city tương ứng thay đổi cố định của y", trong khi 3 thành phố không có quan hệ tuyến tính như vậy. - KNN / SVM (kernel): tính khoảng cách
|HN - DN| = 2,|HCM - DN| = 1— model tưởng DN gần HCM hơn HN, vô căn cứ. - Neural Network: weight nhân với 0, 1, 2 — gradient sai hướng vì input space mất tính rời rạc.
- Tree-based (Decision Tree, Random Forest, XGBoost): ít nhạy hơn vì split dạng
city ≤ 0.5,city ≤ 1.5— vẫn cô lập được từng nhóm. Nhưng tree phải dùng nhiều split để mô tả 1 quan hệ rời rạc — kém hiệu quả hơn one-hot và đôi khi mất signal khi cardinality cao.
Quy tắc an toàn: feature nominal → luôn OneHotEncoder (Bài 10), hoặc target encoding / embedding cho cardinality cao. Không bao giờ LabelEncoder/OrdinalEncoder cho nominal feature.
Bảng quyết định chọn encoder
Quy chiếu nhanh khi xử lý dataset có cột categorical:
| Cột thuộc loại | Encoder | Ghi chú |
|---|---|---|
| Nominal feature, cardinality thấp (< 50) | OneHotEncoder | Bài 10. Bật handle_unknown="ignore" cho production. |
| Nominal feature, cardinality cao | Target encoding / Frequency / Embedding / Gộp rare + OHE | Bài 10 mục 10. sklearn.preprocessing.TargetEncoder (1.3+) hoặc category_encoders. |
| Ordinal feature | OrdinalEncoder(categories=[...]) | Truyền thứ tự đúng từ domain knowledge. |
| Ordinal feature nhưng khoảng cách không đều | OneHotEncoder | Mục 11 — khi rating 4→5 khác xa 1→2. |
| Target y là string (classification) | LabelEncoder | Hoặc y.map({...}) nếu cần control thứ tự class. |
| Target y là số (regression) | Không cần encode | Có thể scale nếu loss nhạy magnitude. |
pandas Categorical với ordered=True
Pandas có dtype riêng cho ordinal: pd.Categorical với ordered=True. Dùng khi muốn ordering có sẵn ở data layer, không cần qua sklearn:
import pandas as pd
df = pd.DataFrame({"size": ["M", "S", "XL", "L", "M"]})
df["size"] = pd.Categorical(df["size"], categories=["S", "M", "L", "XL"], ordered=True)
print(df["size"])
# 0 M
# 1 S
# 2 XL
# 3 L
# 4 M
# Categories (4, object): ['S' < 'M' < 'L' < 'XL']
df["size_code"] = df["size"].cat.codes
print(df["size_code"].tolist())
# [1, 0, 3, 2, 1]
.cat.codes trả về integer code theo đúng thứ tự đã khai báo. Tiện cho EDA và groupby (pandas sort theo thứ tự ordinal thay vì alphabet).
Hạn chế so với OrdinalEncoder:
- Không phải estimator — không lưu state để transform trên test set một cách nhất quán.
- Không có
handle_unknownbuilt-in. Category mới ở test →NaNở cột categorical,-1ở.cat.codes(silent).
Khi nào dùng cái nào: pd.Categorical cho EDA và xử lý dataframe; OrdinalEncoder cho pipeline ML chính thức.
handle_unknown trong production
Mặc định, OrdinalEncoder raise ValueError khi gặp category chưa thấy ở fit:
encoder = OrdinalEncoder(categories=[["S", "M", "L", "XL"]])
encoder.fit(np.array([["S"], ["M"], ["L"], ["XL"]]))
try:
encoder.transform(np.array([["XXL"]]))
except ValueError as e:
print("Lỗi:", e)
# Lỗi: Found unknown categories ['XXL'] in column 0 during transform
Trong serving, một typo hoặc nhãn mới phát sinh sẽ làm crash request. Bật handle_unknown="use_encoded_value" kèm unknown_value:
encoder = OrdinalEncoder(
categories=[["S", "M", "L", "XL"]],
handle_unknown="use_encoded_value",
unknown_value=-1,
)
encoder.fit(np.array([["S"], ["M"], ["L"], ["XL"]]))
print(encoder.transform(np.array([["XXL"]])))
# [[-1.]]
Các option phổ biến cho unknown_value:
-1— giá trị nằm ngoài range hợp lệ (0..k-1), tree-based và linear vẫn xử lý được. Đơn giản, dễ debug.np.nan— dùng với model hỗ trợ NaN (HistGradientBoosting, XGBoost vớimissing, LightGBM). Cần tham sốencoded_missing_valuenếu muốn xử lý NaN ở input gốc.len(categories)— gán mã liền kề ngay sau range hợp lệ. Phù hợp khi muốn "unknown" được coi như category riêng có thể học weight.
LabelEncoder không có handle_unknown. Nếu cần xử lý class lạ ở y (hiếm), thường gặp ở stream/online learning, phải bọc tay hoặc dùng category_encoders.
Khi ordinal vẫn nên one-hot
Ordinal encoding giả định khoảng cách giữa các mức là đều: từ S→M giống M→L về ảnh hưởng lên y. Giả định này không phải lúc nào cũng đúng.
Ví dụ rating ∈ {1, 2, 3, 4, 5} sao trên app review: trong nhiều dataset thực tế, chênh lệch 4→5 sao tác động đến hành vi mua hàng mạnh hơn nhiều so với 1→2 sao. Encode thành 0,1,2,3,4 → linear model áp một slope duy nhất cho mọi đoạn → mất tính phi tuyến.
Khi nào nên one-hot cho ordinal:
- Khoảng cách giữa các mức không đều và bạn không muốn ép giả định đều.
- Model là linear / logistic không có feature engineering phi tuyến đi kèm.
- Số mức không nhiều (vd 4-6) — chi phí thêm cột chấp nhận được.
Khi nào giữ ordinal:
- Tree-based — split rời rạc tự xử lý phi tuyến, ordinal đủ tốt.
- Muốn giảm chiều và tin rằng thứ tự là signal chính.
- Đã thử nghiệm hai cách và metric không khác đáng kể — chọn cái đơn giản hơn.
Cách thực dụng: với ordinal cardinality thấp, thử cả hai và so sánh validation metric. Không có công thức chung.
Tree-based với categorical native
Một vài model gradient boosting hiện đại nhận categorical raw, không cần encode tay:
- LightGBM:
categorical_feature=[...]hoặc dtypecategorycủa pandas. Cast cột vềintsau ordinal encoding rồi báo cho LightGBM "đây là categorical" — model dùng thuật toán split riêng cho categorical (Fisher exact + grouping), thường tốt hơn one-hot khi cardinality > 20. - CatBoost:
cat_features=[...]. Tự chạy target-statistics encoding với cơ chế chống leak built-in (ordered boosting). Cardinality cao là sở trường. - XGBoost ≥ 1.5:
enable_categorical=True+ dtypecategory. Hỗ trợ cơ bản, không tinh vi bằng LightGBM/CatBoost. - sklearn
HistGradientBoosting*: tham sốcategorical_features(sklearn 1.0+). Cần encode về int trước (vdOrdinalEncoder) rồi báo cột nào là categorical.
Quy trình chuẩn cho gradient boosting hiện đại: OrdinalEncoder (mapping nhãn → int, không quan tâm thứ tự nếu nominal) → khai báo categorical → fit. Không one-hot, không scale.
Demo end-to-end
Bài toán: dự đoán is_buyer ∈ {yes, no} từ city (nominal), size (ordinal), education (ordinal).
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, LabelEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
rng = np.random.default_rng(42)
n = 500
df = pd.DataFrame({
"city": rng.choice(["HN", "HCM", "DN"], size=n),
"size": rng.choice(["S", "M", "L", "XL"], size=n),
"education": rng.choice(["primary", "secondary", "bachelor", "master"], size=n),
})
# Target tự sinh có quan hệ với size + education
score = (
pd.Series(df["size"]).map({"S": 0, "M": 1, "L": 2, "XL": 3}) +
pd.Series(df["education"]).map({"primary": 0, "secondary": 1, "bachelor": 2, "master": 3})
)
df["is_buyer"] = np.where(score + rng.normal(0, 1, size=n) > 3, "yes", "no")
X = df[["city", "size", "education"]]
y = df["is_buyer"]
# Encode target
le = LabelEncoder()
y_enc = le.fit_transform(y)
print("Target classes:", le.classes_)
# Target classes: ['no' 'yes']
# Encode features
preprocessor = ColumnTransformer([
("city", OneHotEncoder(handle_unknown="ignore"), ["city"]),
("size", OrdinalEncoder(
categories=[["S", "M", "L", "XL"]],
handle_unknown="use_encoded_value", unknown_value=-1,
), ["size"]),
("edu", OrdinalEncoder(
categories=[["primary", "secondary", "bachelor", "master"]],
handle_unknown="use_encoded_value", unknown_value=-1,
), ["education"]),
])
X_train, X_test, y_train, y_test = train_test_split(X, y_enc, test_size=0.2, random_state=42)
X_train_enc = preprocessor.fit_transform(X_train)
X_test_enc = preprocessor.transform(X_test)
model = LogisticRegression(max_iter=1000).fit(X_train_enc, y_train)
pred = model.predict(X_test_enc)
print("Accuracy:", accuracy_score(y_test, pred))
Quan sát: city qua one-hot, size và education qua ordinal với thứ tự custom, target y qua LabelEncoder. Mỗi cột dùng đúng class phù hợp với kiểu dữ liệu.
Để thấy hiệu ứng pitfall, thử thay cột city bằng OrdinalEncoder (không truyền categories) — sklearn sort alphabet thành DN=0, HCM=1, HN=2. Train lại Logistic Regression và so sánh accuracy: nominal mà ép thứ tự thường kém hơn one-hot, nhất là khi y thật sự độc lập với "thứ tự" alphabet.
Bài tập thực hành
Bài 1. Cho X = np.array([["M"], ["XS"], ["L"], ["S"], ["XL"], ["M"]]). Encode bằng OrdinalEncoder với thứ tự XS < S < M < L < XL. In ra kết quả encode và encoder.categories_.
Bài 2. Cho y = ["benign", "malignant", "malignant", "benign", "benign"]. Encode bằng LabelEncoder. In ra le.classes_, y_encoded, và verify bằng le.inverse_transform([0, 1]).
Bài 3. Cho DataFrame có 3 cột: city ∈ {HN, HCM, DN}, size ∈ {S, M, L, XL}, education ∈ {primary, secondary, bachelor, master}. Với mỗi cột, xác định loại (nominal/ordinal) và chọn encoder phù hợp. Viết một ColumnTransformer apply đúng encoder cho từng cột; in get_feature_names_out().
Bài 4. Lặp lại bài 3 nhưng đổi OrdinalEncoder cho size thành "không truyền tham số categories". Kiểm tra encoder.categories_ sau fit và giải thích vì sao kết quả sẽ encode sai thứ tự ngữ nghĩa.
Bài 5. Build pipeline đầy đủ với dataset demo ở mục 13. Thử ba biến thể cho cột city: (a) OneHotEncoder, (b) OrdinalEncoder không truyền categories, (c) OrdinalEncoder với categories=[["HN", "HCM", "DN"]]. So sánh accuracy trên test với cùng random seed. Diễn giải kết quả theo phần pitfall của mục 7.
Gợi ý đáp án bài 3: city nominal → OneHotEncoder(handle_unknown="ignore"); size ordinal → OrdinalEncoder(categories=[["S","M","L","XL"]]); education ordinal → OrdinalEncoder(categories=[["primary","secondary","bachelor","master"]]). get_feature_names_out() sẽ ra dạng city_* (3 cột one-hot) + size + education (mỗi cột 1 số).
Bài tiếp theo
Bài 12: Xử lý outlier — IQR và Z-score — phát hiện outlier với IQR (interquartile range) và Z-score, khi nào cắt bỏ, khi nào giữ, và ảnh hưởng tới scaling/model.
Tài liệu tham khảo
- scikit-learn — OrdinalEncoder API reference
- scikit-learn — LabelEncoder API reference
- scikit-learn — User Guide: Encoding categorical features
- pandas — Categorical API reference
- pandas — User Guide: Categorical data
- LightGBM — Categorical Feature Support
- CatBoost — Categorical features handling
- scikit-learn — HistGradientBoostingClassifier (categorical_features)
