Có một câu nói nổi tiếng trong giới Machine Learning: "Data is the new oil." Nhưng thực tế, raw data giống dầu thô hơn - cần phải refined (tinh chế) trước khi sử dụng được.
Bạn có thể có thuật toán ML state-of-the-art nhất, infrastructure xịn nhất, nhưng nếu data bạn feed vào là rác? Kết quả sẽ là rác. Đây chính là nguyên tắc "Garbage In, Garbage Out" (GIGO).
Một nghiên cứu của Google cho thấy: 80% thời gian của ML projects được dành cho data preparation, chỉ 20% còn lại mới là modeling. Đó là lý do tại sao Data Engineering for ML lại quan trọng đến vậy.
Trong thực tế, bạn hiếm khi có dataset hoàn hảo. Missing values (giá trị thiếu) xuất hiện vì:
Ví dụ dataset:
Name Age Income Credit_Score
Alice 28 50000 720
Bob ? 65000 ?
Charlie 35 ? 680
Diana 42 80000 750
Listwise Deletion: Xóa toàn bộ row có bất kỳ missing value nào.
df.dropna() # Drop tất cả rows có missing values
Ưu điểm: Đơn giản, nhanh
Nhược điểm:
Khi nào dùng:
Fill missing values bằng giá trị trung bình/trung vị/mode.
# Mean imputation (cho numerical)
df['Age'].fillna(df['Age'].mean(), inplace=True)
# Median imputation (tốt hơn nếu có outliers)
df['Income'].fillna(df['Income'].median(), inplace=True)
# Mode imputation (cho categorical)
df['City'].fillna(df['City'].mode()[0], inplace=True)
Ưu điểm: Đơn giản, giữ được sample size
Nhược điểm:
Ví dụ vấn đề:
Ages: [25, 28, 30, 32, 95, ?]
Mean = 42 (bị outlier 95 làm lệch)
Median = 30 (robust hơn)
Hữu ích với time series data.
# Forward fill: Dùng giá trị trước đó
df['Temperature'].fillna(method='ffill')
# Backward fill: Dùng giá trị sau đó
df['Temperature'].fillna(method='bfill')
Ví dụ:
Time Temperature
09:00 25.0
09:30 NaN → 25.0 (forward fill)
10:00 26.5
10:30 NaN → 26.5 (forward fill)
Dùng ML model để predict missing values dựa trên các features khác.
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
# Sử dụng regression để predict missing
imputer = IterativeImputer(random_state=42)
df_imputed = imputer.fit_transform(df)
Ví dụ logic:
Missing: Bob's Age
Known: Bob's Income=65000, Credit_Score=680
Model học pattern:
- Income cao → Age thường 30-45
- Credit_Score 680 → Age thường >30
Prediction: Bob's Age ≈ 35
Ưu điểm: Sophisticated, giữ được relationships
Nhược điểm: Tốn thời gian, có thể tạo bias nếu model sai
Tạo binary column để đánh dấu giá trị bị missing.
df['Age_missing'] = df['Age'].isnull().astype(int)
df['Age'].fillna(df['Age'].median(), inplace=True)
Tại sao hữu ích?
Đôi khi việc data bị missing chính là một signal quan trọng!
Ví dụ: Credit card fraud detection
Missing_count có thể là một feature mạnh!Nếu một column missing >70% data → xem xét xóa column thay vì impute:
# Tính % missing cho mỗi column
missing_percent = df.isnull().sum() / len(df) * 100
print(missing_percent)
# Drop columns có >70% missing
df = df.loc[:, missing_percent < 70]
Vấn đề: Features có scale khác nhau rất nhiều.
Feature Range
Age: 18 - 80
Income: 20,000 - 200,000
Credit_Score: 300 - 850
Nhiều ML algorithms (k-NN, SVM, Neural Networks) nhạy cảm với scale:
Ví dụ k-NN (k-Nearest Neighbors):
# Distance giữa 2 người:
Person A: Age=30, Income=50,000
Person B: Age=32, Income=51,000
# Euclidean distance
distance = sqrt((30-32)² + (50000-51000)²)
= sqrt(4 + 1,000,000)
= sqrt(1,000,004) ≈ 1000
# Income "chiếm đa số" distance vì scale lớn hơn Age rất nhiều!
→ Model sẽ "ignore" Age hoàn toàn vì Income có magnitude lớn hơn.
Scale tất cả features về khoảng [0, 1].
Công thức:
X_normalized = (X - X_min) / (X_max - X_min)
Code:
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
df_normalized = scaler.fit_transform(df[['Age', 'Income', 'Credit_Score']])
Ví dụ:
Age: 30 → (30-18)/(80-18) = 0.19
Income: 50,000 → (50k-20k)/(200k-20k) = 0.17
Credit_Score: 720 → (720-300)/(850-300) = 0.76
Ưu điểm:
Nhược điểm:
Khi nào dùng:
Transform data để có mean=0, std=1.
Công thức:
X_standardized = (X - mean) / std
Code:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
df_standardized = scaler.fit_transform(df[['Age', 'Income', 'Credit_Score']])
Ví dụ:
Age: Mean=40, Std=15
Age=30 → (30-40)/15 = -0.67
Income: Mean=80k, Std=40k
Income=50k → (50k-80k)/40k = -0.75
Ưu điểm:
Nhược điểm:
Khi nào dùng:
import numpy as np
import matplotlib.pyplot as plt
# Data with outlier
data = [20, 25, 30, 35, 40, 200] # 200 là outlier
# Normalization
data_norm = (data - np.min(data)) / (np.max(data) - np.min(data))
# Result: [0.0, 0.03, 0.06, 0.08, 0.11, 1.0]
# Outlier chiếm hết range, các giá trị bình thường đều gần 0!
# Standardization
data_std = (data - np.mean(data)) / np.std(data)
# Result: [-0.6, -0.5, -0.4, -0.3, -0.2, 2.1]
# Outlier vẫn nổi bật nhưng không "chiếm đoạt" toàn bộ scale
Rule of thumb:
ML models chỉ hiểu numbers. Vậy làm sao xử lý categorical data (text, categories)?
Assign mỗi category một số nguyên.
from sklearn.preprocessing import LabelEncoder
# Input
colors = ['Red', 'Blue', 'Green', 'Red', 'Blue']
# Encode
encoder = LabelEncoder()
encoded = encoder.fit_transform(colors)
# Output: [2, 0, 1, 2, 0] (Blue=0, Green=1, Red=2)
Ưu điểm: Compact, dễ implement
Nhược điểm:
Khi nào dùng:
# ✅ Ordinal data (có thứ tự)
priority = ['Low', 'Medium', 'High', 'Low']
# Low=0, Medium=1, High=2 → makes sense
# ❌ Nominal data (không thứ tự)
colors = ['Red', 'Blue', 'Green']
# Red=2 > Blue=0 → nonsense!
Tạo một binary column cho mỗi category.
import pandas as pd
# Input
df = pd.DataFrame({'Color': ['Red', 'Blue', 'Green', 'Red']})
# One-hot encode
df_encoded = pd.get_dummies(df, columns=['Color'])
# Output:
# Color_Blue Color_Green Color_Red
# 0 0 0 1
# 1 1 0 0
# 2 0 1 0
# 3 0 0 1
Ưu điểm:
Nhược điểm:
Khi nào dùng:
Replace category bằng mean của target variable cho category đó.
# Data
df = pd.DataFrame({
'City': ['HN', 'HCM', 'HN', 'DN', 'HCM', 'HN'],
'Purchased': [1, 1, 0, 0, 1, 1] # Target
})
# Target encoding
city_means = df.groupby('City')['Purchased'].mean()
# HN: 2/3 = 0.67
# HCM: 2/2 = 1.0
# DN: 0/1 = 0.0
df['City_encoded'] = df['City'].map(city_means)
# Output:
# City Purchased City_encoded
# HN 1 0.67
# HCM 1 1.0
# HN 0 0.67
# DN 0 0.0
# HCM 1 1.0
# HN 1 0.67
Ưu điểm:
Nhược điểm:
Best practice:
# ❌ WRONG - encoding cả train + test cùng lúc
df['City_encoded'] = df.groupby('City')['Target'].transform('mean')
# ✅ CORRECT - chỉ học từ train, apply lên test
train_means = train.groupby('City')['Target'].mean()
train['City_encoded'] = train['City'].map(train_means)
test['City_encoded'] = test['City'].map(train_means) # Dùng mean từ train
Khi nào dùng:
Encode based on tần suất xuất hiện của category.
# Data
cities = ['HN', 'HCM', 'HN', 'DN', 'HCM', 'HN', 'HN']
# Frequency count
freq = pd.Series(cities).value_counts()
# HN: 4, HCM: 2, DN: 1
# Encode
city_freq = pd.Series(cities).map(freq)
# [4, 2, 4, 1, 2, 4, 4]
Khi nào dùng:
Học một dense vector representation cho mỗi category (sẽ đi sâu trong phần Deep Learning).
# Thay vì:
# Red=[1,0,0], Blue=[0,1,0], Green=[0,0,1] (sparse, 3D)
# Embedding học:
# Red=[0.2, 0.8, -0.3]
# Blue=[0.7, -0.1, 0.5]
# Green=[-0.3, 0.4, 0.9]
# (dense, 3D, nhưng capture được similarity)
Ưu điểm:
Nhược điểm:
Imbalanced data: Một class chiếm đa số (~90-99%), class kia là thiểu số (~1-10%).
Ví dụ:
# Dataset: 990 class 0, 10 class 1
# Model "ngây thơ": Predict tất cả = 0
# Accuracy = 990/1000 = 99%!
# Nhưng hoàn toàn vô dụng - miss 100% fraud cases!
→ Model học được pattern của majority class, ignore minority class.
Randomly xóa bớt samples từ majority class.
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X, y)
# Before: 990 class 0, 10 class 1
# After: 10 class 0, 10 class 1
Ưu điểm: Cân bằng data, faster training
Nhược điểm: Mất nhiều data (potentially useful information)
Random Over-sampling: Duplicate minority samples.
from imblearn.over_sampling import RandomOverSampler
ros = RandomOverSampler(random_state=42)
X_resampled, y_resampled = ros.fit_resample(X, y)
# Before: 990 class 0, 10 class 1
# After: 990 class 0, 990 class 1 (duplicated)
Vấn đề: Duplicate → dễ overfit.
SMOTE (Synthetic Minority Over-sampling Technique): Tạo synthetic samples thay vì duplicate.
from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X, y)
SMOTE hoạt động:
1. Chọn một minority sample: A
2. Tìm k nearest neighbors (VD: k=5) cùng class
3. Chọn random 1 neighbor: B
4. Tạo synthetic sample giữa A và B:
new_sample = A + random(0,1) * (B - A)
Ưu điểm: Không mất data, không duplicate
Nhược điểm: Có thể tạo ra noisy samples nếu data overlap nhiều
Thay vì resample data, assign higher weight cho minority class.
from sklearn.linear_model import LogisticRegression
# Tự động tính class weights inversely proportional to frequency
model = LogisticRegression(class_weight='balanced')
# Hoặc manual
model = LogisticRegression(class_weight={0: 1, 1: 99})
# Mỗi sample của class 1 = 99 samples của class 0
Loss function sẽ penalize nhiều hơn khi misclassify minority class.
Ưu điểm: Không cần resample, giữ nguyên data
Nhược điểm: Không phải mọi algorithm đều support class weights
Nếu imbalance quá nghiêm trọng (99.9% vs 0.1%), treat như anomaly detection problem.
from sklearn.ensemble import IsolationForest
# Train chỉ trên majority class (normal behavior)
model = IsolationForest(contamination=0.001)
model.fit(X_normal) # Chỉ class 0
# Predict: -1 = anomaly (fraud), 1 = normal
predictions = model.predict(X_test)
Curse of Dimensionality: Khi số features tăng, data trở nên "sparse" (thưa thớt) trong không gian cao chiều.
Ví dụ với k-NN:
1D space (Age):
- 100 samples → khá dense
- Để tìm 5 nearest neighbors → dễ
100D space (100 features):
- Cùng 100 samples → cực kỳ sparse
- "Nearest" neighbors giờ rất xa!
- Distance trở nên meaningless
Hình dung:
1D: ••••••••••• (samples gần nhau)
2D: • • • (sparse hơn)
• •
•
3D: • (rất sparse)
•
•
Kết quả:
Chọn subset features quan trọng nhất.
Filter Methods:
from sklearn.feature_selection import SelectKBest, f_classif
# Chọn top 10 features based on ANOVA F-test
selector = SelectKBest(score_func=f_classif, k=10)
X_selected = selector.fit_transform(X, y)
Wrapper Methods:
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
# Recursive Feature Elimination
model = RandomForestClassifier()
rfe = RFE(model, n_features_to_select=10)
X_selected = rfe.fit_transform(X, y)
Embedded Methods:
# L1 Regularization (Lasso) → features không quan trọng → weight = 0
from sklearn.linear_model import Lasso
model = Lasso(alpha=0.1)
model.fit(X, y)
# Chỉ giữ features có coefficient != 0
Transform features sang new coordinate system sao cho:
from sklearn.decomposition import PCA
# Giảm từ 100 features → 10 principal components
pca = PCA(n_components=10)
X_reduced = pca.fit_transform(X)
# Xem mỗi PC giữ được bao nhiêu % variance
print(pca.explained_variance_ratio_)
# [0.4, 0.25, 0.15, 0.08, ...] # PC1 giữ 40% variance
Ưu điểm:
Nhược điểm:
✅ Understand your data trước khi preprocess:
EDA (Exploratory Data Analysis) là bước quan trọng nhất.
✅ Train/Test contamination:
Scaling, encoding, imputation đều phải fit trên train, apply lên test.
# ❌ WRONG
scaler.fit(all_data) # Leak test info vào train
# ✅ CORRECT
scaler.fit(train_data)
train_scaled = scaler.transform(train_data)
test_scaled = scaler.transform(test_data)
✅ Feature engineering > fancy algorithms:
Một feature tốt có thể quan trọng hơn chọn algorithm phức tạp.
✅ Document your preprocessing pipeline:
Khi deploy, phải apply CHÍNH XÁC cùng preprocessing như training.
✅ Validation set để tune preprocessing:
Thử nhiều strategies (imputation methods, encoding, scaling) → chọn cái best trên validation set.
✅ Domain knowledge:
Hiểu business context giúp engineer features hiệu quả hơn.
Next steps:
Trong bài tiếp theo, chúng ta sẽ khám phá Model Evaluation Metrics - cách đánh giá model ML không chỉ bằng accuracy mà còn nhiều metrics khác quan trọng hơn.
Bài viết thuộc series "From Zero to AI Engineer" - Module 4: ML Foundations