Danh sách bài viết

Bài 11: Label Encoding và Ordinal Encoding

OrdinalEncoder cho feature ordinal với thứ tự custom, LabelEncoder cho target y, pitfall dùng label cho nominal, handle_unknown trong production, và pandas Categorical với ordered=True.

24/05/2026
12 phút đọc
0 lượt xem
1

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 OrdinalEncoder với tham số categories để giữ đúng thứ tự ngữ nghĩa.
  • Dùng LabelEncoder cho 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.
2

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 khi fit().

Hai class trên dễ nhầm vì tên gần nhau. Mục 6 sẽ phân định rõ ranh giới.

3

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.

4

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.

categorieslist 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).

5

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.

6

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íOrdinalEncoderLabelEncoder
Dùng choFeature XTarget y
Input shape2D (n_samples, n_features)1D (n_samples,)
Nhiều cộtCó — list of categoriesKhông, chỉ 1 vector
Control thứ tựCó, qua categories=[...]Không, luôn sort alphabet
handle_unknownKhông
Tích hợp PipelineCó, vào ColumnTransformerKhô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.

7

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.

8

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ạiEncoderGhi chú
Nominal feature, cardinality thấp (< 50)OneHotEncoderBài 10. Bật handle_unknown="ignore" cho production.
Nominal feature, cardinality caoTarget encoding / Frequency / Embedding / Gộp rare + OHEBài 10 mục 10. sklearn.preprocessing.TargetEncoder (1.3+) hoặc category_encoders.
Ordinal featureOrdinalEncoder(categories=[...])Truyền thứ tự đúng từ domain knowledge.
Ordinal feature nhưng khoảng cách không đềuOneHotEncoderMục 11 — khi rating 4→5 khác xa 1→2.
Target y là string (classification)LabelEncoderHoặc y.map({...}) nếu cần control thứ tự class.
Target y là số (regression)Không cần encodeCó thể scale nếu loss nhạy magnitude.
9

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_unknown built-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.

10

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ới missing, LightGBM). Cần tham số encoded_missing_value nế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.

11

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.

12

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 dtype category của pandas. Cast cột về int sau 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 + dtype category. 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 (vd OrdinalEncoder) 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.

13

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, sizeeducation 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.

14

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ố).

15

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.