Data Engineering for ML: "Garbage In, Garbage Out"

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.

Missing Data: Khi Dữ liệu "Khuyết tật"

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ì:

  • User không điền hết form
  • Sensor bị lỗi không ghi được data
  • Data entry sai sót
  • Privacy concerns (user không muốn chia sẻ)

Ví dụ dataset:

Name      Age   Income   Credit_Score
Alice     28    50000    720
Bob       ?     65000    ?
Charlie   35    ?        680
Diana     42    80000    750

Phương pháp xử lý Missing Data

1. Deletion (Xóa)

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:

  • Mất nhiều data (nếu 10 columns, 1 column missing 50% → mất 50% rows)
  • Có thể tạo bias nếu missing không random

Khi nào dùng:

  • Missing data rất ít (<5% rows)
  • Missing hoàn toàn random
  • Còn đủ data sau khi xóa

2. Mean/Median/Mode Imputation

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:

  • Giảm variance của data
  • Không capture được relationship giữa features
  • Mean sensitive với outliers

Ví dụ vấn đề:

Ages: [25, 28, 30, 32, 95, ?]
Mean = 42 (bị outlier 95 làm lệch)
Median = 30 (robust hơn)

3. Forward Fill / Backward Fill

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)

4. Model-Based Imputation

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

5. Indicator Variables (Thêm cột đánh dấu)

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

  • Legitimate users thường điền đầy đủ thông tin
  • Fraudsters thường skip nhiều fields → Missing_count có thể là một feature mạnh!

Khi nào không nên Impute?

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]

Feature Scaling: Đưa Features về "Chung Mẫu số"

Tại sao cần Scaling?

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.

Normalization (Min-Max Scaling)

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:

  • Bounded range [0,1] - tốt cho neural networks
  • Giữ được shape của distribution

Nhược điểm:

  • Rất sensitive với outliers
  • Nếu test data có giá trị ngoài [min, max] của train data → có thể ra giá trị <0 hoặc >1

Khi nào dùng:

  • Khi biết chắc min/max boundaries (VD: Age 0-120, Percentage 0-100)
  • Neural networks với activation functions như sigmoid/tanh
  • Khi data không có outliers nhiều

Standardization (Z-score Normalization)

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:

  • Robust với outliers (so với normalization)
  • Không bị bounded trong range cố định
  • Phù hợp khi data có Gaussian distribution

Nhược điểm:

  • Output không bounded → có thể có giá trị âm/dương bất kỳ
  • Khó interpret (giá trị -1.5 có nghĩa gì?)

Khi nào dùng:

  • Algorithms assume Gaussian distribution (Linear Regression, Logistic Regression)
  • Khi có outliers trong data
  • SVM, PCA (Principal Component Analysis)

So sánh Normalization vs Standardization

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:

  • Data có outliers nhiều → Standardization
  • Data không có outliers, cần bounded range [0,1] → Normalization
  • Neural networks → thử cả hai, xem cái nào better
  • Tree-based models (Decision Tree, Random Forest) → KHÔNG CẦN scaling!

Feature Encoding: Chuyển Text thành Numbers

ML models chỉ hiểu numbers. Vậy làm sao xử lý categorical data (text, categories)?

1. Label Encoding

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:

  • Tạo ra ordinal relationship không tồn tại: Red=2 > Blue=0 → model nghĩ Red "lớn hơn" Blue
  • Chỉ phù hợp với ordinal data (có thứ tự): Low < Medium < High

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!

2. One-Hot Encoding

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:

  • Không tạo ordinal relationship sai
  • Phù hợp với hầu hết ML algorithms

Nhược điểm:

  • High dimensionality: Nếu có 1000 categories → 1000 columns mới!
  • Sparse matrix: Hầu hết giá trị là 0
  • Curse of dimensionality: Quá nhiều features làm model performance giảm

Khi nào dùng:

  • Categorical features với ít categories (<50)
  • Tree-based models (handle sparse data tốt)
  • Linear models

3. Target Encoding (Mean Encoding)

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:

  • Chỉ tạo 1 column (không như one-hot)
  • Capture được relationship với target
  • Hiệu quả với high-cardinality features (nhiều unique values)

Nhược điểm:

  • Data leakage risk: Dùng target để tạo feature → có thể overfit
  • Cần cẩn thận với train/test split

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:

  • High-cardinality categorical (nhiều unique values)
  • Tree-based models (Gradient Boosting, XGBoost)
  • Cần reduce dimensionality

4. Frequency Encoding

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:

  • Khi tần suất xuất hiện của category có ý nghĩa với target
  • Ví dụ: Trong fraud detection, rare payment methods có thể suspicious hơn

5. Embedding (cho Deep Learning)

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:

  • Capture semantic similarity (similar categories có vectors gần nhau)
  • Compact representation
  • State-of-the-art cho NLP, recommendation systems

Nhược điểm:

  • Cần nhiều data để train tốt
  • Chỉ dùng được với deep learning models

Imbalanced Data: Khi "Thiểu số" là Quan trọng

Imbalanced data: Một class chiếm đa số (~90-99%), class kia là thiểu số (~1-10%).

Ví dụ:

  • Fraud detection: 99.5% legitimate, 0.5% fraud
  • Disease diagnosis: 95% healthy, 5% sick
  • Churn prediction: 85% stay, 15% churn

Vấn đề với Imbalanced Data

# 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.

Giải pháp 1: Resampling

Under-sampling (Giảm majority 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)

Over-sampling (Tăng minority class)

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

Giải pháp 2: Class Weights

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

Giải pháp 3: Anomaly Detection

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: "Nhiều Quá cũng Hóa Độc"

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ấn đề

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ả:

  • k-NN, SVM performance giảm mạnh
  • Cần exponentially nhiều data hơn khi tăng features
  • Overfitting risk tăng

Giải pháp: Dimensionality Reduction

1. Feature Selection

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

2. Principal Component Analysis (PCA)

Transform features sang new coordinate system sao cho:

  • PC1 (Principal Component 1) capture maximum variance
  • PC2 capture variance còn lại (orthogonal với PC1)
  • ...
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:

  • Giảm dimensionality hiệu quả
  • Remove multicollinearity (correlation giữa features)
  • Denoise data

Nhược điểm:

  • Lost interpretability: Principal components không có ý nghĩa thực tế rõ ràng
  • Linear transformation only (không capture non-linear patterns)

Best Practices cho Data Engineering

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.

Key Takeaways

  • 80% công sức ML là data preparation - invest time vào bước này
  • Missing data: Chọn imputation method phù hợp với data distribution và missing pattern
  • Feature Scaling: Normalization cho bounded data, Standardization cho data có outliers
  • Encoding: One-hot cho low cardinality, Target/Frequency encoding cho high cardinality
  • Imbalanced data: SMOTE, class weights, hoặc treat như anomaly detection
  • Curse of dimensionality: Feature selection hoặc PCA để giảm dimensions
  • ALWAYS split train/test before preprocessing để tránh data leakage

Next steps:

  • Thực hành preprocessing pipeline với scikit-learn Pipeline
  • Hiểu sâu về feature engineering cho domain cụ thể (finance, healthcare, e-commerce)
  • Học advanced techniques: Autoencoders cho dimensionality reduction, Feature crosses

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