Data Leakage: Mô hình vô tình nhìn thấy thông tin tương lai bằng những con đường nào?
Data Leakage
Data Leakage: Mô hình vô tình nhìn thấy thông tin tương lai bằng những con đường nào?
Trong nghiên cứu Quant và Machine Learning, một mô hình đạt độ chính xác rất cao chưa chắc đã thực sự tìm được quy luật của thị trường. Đôi khi kết quả đẹp bất thường xuất hiện vì dữ liệu đầu vào đã vô tình tiết lộ đáp án. Mô hình giống như một thí sinh được xem trước một phần đề thi: kết quả trên dữ liệu lịch sử rất tốt, nhưng khi đưa vào vận hành thực tế lại suy giảm nhanh chóng. Hiện tượng này được gọi là Data Leakage, hay rò rỉ dữ liệu.
Data Leakage đặc biệt nguy hiểm trong tài chính vì dữ liệu luôn gắn với thời gian. Giá ngày mai không thể được sử dụng để ra quyết định hôm nay; báo cáo kết thúc ngày 31/3 chưa chắc đã được thị trường biết vào ngày 1/4; số liệu GDP hiện được tải về có thể đã được điều chỉnh nhiều lần so với bản mà nhà đầu tư từng nhìn thấy. Chỉ cần một trong những thông tin này đi ngược về quá khứ, backtest có thể trở nên đẹp hơn rất nhiều so với khả năng thực tế.

Phần 1: Data Leakage làm kết quả mô hình đẹp giả như thế nào?
Giả sử chúng ta muốn xây dựng mô hình vào cuối mỗi phiên để dự báo cổ phiếu có tăng trong năm phiên tiếp theo hay không. Tại ngày 10/6, mô hình chỉ được sử dụng thông tin đã tồn tại đến hết ngày 10/6. Giá ngày 11/6, 12/6 hoặc kết quả kinh doanh công bố ngày 20/6 không được phép xuất hiện trong dữ liệu đầu vào.
Tuy nhiên, trong quá trình tạo biến, người nghiên cứu vô tình tính lợi suất năm phiên tương lai rồi đưa chính biến đó vào mô hình. Dữ liệu có thể trông như sau:

Mô hình chỉ cần nhìn vào cột “Lợi suất 5 phiên tới” là biết chính xác nhãn “Tăng” hay “Giảm”. Độ chính xác có thể đạt gần 100%, nhưng đây không phải khả năng dự báo. Mô hình đang được cung cấp đáp án.
Data Leakage cũng có thể xuất hiện kín đáo hơn trong bước chuẩn hóa. Giả sử tập huấn luyện có năm giá trị:
10, 12, 14, 16, 18Giá trị trung bình của tập huấn luyện là:
14Tập kiểm tra có một giá trị rất lớn:
100Nếu chuẩn hóa bằng toàn bộ sáu giá trị trước khi chia dữ liệu, trung bình được sử dụng sẽ là:
(10 + 12 + 14 + 16 + 18 + 100) / 6 = 28,33Như vậy, dữ liệu kiểm tra đã tác động đến cách chuẩn hóa dữ liệu huấn luyện. Mô hình chưa trực tiếp nhìn thấy nhãn tương lai, nhưng đã biết một phần đặc điểm phân phối của tập kiểm tra. Đây vẫn là Data Leakage.
Một ví dụ quan trọng khác nằm ở báo cáo tài chính. Giả sử báo cáo quý I có các mốc thời gian:
Ngày kết thúc kỳ báo cáo: 31/03/2026
Ngày doanh nghiệp công bố: 28/04/2026 lúc 18:15
Ngày giao dịch đầu tiên được phép sử dụng: 29/04/2026
Lợi nhuận sau thuế: 2.000 tỷ đồngNếu hệ thống gắn con số lợi nhuận 2.000 tỷ đồng vào ngày 31/3 và cho phép mô hình sử dụng từ ngày 1/4, mô hình đã được nhìn thấy báo cáo gần một tháng trước thị trường. Khi backtest factor ROE, tăng trưởng lợi nhuận hoặc định giá, sai lệch này có thể làm chiến lược trông hiệu quả hơn rất nhiều.
Cách lưu đúng phải phân biệt rõ:
period_end = 2026-03-31
announced_at = 2026-04-28 18:15
available_at = 2026-04-29Mô hình chỉ được sử dụng báo cáo từ ngày available_at, không phải từ ngày period_end.
Phần 2: Ví dụ Python cho thấy mô hình bị Leakage và mô hình được kiểm tra đúng
Đoạn code dưới đây tạo một chuỗi giá giả lập gồm 1.200 phiên. Đây là dữ liệu minh họa, không phải dữ liệu thị trường thực tế. Mục tiêu là dự báo giá sau năm phiên cao hơn giá hiện tại hay không.
Ví dụ sai: Đưa lợi suất tương lai vào feature
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
# Giữ kết quả có thể tái lập
np.random.seed(42)
# Tạo dữ liệu giá giả lập
n = 1200
daily_return = np.random.normal(
loc=0.0003,
scale=0.012,
size=n
)
price = 100 * np.cumprod(1 + daily_return)
df = pd.DataFrame({
"price": price
})
# Target: giá sau 5 phiên có cao hơn giá hiện tại không?
df["future_return_5d"] = (
df["price"].shift(-5) / df["price"] - 1
)
df["target"] = (
df["future_return_5d"] > 0
).astype(int)
# LỖI NGHIÊM TRỌNG:
# future_return_5d được tạo từ giá tương lai
# nhưng lại bị đưa vào feature
data_bad = df.dropna().copy()
X_bad = data_bad[["future_return_5d"]]
y_bad = data_bad["target"]
X_train, X_test, y_train, y_test = train_test_split(
X_bad,
y_bad,
test_size=0.30,
shuffle=True,
random_state=42
)
model_bad = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression())
])
model_bad.fit(X_train, y_train)
prediction_bad = model_bad.predict(X_test)
accuracy_bad = accuracy_score(
y_test,
prediction_bad
)
print("Accuracy của mô hình bị Leakage:")
print(round(accuracy_bad, 4))Kết quả với dữ liệu và seed trên:
Accuracy của mô hình bị Leakage:
1.0Mô hình đạt độ chính xác 100% không phải vì Logistic Regression phát hiện được bí mật thị trường. Feature future_return_5d đã chứa trực tiếp đáp án. Nếu lợi suất tương lai lớn hơn 0 thì target bằng 1; nếu lợi suất tương lai nhỏ hơn hoặc bằng 0 thì target bằng 0.
Ví dụ đúng hơn: Chỉ dùng dữ liệu đã tồn tại trước thời điểm dự báo
Trong phiên bản tiếp theo, chúng ta chỉ tạo feature từ giá và lợi suất quá khứ. Các feature đều được dịch lùi một phiên để đảm bảo giá trị tại ngày hiện tại không vô tình sử dụng dữ liệu chưa hoàn tất.
import numpy as np
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
np.random.seed(42)
n = 1200
daily_return = np.random.normal(
loc=0.0003,
scale=0.012,
size=n
)
price = 100 * np.cumprod(1 + daily_return)
df = pd.DataFrame({
"price": price
})
# Feature 1:
# Lợi suất một phiên đã hoàn tất
df["return_1d"] = (
df["price"].pct_change().shift(1)
)
# Feature 2:
# Lợi suất năm phiên trong quá khứ
df["return_5d"] = (
df["price"].pct_change(5).shift(1)
)
# Feature 3:
# Khoảng cách giữa giá và MA20 trong quá khứ
df["ma_gap_20"] = (
df["price"].shift(1)
/ df["price"].rolling(20).mean().shift(1)
- 1
)
# Feature 4:
# Biến động 20 phiên trong quá khứ
df["volatility_20"] = (
df["price"]
.pct_change()
.rolling(20)
.std()
.shift(1)
)
# Target:
# Giá sau 5 phiên có cao hơn giá hiện tại không?
df["target"] = (
df["price"].shift(-5) / df["price"] - 1 > 0
).astype(int)
data_good = df.dropna().copy()
features = [
"return_1d",
"return_5d",
"ma_gap_20",
"volatility_20"
]
X = data_good[features]
y = data_good["target"]
# Giữ đúng thứ tự thời gian
time_split = TimeSeriesSplit(
n_splits=5
)
model_good = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression())
])
scores = cross_val_score(
model_good,
X,
y,
cv=time_split,
scoring="accuracy"
)
print("Accuracy của từng giai đoạn:")
print(np.round(scores, 4))
print("Accuracy trung bình:")
print(round(scores.mean(), 4))Với dữ liệu giả lập trên, kết quả nhận được là:
Accuracy của từng giai đoạn:
[0.6071 0.4949 0.4439 0.5561 0.5255]
Accuracy trung bình:
0.5255Độ chính xác chỉ khoảng 52,55%, thấp hơn rất nhiều so với mức 100% của mô hình bị Leakage. Tuy nhiên, kết quả thấp hơn này lại đáng tin hơn vì mô hình chỉ được sử dụng dữ liệu quá khứ. Dữ liệu giá được tạo ngẫu nhiên nên việc mô hình chỉ đạt gần 50% cũng là điều hợp lý.
Sự khác biệt giữa hai kết quả rất rõ:
Mô hình bị Leakage: Accuracy = 100%
Mô hình không dùng giá tương lai: Accuracy khoảng 52,55%Đây là lý do một kết quả quá đẹp phải được kiểm tra trước khi được tin tưởng.
Ví dụ ghép báo cáo tài chính sai ngày
Đoạn code dưới đây minh họa báo cáo quý I kết thúc ngày 31/3 nhưng chỉ được sử dụng từ ngày 29/4.
import pandas as pd
# Dữ liệu giá giao dịch
prices = pd.DataFrame({
"trading_date": pd.date_range(
start="2026-04-01",
end="2026-05-05",
freq="B"
),
"ticker": "ABC"
})
# Báo cáo tài chính
financial_report = pd.DataFrame({
"ticker": ["ABC"],
"period_end": [
pd.Timestamp("2026-03-31")
],
"announced_at": [
pd.Timestamp("2026-04-28 18:15")
],
"available_at": [
pd.Timestamp("2026-04-29")
],
"net_income": [2000]
})
prices = prices.sort_values(
"trading_date"
)
financial_report = financial_report.sort_values(
"available_at"
)
# CÁCH ĐÚNG:
# Chỉ ghép báo cáo từ ngày available_at trở đi
merged_correct = pd.merge_asof(
prices,
financial_report[
[
"ticker",
"available_at",
"net_income"
]
],
left_on="trading_date",
right_on="available_at",
by="ticker",
direction="backward"
)
print(
merged_correct[
[
"trading_date",
"available_at",
"net_income"
]
].tail(10)
)Trong kết quả đúng, các phiên trước ngày 29/4 sẽ chưa có net_income:
Trước 29/04/2026: net_income = NaN
Từ 29/04/2026: net_income = 2000Nếu ghép theo period_end, con số lợi nhuận có thể xuất hiện từ đầu tháng 4, dù lúc đó báo cáo chưa được công bố.
Ví dụ chuẩn hóa đúng bằng Pipeline
Cách sai thường gặp là chuẩn hóa toàn bộ dữ liệu trước rồi mới chia train và test:
# KHÔNG NÊN LÀM
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Sau đó mới chia dữ liệuCách đúng là đưa bộ chuẩn hóa và mô hình vào cùng một Pipeline. Khi cross-validation chạy, bộ chuẩn hóa chỉ được học từ phần train của từng giai đoạn.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
model = Pipeline([
(
"scaler",
StandardScaler()
),
(
"classifier",
LogisticRegression()
)
])Pipeline không tự động giải quyết mọi dạng Data Leakage, nhưng giúp tránh việc tập test tham gia vào quá trình tính trung bình, độ lệch chuẩn và các tham số tiền xử lý.
Phần 3: Kiểm tra Data Leakage bằng số liệu và các phép thử thực tế
Sau khi mô hình được xây dựng, không nên chỉ nhìn Accuracy hoặc đường Equity Curve. Cần so sánh kết quả giữa nhiều phiên bản dữ liệu và thực hiện các phép thử cố ý phá tín hiệu. Nếu mô hình vẫn cho kết quả rất cao sau khi target bị xáo trộn, khả năng lớn dữ liệu đang chứa đáp án.
Đoạn code dưới đây xáo trộn target. Khi nhãn không còn liên hệ với feature, Accuracy hợp lý phải quay về gần 50% đối với bài toán hai lớp tương đối cân bằng.
import numpy as np
from sklearn.model_selection import TimeSeriesSplit
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
np.random.seed(42)
# Xáo trộn target
y_shuffled = np.random.permutation(y)
model = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression())
])
time_split = TimeSeriesSplit(
n_splits=5
)
shuffled_scores = cross_val_score(
model,
X,
y_shuffled,
cv=time_split,
scoring="accuracy"
)
print("Accuracy khi target bị xáo trộn:")
print(np.round(shuffled_scores, 4))
print("Accuracy trung bình:")
print(round(shuffled_scores.mean(), 4))Nếu mô hình vẫn đạt Accuracy 80–90% sau khi target đã bị xáo trộn, cần kiểm tra ngay xem feature có chứa target, mã định danh kết quả hoặc dữ liệu tương lai hay không.
Một phép kiểm tra khác là trì hoãn toàn bộ feature thêm một phiên:
X_delayed = X.shift(1).dropna()
y_delayed = y.loc[X_delayed.index]
delayed_scores = cross_val_score(
model,
X_delayed,
y_delayed,
cv=time_split,
scoring="accuracy"
)
print("Accuracy sau khi trì hoãn feature:")
print(np.round(delayed_scores, 4))
print("Accuracy trung bình:")
print(round(delayed_scores.mean(), 4))Nếu mô hình ban đầu có Accuracy 75% nhưng chỉ cần trì hoãn feature một phiên đã giảm về 50%, tín hiệu có thể phụ thuộc vào thông tin chưa thực sự có tại thời điểm đặt lệnh. Điều này thường xảy ra khi sử dụng giá đóng cửa để giả định mua ngay chính mức giá đóng cửa đó. Trong thực tế, chỉ sau khi phiên đóng cửa nhà đầu tư mới biết chính xác giá đóng cửa, vì vậy lệnh thường phải được thực hiện từ phiên tiếp theo.
Đối với một chiến lược giao dịch, có thể tạo bảng so sánh như sau:
Phiên bản kiểm traAccuracySharpeMaximum DrawdownDữ liệu ban đầu78%3,2-8%Feature lùi một phiên54%0,7-21%Dùng ngày công bố báo cáo52%0,4-25%Thêm phí giao dịch51%0,2-28%Target xáo trộn49%0,0-31%
Các số trong bảng trên chỉ là ví dụ minh họa. Tuy nhiên, nếu hiệu quả giảm mạnh sau khi sửa timestamp, lùi tín hiệu hoặc thêm độ trễ thực tế, kết quả ban đầu có thể đã được tạo ra bởi Leakage chứ không phải lợi thế đầu tư.
Một quy trình kiểm tra nên bao gồm:
1. Kiểm tra train có hoàn toàn nằm trước test hay không.
2. Kiểm tra preprocessing có được fit riêng trên train không.
3. Kiểm tra feature có sử dụng shift âm hoặc dữ liệu tương lai không.
4. Kiểm tra rolling có dùng center=True không.
5. Kiểm tra báo cáo tài chính theo available_at.
6. Kiểm tra dữ liệu vĩ mô theo ngày công bố và phiên bản.
7. Kiểm tra danh sách cổ phiếu đúng tại từng thời điểm.
8. Kiểm tra train và test có bản ghi trùng lặp không.
9. Xáo trộn target và chạy lại mô hình.
10. Lùi toàn bộ feature thêm một phiên để kiểm tra độ nhạy.
11. Thêm phí giao dịch, trượt giá và độ trễ khớp lệnh.
12. Giữ một tập test cuối cùng chưa từng được sử dụng.Kết luận
Data Leakage không chỉ xảy ra khi giá ngày mai được đưa thẳng vào dữ liệu hôm nay. Thông tin tương lai có thể đi vào mô hình qua việc chuẩn hóa toàn bộ dữ liệu, chia mẫu ngẫu nhiên, ghép báo cáo theo ngày cuối quý, sử dụng số liệu vĩ mô đã điều chỉnh, chọn danh sách cổ phiếu hiện tại hoặc liên tục sửa mô hình sau khi nhìn kết quả của tập test.
Ví dụ Python cho thấy một feature chứa lợi suất tương lai có thể giúp mô hình đạt Accuracy 100%. Khi loại bỏ thông tin tương lai và chỉ sử dụng các biến quá khứ, Accuracy giảm về khoảng 52,55%. Kết quả thấp hơn nhưng đáng tin hơn vì nó phản ánh đúng lượng thông tin mà mô hình có thể sử dụng trong thực tế.
Một mô hình Quant không nên được đánh giá bằng câu hỏi “lợi nhuận có cao không?” trước tiên. Câu hỏi đầu tiên phải là: “Tại thời điểm đưa ra mỗi quyết định trong backtest, mô hình có thực sự được phép biết những thông tin này hay không?”
