Danh sách bài viết

Bài 6: Train / Validation / Test Split — vì sao phải chia 3 phần

Chia dataset thành train / validation / test để đánh giá model trên data chưa từng thấy. Vai trò từng phần, tỉ lệ phổ biến (60/20/20, 70/15/15, 98/1/1 cho dataset lớn), train_test_split của sklearn, stratify cho classification, shuffle=False cho time series, GroupShuffleSplit chống group leak, và các pitfall data leak qua split.

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

Mục tiêu bài học

Sau bài học, bạn sẽ:

  • Hiểu vì sao phải chia dataset trước khi train — generalization và unbiased evaluation.
  • Phân biệt rõ vai trò 3 phần: train để fit, validation để tune, test để chấm điểm cuối.
  • Chọn tỉ lệ chia phù hợp với kích thước dataset (60/20/20 cho data nhỏ, 98/1/1 cho data rất lớn).
  • Dùng train_test_split của sklearn, hiểu test_size, random_state, shuffle, stratify.
  • Chia data thành 3 phần bằng cách gọi train_test_split hai lần.
  • Biết khi nào không shuffle (time series) và khi nào dùng GroupShuffleSplit (data theo group).
  • Tránh 3 pitfall data leak qua split: group leak, time leak, duplicate row.
2

Vì sao phải chia data

Mục tiêu cuối của một model ML không phải dự đoán đúng trên data đã thấy — mà dự đoán đúng trên data chưa từng thấy. Tính chất này gọi là generalization.

Nếu chấm điểm model bằng chính tập data dùng để train, kết quả gần như chắc chắn quá lạc quan: model có thể "nhớ" data huấn luyện và đạt accuracy rất cao mà không hề học được pattern. Hiện tượng này gọi là overfitting (sẽ học kỹ ở Bài 11).

Cách kiểm tra: giữ lại một phần data không đưa cho model thấy trong lúc train, dùng phần đó để đánh giá. Đó chính là test set. Sklearn chuẩn hoá quy trình này qua hàm train_test_split.

Trong thực tế, ta cần thêm một phần thứ ba — validation set — để tune model mà không động vào test set. Lý do tách 2 phần này sẽ được giải thích ở mục 3.

3

3 phần và vai trò

Một dataset chia thành 3 phần độc lập:

Train set (~60–80%)

  • Dùng để fit tham số của model (weights, coefficients, cây quyết định…).
  • Model được "nhìn thấy" cả X và y của phần này nhiều lần trong lúc train.
  • Phần lớn nhất vì nhiều data hơn thường giúp model học pattern tốt hơn.

Validation set (~10–20%) — còn gọi là dev set

  • Dùng để tune hyperparameter: chọn learning rate, regularization, số layer, độ sâu cây…
  • Dùng để so sánh giữa các model khác nhau (Logistic vs SVM vs Random Forest) và chọn ra model tốt nhất.
  • Dùng để early stopping: dừng training khi loss trên val bắt đầu tăng (signal overfit).
  • Model không fit trực tiếp trên phần này, nhưng nhiều quyết định về model dựa trên kết quả ở đây — nên val set vẫn ảnh hưởng đến model cuối.

Test set (~10–20%)

  • Dùng để đánh giá cuối cùng — báo cáo accuracy / F1 / RMSE của model đã chốt.
  • Quy tắc vàng: dùng 1 lần duy nhất, sau khi đã xong toàn bộ quá trình tune.
  • Không bao giờ quay lại sửa model dựa trên kết quả test.

Vì sao phải tách Val và Test?

Nếu chỉ có train + test, ta sẽ tune hyperparameter dựa trên kết quả test. Sau vài chục lần thử, model đã "thấy" test set gián tiếp — ta chọn cấu hình nào cho điểm cao trên test, tức là test đã trở thành một dạng training signal. Khi đó test không còn unbiased; estimate về generalization sẽ lạc quan giả tạo.

Tách val ra giải quyết vấn đề: tune hết mức trên val, giữ test "đóng băng" cho lần đánh giá cuối. Mọi quyết định liên quan đến model phải kết thúc trước khi đụng vào test set.

4

Tỉ lệ phổ biến

Không có tỉ lệ "đúng tuyệt đối". Lựa chọn phụ thuộc kích thước dataset và độ phức tạp của model:

  • Dataset nhỏ (< 10.000 sample): 60/20/20 hoặc 70/15/15. Cần phần val và test đủ lớn để metric ổn định, không bị nhiễu vì quá ít sample.
  • Dataset vừa (10.000 – 100.000): 80/10/10 thường đủ. Val và test mỗi phần vẫn có hàng nghìn sample.
  • Dataset lớn (> 100.000): có thể 98/1/1. 1% của 1 triệu vẫn là 10.000 sample — đủ thống kê.
  • Dataset rất lớn (LLM pretrain corpus, ImageNet, web crawl): test có khi chỉ 0.1% hoặc nhỏ hơn. Mục tiêu là giữ lại nhiều data nhất có thể cho training, vì variance của metric trên test 0.1% × 1 tỷ sample = 1 triệu sample vẫn rất thấp.

Quy tắc thực dụng: ước lượng số lượng sample tối thiểu cần cho val/test để metric ổn định. Với binary classification, vài nghìn sample là đủ để accuracy có sai số < 1%. Tự đó tính ngược ra tỉ lệ phù hợp.

Ví dụ:

  • Iris (150 sample) — 60/20/20 → 90 / 30 / 30. Val và test chỉ 30 sample mỗi phần, metric sẽ rất nhiễu. Bài học: với data quá nhỏ, nên dùng cross-validation (mục 9) thay vì holdout.
  • MNIST (60k train + 10k test có sẵn) — quy ước cộng đồng dùng nguyên 10k làm test, chia 50k/10k cho train/val. Tỉ lệ ~83/8/8.
  • ImageNet (1.28M train + 50k val + 100k test) — tỉ lệ ~89/3.5/7, val và test cố định trong benchmark.
5

train_test_split của sklearn

Hàm train_test_split nằm trong module sklearn.model_selection. Đây là cách chuẩn để chia data thành 2 phần (train + test) hoặc 3 phần (gọi 2 lần — xem mục 6).

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True)

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,        # 20% cho test
    random_state=42       # seed để tái lập
)

print(X_train.shape, X_test.shape)  # (120, 4) (30, 4)
print(y_train.shape, y_test.shape)  # (120,) (30,)

Tham số quan trọng

  • test_size — kích thước test set. Có thể là float (tỉ lệ 0–1, ví dụ 0.2 = 20%) hoặc int (số sample, ví dụ 30). Nếu không truyền, mặc định là 0.25.
  • train_size — kích thước train set. Tương tự, float hoặc int. Thường chỉ truyền một trong hai (test_size hoặc train_size); phần còn lại tự tính.
  • random_state — seed cho random shuffle. Truyền số nguyên (ví dụ 42) để mỗi lần chạy ra cùng một split. Bỏ qua → kết quả khác nhau giữa các lần chạy, khó debug.
  • shuffle — mặc định True. Đảo thứ tự sample trước khi chia. Đặt False cho time series (mục 8).
  • stratify — giữ tỉ lệ class. Truyền vector label vào (stratify=y). Bắt buộc cho classification, đặc biệt khi class imbalanced (mục 7).

Chấp nhận nhiều input cùng lúc

train_test_split có thể chia nhiều mảng cùng lúc, miễn là có cùng số dòng. Ví dụ chia thêm sample weight:

X_train, X_test, y_train, y_test, w_train, w_test = train_test_split(
    X, y, sample_weight, test_size=0.2, random_state=42
)

Tất cả các mảng được chia cùng index — sample thứ i trong X_train luôn tương ứng với label thứ i trong y_train.

6

Chia thành 3 phần — gọi hai lần

train_test_split chỉ chia được 2 phần một lần. Để có 3 phần (train + val + test), gọi 2 lần liên tiếp:

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

X, y = load_iris(return_X_y=True)

# Lần 1: tách 40% ra làm "temp" (sẽ chia tiếp thành val + test)
X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=0.4, random_state=42, stratify=y
)

# Lần 2: chia đôi temp thành val và test (mỗi phần 20%)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
)

print("Train:", X_train.shape)   # (90, 4)
print("Val  :", X_val.shape)     # (30, 4)
print("Test :", X_test.shape)    # (30, 4)

Logic tỉ lệ:

  • Lần 1: test_size=0.4 → 60% train, 40% temp.
  • Lần 2: chia temp 50/50 → 20% val, 20% test (trên tổng).
  • Kết quả: 60/20/20.

Để có tỉ lệ 70/15/15:

X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
)
# → 70% / 15% / 15%

Quy tắc: luôn dùng cùng random_state cho cả hai lần gọi nếu muốn split tái lập được. Nếu split phụ thuộc thời gian (lấy dữ liệu mới), nên lưu lại index của train/val/test thành file để mọi experiment dùng cùng một split.

7

stratify cho classification

stratify giữ tỉ lệ class trong train, val, test giống dataset gốc. Đây là tham số quan trọng nhất khi làm classification.

Vấn đề nếu không stratify

Giả sử dataset có 95% class 0, 5% class 1 (binary imbalanced). Nếu shuffle ngẫu nhiên và chia, test set có thể tình cờ chứa 99% class 0, 1% class 1 — hoặc thậm chí 100% class 0 nếu test nhỏ. Khi đó:

  • Phân phối class ở test khác train → đánh giá không phản ánh đúng performance.
  • Một số class hiếm có thể biến mất hoàn toàn khỏi test hoặc train.
  • Metric (đặc biệt F1, precision, recall) bị bóp méo nặng.

Cách dùng

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y          # giữ tỉ lệ class theo y
)

Khi nào nên dùng:

  • Luôn dùng cho classification, kể cả khi dataset cân bằng — chi phí gần bằng 0, lợi ích rõ ràng.
  • Bắt buộc khi class imbalanced (tỉ lệ ≥ 2:1).
  • Bắt buộc khi dataset nhỏ (< 1000 sample) — variance ngẫu nhiên cao.
  • Không dùng cho regression (không có class). Sklearn sẽ báo lỗi nếu stratify là biến liên tục.

Verify stratify hoạt động đúng:

import numpy as np

# Tỉ lệ class trong gốc, train, test
for name, arr in [("gốc", y), ("train", y_train), ("test", y_test)]:
    vals, counts = np.unique(arr, return_counts=True)
    pct = counts / counts.sum()
    print(f"{name:5s}: {dict(zip(vals, pct.round(3)))}")
# gốc  : {0: 0.333, 1: 0.333, 2: 0.333}
# train: {0: 0.333, 1: 0.333, 2: 0.333}
# test : {0: 0.333, 1: 0.333, 2: 0.333}
8

Time series — shuffle=False

Với time series (giá cổ phiếu, sales theo ngày, log server theo phút…), không được shuffle. Lý do: thứ tự thời gian mang thông tin, và mục tiêu là dự đoán tương lai dựa trên quá khứ.

Nếu shuffle, model có thể được train trên data ngày 15/3 rồi đánh giá trên ngày 10/3 — đó là dự đoán quá khứ, không phản ánh thực tế và còn gây time leak: thông tin tương lai (sau ngày 10/3) đã "rò rỉ" vào training. Trong production, model không bao giờ có sẵn thông tin tương lai.

Cách chia đúng

import pandas as pd
from sklearn.model_selection import train_test_split

# Giả sử df đã sort theo cột "date"
df = df.sort_values("date").reset_index(drop=True)
X = df.drop(columns=["target", "date"]).values
y = df["target"].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    shuffle=False        # giữ nguyên thứ tự
)
# Train = 80% đầu (quá khứ), Test = 20% cuối (tương lai)

Khi shuffle=False, không thể dùng stratify (sklearn báo lỗi). Đây là trade-off chấp nhận được vì time series ưu tiên thứ tự hơn cân bằng class.

TimeSeriesSplit — cross-validation cho time series

Với time series, K-Fold thông thường không phù hợp (sẽ trộn quá khứ-tương lai). Sklearn cung cấp TimeSeriesSplit tạo các fold tăng dần theo thời gian:

from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for fold, (train_idx, val_idx) in enumerate(tscv.split(X)):
    print(f"Fold {fold}: train={train_idx[-1]+1}, val={len(val_idx)}")
# Fold 0: train=25, val=25      → train trên 25 mẫu đầu, val trên 25 mẫu kế
# Fold 1: train=50, val=25      → train trên 50 mẫu đầu, val trên 25 mẫu kế
# ...

Mỗi fold, train luôn là quá khứ và val là tương lai liền kề — mô phỏng cách deploy thực tế. Chi tiết về CV sẽ học ở Bài 39.

9

Cross-Validation — alternative khi data nhỏ

Khi dataset nhỏ (vài trăm tới vài nghìn sample), holdout val set có 2 vấn đề: (1) val set quá nhỏ → metric nhiễu, (2) lãng phí 10–20% data có thể dùng để train.

Giải pháp: K-Fold Cross-Validation. Không tách val set riêng, mà:

  1. Tách test set ra (giữ nguyên, 1 lần).
  2. Chia phần còn lại (train) thành K fold (thường K=5 hoặc 10).
  3. Lặp K lần: mỗi lần dùng K-1 fold để train, 1 fold để đánh giá.
  4. Trung bình metric qua K lần → ước lượng ổn định hơn so với 1 val set duy nhất.

Workflow tổng:

from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LogisticRegression

# Tách test ra trước (giữ nguyên, không động đến)
X_trainval, X_test, y_trainval, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# CV trên trainval để tune
model = LogisticRegression(max_iter=200)
scores = cross_val_score(model, X_trainval, y_trainval, cv=5)
print("CV accuracy:", scores.mean(), "+/-", scores.std())

# Sau khi chốt cấu hình, train lại trên toàn trainval rồi đánh giá test
model.fit(X_trainval, y_trainval)
print("Test accuracy:", model.score(X_test, y_test))

Cross-validation chi tiết (K-Fold, Stratified K-Fold, Leave-One-Out, nested CV) sẽ học ở Bài 39. Bài này chỉ cần nhớ: khi data nhỏ, ưu tiên CV thay vì val set riêng.

10

Data leak qua split — pitfall

Chia data sai cách dẫn đến data leak — thông tin từ test rò rỉ vào train, khiến đánh giá lạc quan giả tạo. Ba dạng leak phổ biến qua split:

1. Group leak

Khi nhiều dòng thuộc cùng một đối tượng (cùng patient, cùng user, cùng device…), shuffle ngẫu nhiên có thể đẩy một số dòng của cùng đối tượng vào train và một số vào test. Model "nhớ" được đối tượng đó → predict trên test rất tốt, nhưng performance trên đối tượng mới (chưa thấy bao giờ) lại tệ.

Ví dụ: y tế — mỗi patient có 10 lần khám, mỗi lần là 1 dòng. Nếu 7 dòng vào train + 3 dòng vào test, model học được "đặc điểm patient này" và predict đúng trên 3 dòng test — nhưng đó không phải generalization.

Giải pháp: GroupShuffleSplit — split theo group, không theo dòng:

from sklearn.model_selection import GroupShuffleSplit

# patient_id: array shape (n_samples,), cùng giá trị → cùng patient
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(gss.split(X, y, groups=patient_id))

X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]

# Verify: không patient nào xuất hiện ở cả train và test
print("Train patients:", set(patient_id[train_idx]))
print("Test  patients:", set(patient_id[test_idx]))
print("Giao nhau     :", set(patient_id[train_idx]) & set(patient_id[test_idx]))
# Giao nhau: set()

2. Time leak

Đã nói ở mục 8: shuffle time series khiến model "thấy tương lai". Luôn shuffle=False với time series.

3. Duplicate row

Nếu dataset có dòng trùng lặp (cùng X, cùng y), shuffle có thể đẩy bản sao này vào train, bản kia vào test. Model "nhớ" và predict đúng → leak. Cách tránh: df.drop_duplicates() trước khi split.

Ngoài 3 dạng trên, còn các dạng leak qua preprocessing (fit scaler/encoder trên toàn bộ data trước khi split) — sẽ học ở Bài 7 (feature scaling) và Bài 10 (pipeline).

11

Imbalanced dataset và rare class

Dataset imbalanced (ví dụ fraud detection: 99% normal, 1% fraud) làm split phức tạp hơn.

stratify giúp giữ tỉ lệ — đó là bước bắt buộc đầu tiên. Nhưng còn vấn đề: nếu class hiếm có quá ít sample (ví dụ chỉ 50 fraud trong 5000 sample), test set 20% chỉ chứa 10 fraud — quá ít để metric ổn định.

Các kỹ thuật bổ sung (sẽ học ở Bài 42):

  • SMOTE — tạo synthetic minority samples bằng interpolation trong feature space. Chỉ áp dụng cho train, không bao giờ cho val/test (sẽ leak).
  • class_weight — weight loss của model nhiều hơn cho class hiếm. Tham số class_weight="balanced" có sẵn trong nhiều estimator của sklearn.
  • Sampling strategy — undersampling class đa số hoặc oversampling class hiếm.

Quy tắc quan trọng: mọi kỹ thuật xử lý imbalance áp dụng sau khi split, và chỉ trên train set. Val và test phải giữ phân phối thực tế để đánh giá phản ánh đúng production.

12

Quy ước đặt tên và shape check

Quy ước cộng đồng (sklearn, mọi tutorial, notebook Kaggle):

  • X_train, y_train — train set.
  • X_val, y_val — validation set. Tên thay thế: X_dev, y_dev (DeepLearning community).
  • X_test, y_test — test set.
  • X_temp, y_temp — phần trung gian khi chia 2 lần (sẽ chia tiếp thành val + test).

Sau mỗi lần split, luôn in shape để verify. Đây là thói quen rẻ và bắt được hàng loạt bug:

print(f"X_train: {X_train.shape}, y_train: {y_train.shape}")
print(f"X_val  : {X_val.shape}, y_val  : {y_val.shape}")
print(f"X_test : {X_test.shape}, y_test : {y_test.shape}")

# Sanity check: tổng phải bằng dataset gốc
assert X_train.shape[0] + X_val.shape[0] + X_test.shape[0] == X.shape[0]
# Sanity check: số cột phải nhất quán
assert X_train.shape[1] == X_val.shape[1] == X_test.shape[1] == X.shape[1]

Hai assert trên bắt được 90% lỗi split: thiếu phần tử (do test_size sai), nhầm transpose (đảo dòng-cột), hoặc đưa nhầm biến vào split.

13

Code đầy đủ — split iris 3 phần

Ghép mọi thứ lại: split iris thành 60/20/20 với stratify, verify class balance, in shape, và demo GroupShuffleSplit ngắn.

import numpy as np
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split, GroupShuffleSplit

# Load
X, y = load_iris(return_X_y=True)
print("Dataset gốc:", X.shape, y.shape)   # (150, 4) (150,)

# Split 60/20/20 với stratify
X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=0.4, random_state=42, stratify=y
)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
)

# In shape
print(f"Train: X{X_train.shape}, y{y_train.shape}")  # (90, 4) (90,)
print(f"Val  : X{X_val.shape}, y{y_val.shape}")      # (30, 4) (30,)
print(f"Test : X{X_test.shape}, y{y_test.shape}")    # (30, 4) (30,)

# Sanity check
assert X_train.shape[0] + X_val.shape[0] + X_test.shape[0] == X.shape[0]

# Verify class balance
def class_dist(name, arr):
    vals, counts = np.unique(arr, return_counts=True)
    pct = (counts / counts.sum()).round(3)
    print(f"{name:5s}: {dict(zip(vals.tolist(), pct.tolist()))}")

class_dist("gốc",   y)
class_dist("train", y_train)
class_dist("val",   y_val)
class_dist("test",  y_test)
# gốc  : {0: 0.333, 1: 0.333, 2: 0.333}
# train: {0: 0.333, 1: 0.333, 2: 0.333}
# val  : {0: 0.333, 1: 0.333, 2: 0.333}
# test : {0: 0.333, 1: 0.333, 2: 0.333}

Demo GroupShuffleSplit trên iris (giả sử mỗi 10 hoa là cùng "vườn"):

# Tạo group: 150 hoa, mỗi 10 hoa là 1 vườn → 15 vườn
groups = np.repeat(np.arange(15), 10)

gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(gss.split(X, y, groups=groups))

train_gardens = set(groups[train_idx])
test_gardens = set(groups[test_idx])
print(f"Vườn train: {sorted(train_gardens)}")
print(f"Vườn test : {sorted(test_gardens)}")
print(f"Giao nhau : {train_gardens & test_gardens}")
# Giao nhau : set()    → không vườn nào xuất hiện ở cả train và test

Điều cần để ý: với GroupShuffleSplit, test_size=0.2tỉ lệ theo group, không phải theo sample. 3 vườn × 10 hoa = 30 sample test (giống split sample, nhưng đảm bảo không leak).

14

Bài tập

  1. Chia iris thành 70/15/15 train/val/test với stratify=y. In shape của 3 phần. Verify tổng số sample = 150.
  2. Verify class balance ở 3 phần (gợi ý: np.unique(y_part, return_counts=True) rồi tính phần trăm). Kết quả phải xấp xỉ 33.3% cho mỗi class.
  3. Tạo một time series tổng hợp: 100 dòng, mỗi dòng có cột day (0–99), X ngẫu nhiên, y = 2*X + noise. Chia 80/20 với shuffle=False. Kiểm tra train chứa day 0–79, test chứa 80–99.
  4. Tạo dataset 200 sample với 20 group (mỗi group 10 sample). Dùng GroupShuffleSplit chia với test_size=0.2. Verify không group nào xuất hiện ở cả train và test.
  5. So sánh: chia iris bằng train_test_split(X, y, test_size=0.2) (không stratify, không seed) — chạy 5 lần, in tỉ lệ class trong test mỗi lần. Có ổn định không?
Đáp án ngắn
  1. train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) rồi train_test_split(X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp). Shape: (105, 4) / (22 hoặc 23, 4) / (23 hoặc 22, 4). Tổng = 150.
  2. Cả 3 phần đều ~33.3% cho mỗi class (sai số ≤ 4% do làm tròn).
  3. Train: day 0–79 (80 dòng), Test: day 80–99 (20 dòng). Không có giao nhau.
  4. GroupShuffleSplit chọn 4 group cho test (20% × 20), 16 group cho train. Tổng sample test = 40, train = 160. set(groups_train) & set(groups_test) == set().
  5. Không stratify, không seed: tỉ lệ class trong test dao động ±10% giữa các lần chạy. Stratify + seed → ổn định hoàn toàn.
15

Tóm tắt

  • Chia data để đánh giá model trên sample chưa từng thấy — kiểm chứng generalization, tránh bị lừa bởi overfit.
  • 3 phần: train (fit tham số) — val (tune hyperparameter, chọn model, early stopping) — test (đánh giá cuối, dùng 1 lần).
  • Tách val và test vì: nếu tune trên test → test bị "leak" gián tiếp, không còn unbiased.
  • Tỉ lệ: data nhỏ 60/20/20 hoặc 70/15/15; data lớn (> 100k) có thể 98/1/1; data rất lớn có khi test < 0.1%.
  • train_test_split trong sklearn.model_selection: tham số test_size, random_state, shuffle, stratify.
  • Chia 3 phần: gọi train_test_split 2 lần — lần 1 tách train ra, lần 2 chia phần còn lại thành val + test.
  • stratify=y giữ tỉ lệ class — luôn dùng cho classification, bắt buộc khi imbalanced hoặc data nhỏ.
  • Time series: shuffle=False — train là quá khứ, test là tương lai. Không dùng được stratify. CV thì dùng TimeSeriesSplit.
  • Data nhỏ: ưu tiên K-Fold CV thay vì val set riêng — bài 39 sẽ học sâu.
  • 3 pitfall data leak qua split: group leak (dùng GroupShuffleSplit), time leak (shuffle=False), duplicate row (drop_duplicates trước).
  • Imbalanced: stratify trước, rồi SMOTE hoặc class_weight chỉ áp dụng trên train.
  • Quy ước biến: X_train, X_val, X_test, y_train, y_val, y_test. Luôn in shape sau split để verify.
  • Bài 7 sẽ học feature scaling (Min-Max Normalization) — bước preprocessing chuẩn sau split.