Chuỗi thời gian 6 — Đánh giá & backtesting
Chuỗi thời gian 6 — Đánh giá & backtesting
Suốt các bài trước, thông điệp lặp đi lặp lại là "luôn so với baseline", "kiểm tra coverage", "so trên cùng tập kiểm định". Bài này trả nợ tất cả những lời hứa đó. Đánh giá dự báo (forecast evaluation) không phải bước phụ ở cuối — nó là kim chỉ nam cho mọi quyết định mô hình: chọn ARIMA hay Prophet, có nên leo lên deep learning không, tuần này có nên đẩy mô hình mới lên production không. Nếu đo sai, bạn sẽ tự tin đi sai hướng.
Chuỗi thời gian có một cái bẫy chết người mà dữ liệu thường (i.i.d.) không có: thứ tự thời gian là thiêng liêng. Đánh giá phân loại/hồi quy thông thường (ML 5 — Đánh giá & chỉ số) cho phép shuffle rồi chia ngẫu nhiên, k-fold thoải mái. Với chuỗi thời gian, làm vậy là gian lận với chính mình — bạn để mô hình "nhìn trộm tương lai". Bài này đi qua hai trụ cột: (1) đo cái gì — các độ đo sai số và cách chọn; (2) đo như thế nào — tách dữ liệu và backtesting đúng cách.
1. Các độ đo sai số
Gọi giá trị thực là y_t, dự báo là ŷ_t, sai số e_t = y_t − ŷ_t (dương nghĩa là dự báo thiếu — under-forecast). Dưới đây là các độ đo cốt lõi.
1.1. MAE và RMSE — cùng đơn vị với dữ liệu
- MAE (Mean Absolute Error): trung bình
|e_t|. Cùng đơn vị vớiy(VND, số hồ sơ...), dễ giải thích thẳng cho nghiệp vụ: "sai trung bình 3.2 tỷ mỗi ngày". Không phạt nặng ngoại lệ. - MSE (Mean Squared Error) và căn của nó RMSE (Root Mean Squared Error): trung bình
e_t²(rồi lấy căn). RMSE cùng đơn vị vớiynhưng phạt nặng sai số lớn vì bình phương — một ngày dự báo lệch gấp đôi bị phạt gấp bốn.
Chọn giữa hai cái này là quyết định kinh doanh: nếu một cú sai lớn (ATM hết tiền một ngày) tệ hơn nhiều cú sai nhỏ → dùng RMSE (nó "ghét" sai số lớn). Nếu mọi sai số quan trọng như nhau theo độ lớn → MAE. Đừng báo cáo RMSE rồi tối ưu MAE, hay ngược lại — tối ưu và đo phải khớp mục tiêu.
Một hệ quả ít người để ý: dự báo tối ưu theo MAE là trung vị (median) của phân phối tương lai, còn theo MSE là trung bình (mean). Với chuỗi lệch (skewed) như số tiền giao dịch, hai thứ này khác nhau đáng kể.
1.2. MAPE — phổ biến nhất và nhiều bẫy nhất
MAPE (Mean Absolute Percentage Error) = trung bình của |e_t| / |y_t| (nhân 100%). Được yêu vì không thứ nguyên (unit-free): "sai 8%" dễ hiểu và so sánh được giữa các chuỗi khác đơn vị. Đây là độ đo hầu như báo cáo nào cũng có. Nhưng nó có những bẫy nghiêm trọng:
- Chia cho 0 hoặc giá trị gần 0: khi
y_t = 0(ngày ATM không phát sinh rút tiền, tài khoản không giao dịch), MAPE vô định (chia cho 0). Khiy_trất nhỏ, một sai số tuyệt đối bé xíu bị "thổi phồng" thành phần trăm khổng lồ — vài ngày như vậy có thể kéo MAPE trung bình lên vô lý. - Bất đối xứng (asymmetric): MAPE phạt dự báo thừa (over-forecast) nặng hơn dự báo thiếu. Ví dụ thực 100, dự báo 150 → MAPE 50%; nhưng thực 100, dự báo 50 → MAPE cũng 50%, trong khi thực 100 dự báo 200 → 100% mà thực 100 dự báo 0 → chỉ 100%. Hệ quả xấu: khi tối ưu MAPE, mô hình có xu hướng dự báo thấp có hệ thống (bias xuống) để tránh bị phạt.
- Không dùng được cho chuỗi có thể âm hoặc đi qua 0.
MAPE ổn cho chuỗi luôn dương và cách xa 0 (doanh số, số dư tổng). Nhưng cho chuỗi thưa, có ngày bằng 0, hay biến động quanh 0 — đừng dùng MAPE.
1.3. sMAPE — vá tạm bẫy bất đối xứng
sMAPE (symmetric MAPE) đặt cả y_t và ŷ_t vào mẫu số (thường là (|y_t| + |ŷ_t|)/2) để bớt bất đối xứng và tránh chia cho một số quá nhỏ. Nó "đối xứng hơn" MAPE nhưng không hoàn hảo: vẫn có vấn đề khi cả thực lẫn dự báo đều gần 0, và bản thân định nghĩa "symmetric" cũng gây tranh cãi (nó phạt over/under không thật sự đối xứng như tên gọi). Coi sMAPE là bản vá, không phải giải pháp triệt để.
1.4. MASE — độ đo nên dùng nhất
MASE (Mean Absolute Scaled Error) giải quyết gốc rễ: chuẩn hoá sai số theo một baseline naive. Công thức: lấy MAE của mô hình chia cho MAE của dự báo naive một-bước trên tập huấn luyện (với chuỗi mùa vụ, dùng seasonal-naive — dự báo bằng giá trị cùng kỳ mùa trước).
MASE = MAE(mô hình) / MAE(naive trên train)
Ý nghĩa cực kỳ trực quan:
- MASE < 1: mô hình tốt hơn naive.
- MASE = 1: ngang naive (vô dụng — bỏ mô hình đi cho nhẹ).
- MASE > 1: tệ hơn cả đoán ngây thơ (đáng báo động).
Ưu điểm áp đảo: không thứ nguyên (so sánh được giữa các chuỗi), không bị bẫy chia 0 (mẫu số là MAE của naive, hiếm khi bằng 0), đối xứng (dựa trên trị tuyệt đối), và quan trọng nhất — nó tự nhúng phép so với baseline vào chính con số. Đây là lý do MASE được ưa chuộng trong các cuộc thi dự báo (M4/M5) và nên là độ đo mặc định của bạn.
1.5. Bias — dự báo lệch một phía không?
Các độ đo trên đo độ lớn sai số, nhưng che mất chiều của nó. Bias (còn gọi mean error hoặc mean forecast error) = trung bình của e_t (giữ nguyên dấu):
- Bias > 0: dự báo thiếu có hệ thống (under-forecast) — hay thiếu tiền, thiếu quỹ.
- Bias < 0: dự báo thừa có hệ thống (over-forecast) — hay thừa tiền, tồn kho.
- Bias ≈ 0: sai số cân bằng hai phía (dù MAE có thể vẫn lớn).
Một mô hình có MAE thấp nhưng bias lớn dương sẽ luôn làm bạn thiếu tiền — nguy hiểm hơn nhiều so với con số MAE gợi ý. Luôn báo cáo bias song song với độ lớn sai số.
1.6. Bảng tóm tắt
| Độ đo | Đơn vị | Bẫy chính | Khi nào dùng |
|---|---|---|---|
| MAE | Như y | Không phạt ngoại lệ | Sai số đều quan trọng, dễ giải thích |
| RMSE | Như y | Nhạy ngoại lệ | Cú sai lớn cực kỳ tốn kém |
| MAPE | % | Chia 0, bất đối xứng, đẩy bias xuống | Chuỗi luôn dương, xa 0 |
| sMAPE | % | Vẫn hỏng khi cả hai gần 0 | Thay MAPE khi có over/under lớn |
| MASE | Không | Cần chọn đúng chu kỳ mùa vụ | Mặc định — luôn nên dùng |
| Bias | Như y | Không đo độ lớn | Luôn kèm theo, phát hiện lệch phía |
2. Chọn độ đo theo bài toán kinh doanh
Đừng chọn độ đo theo thói quen. Câu hỏi phải là: sai theo hướng nào thì tốn tiền hơn? Trong ngân hàng, chi phí thường bất đối xứng (asymmetric cost):
- Tiếp quỹ ATM: thiếu tiền → ATM hết tiền, khách khiếu nại, mất uy tín (chi phí cao). Thừa tiền → chỉ tốn phí cơ hội của lượng tiền nằm không (chi phí thấp). Vậy dự báo thiếu tệ hơn dự báo thừa nhiều lần.
- Dự phòng thanh khoản: thiếu → rủi ro thanh khoản nghiêm trọng; thừa → chi phí vốn.
Khi chi phí bất đối xứng, MAE/RMSE (phạt hai phía như nhau) không phản ánh đúng tổn thất kinh doanh. Giải pháp: pinball loss (quantile loss) — hàm mất mát phạt over và under với trọng số khác nhau tuỳ phân vị q:
pinball(y, ŷ, q) = max( q·(y − ŷ), (q − 1)·(y − ŷ) )
- Dự báo phân vị cao (P95) → phạt nặng under → mô hình có xu hướng dự báo cao hơn để hiếm khi thiếu. Đúng cho tiếp quỹ ATM.
- P50 → đối xứng, tương đương tối ưu MAE.
Bằng cách chọn q, bạn mã hoá chi phí bất đối xứng thẳng vào phép đánh giá và huấn luyện (xem quantile forecasting ở Chuỗi thời gian 5). Nguyên tắc: độ đo đánh giá nên đại diện cho hàm tổn thất kinh doanh thật, không phải con số đẹp để khoe.
3. Tách dữ liệu theo thời gian — quy tắc bất khả xâm phạm
Đây là phần dễ sai nhất và hậu quả nặng nhất. Data leakage (rò rỉ dữ liệu) trong chuỗi thời gian thường tinh vi và làm kết quả offline đẹp lung linh nhưng production sụp đổ.
Ba điều TUYỆT ĐỐI KHÔNG làm:
- KHÔNG shuffle rồi chia ngẫu nhiên. Shuffle trộn quá khứ và tương lai vào cả train lẫn test → mô hình học được "tương lai" để đoán "quá khứ". Với
train_test_split(shuffle=True)(mặc định của sklearn) là sai ngay từ đầu. - KHÔNG dùng tương lai để dự đoán quá khứ. Test phải luôn nằm sau train về thời gian. Điểm cắt là một mốc thời gian, không phải chọn ngẫu nhiên.
- KHÔNG để feature rò rỉ tương lai. Ví dụ chuẩn hoá (scaling) hay tính rolling-mean phải fit chỉ trên train; tính trên toàn bộ dữ liệu là đã nhét thống kê tương lai vào feature. Tương tự, các feature như "lag" phải đảm bảo tại thời điểm dự báo giá trị đó đã thực sự có (xem Chuỗi thời gian 4).
Nguyên tắc gói gọn: tại mọi thời điểm dự báo, mô hình chỉ được biết những gì thực sự đã xảy ra tính đến thời điểm đó — đúng như production.
4. Backtesting — mô phỏng dùng thật trong quá khứ
Backtesting là đánh giá mô hình bằng cách "tua lại lịch sử": giả vờ đứng ở một mốc quá khứ, dự báo về phía trước, so với những gì thực sự xảy ra sau đó. Có ba mức từ đơn giản đến chắc chắn.
4.1. Hold-out cuối (train/test split đơn giản)
Cắt một mốc, huấn luyện trên phần trước, kiểm tra trên đoạn cuối cùng. Đơn giản, nhanh, đúng chiều thời gian. Nhược điểm: chỉ kiểm tra một đoạn — nếu đoạn cuối tình cờ dễ (hoặc khó) bất thường, đánh giá thiên lệch. Dùng cho vòng lặp nhanh, không đủ để kết luận chắc chắn.
4.2. Time series cross-validation — rolling & expanding window
Thay vì một điểm cắt, ta tạo nhiều điểm cắt liên tiếp, mỗi lần huấn luyện đến đó rồi dự báo đoạn kế, gom sai số của tất cả. Đây là cross-validation cho chuỗi thời gian (sklearn gọi là TimeSeriesSplit), và có hai biến thể:
- Expanding window (cửa sổ mở rộng): mỗi fold sau giữ nguyên điểm bắt đầu, train dài dần ra. Dùng khi càng nhiều dữ liệu quá khứ càng tốt và chuỗi tương đối ổn định.
- Rolling / sliding window (cửa sổ trượt): train có độ dài cố định, cả điểm đầu và cuối trượt về phía trước. Dùng khi dữ liệu cũ quá không còn liên quan (hành vi thay đổi theo thời gian, concept drift) — chỉ giữ lại đoạn gần.
Cả hai đều đảm bảo test luôn sau train. Nhiều fold → ước lượng sai số ổn định và ít may rủi hơn hold-out đơn.
4.3. Walk-forward validation
Walk-forward (còn gọi walk-forward optimization) là biến thể chặt chẽ nhất, mô phỏng gần đúng cách mô hình chạy thật trong production: trượt điểm cắt từng bước một (hoặc từng "đợt tái huấn luyện"), mỗi lần huấn luyện lại trên toàn bộ dữ liệu đến hiện tại rồi dự báo bước/đoạn kế. Đây thực chất là rolling/expanding CV với bước nhỏ và có tái huấn luyện — mô phỏng đúng nhịp bạn sẽ retrain trên production (xem Chuỗi thời gian 8). Tốn tính toán nhất nhưng cho ước lượng trung thực nhất.
4.4. Minh hoạ với sklearn TimeSeriesSplit
# Minh hoa — KHONG danh dau chay duoc (day la Python)
import numpy as np
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error
# y: chuoi da sap theo thoi gian (KHONG shuffle)
tscv = TimeSeriesSplit(n_splits=5, test_size=7) # 5 fold, moi fold test 7 ngay
mase_folds = []
for tr_idx, te_idx in tscv.split(y):
y_tr, y_te = y[tr_idx], y[te_idx]
# --- fit mo hinh CHI tren y_tr, du bao 7 buoc ---
y_hat = fit_and_forecast(y_tr, horizon=len(te_idx)) # minh hoa
# baseline seasonal-naive: gia tri cung ky tuan truoc
naive_mae = mean_absolute_error(y_tr[7:], y_tr[:-7]) # naive tren train
model_mae = mean_absolute_error(y_te, y_hat)
mase_folds.append(model_mae / naive_mae) # MASE cua fold
print("MASE trung binh:", np.mean(mase_folds), "+/-", np.std(mase_folds))
# MASE < 1 => tot hon naive; kem theo do lech giua cac fold de biet on dinh
Lưu ý cách TimeSeriesSplit không bao giờ đưa index tương lai vào train — đó là toàn bộ giá trị của nó so với KFold thường.
5. Baseline là bắt buộc — không phải tuỳ chọn
Không có gì tệ hơn việc khoe "mô hình đạt MAPE 8%" mà không biết naive đạt bao nhiêu. Có thể seasonal-naive đã 7% — mô hình phức tạp của bạn còn tệ hơn đoán ngây thơ. Vì vậy:
- Naive: dự báo bước kế = giá trị cuối cùng (
ŷ_{t+1} = y_t). - Seasonal-naive: dự báo = giá trị cùng kỳ mùa trước (
ŷ_{t+1} = y_{t+1−m}, vớimlà chu kỳ — 7 cho ngày trong tuần, 12 cho tháng trong năm). Với dữ liệu ngân hàng mùa vụ mạnh, đây là baseline khó vượt bất ngờ.
Mọi mô hình phải thắng baseline trên cùng backtest mới đáng tồn tại. MASE nhúng sẵn phép so này (mẫu số là naive) nên là cách gọn nhất để ép mình luôn có baseline.
6. Đánh giá dự báo nhiều bước — theo horizon
Dự báo một-bước (one-step ahead) khác hẳn dự báo bảy-bước. Sai số tích luỹ theo horizon: dự báo ngày mai thường chính xác hơn nhiều dự báo ngày thứ bảy. Nếu gộp chung sai số mọi horizon thành một con số, bạn giấu mất thông tin quan trọng.
Cách làm đúng: báo cáo sai số tách theo từng bước horizon (h=1, h=2, ..., h=H). Thường thấy một đường cong sai số tăng dần theo h. Điều này quyết định thực tế: nếu MASE ở h=1..3 tốt nhưng h=6..7 tệ, có lẽ chỉ nên tin dự báo 3 ngày đầu và tái huấn luyện thường xuyên hơn, thay vì cam kết cả tuần.
7. Đánh giá khoảng dự báo — coverage
Nhiều bài toán ngân hàng cần khoảng chứ không chỉ điểm (Chuỗi thời gian 5). Nhưng một khoảng 90% chỉ có ý nghĩa nếu nó thật sự chứa 90% quan sát. Kiểm tra bằng coverage (độ phủ):
- Empirical coverage = tỷ lệ quan sát thực rơi vào khoảng dự báo trên tập backtest. Khoảng 90% mà chỉ phủ 70% → quá hẹp, tự tin thái quá, nguy hiểm (bạn tưởng an toàn nhưng không). Phủ 99% → quá rộng, vô dụng cho quyết định (khoảng bao trùm mọi thứ thì chẳng nói lên gì).
- Khoảng tốt phải hiệu chỉnh (calibrated): coverage thực ≈ mức danh nghĩa. Đồng thời muốn khoảng càng hẹp càng tốt miễn còn giữ đúng coverage (đo bằng interval width hoặc pinball loss trên các phân vị).
Quy tắc: luôn kiểm coverage của prediction interval trên backtest, đừng chỉ tin mức danh nghĩa mô hình in ra. Một khoảng sai coverage còn nguy hiểm hơn không có khoảng, vì nó tạo cảm giác an toàn giả.
Use case thực tế
Bối cảnh: Đội Vận hành NCB cần chọn mô hình dự báo lượng tiền rút tại 250 ATM cho 7 ngày tới (nối tiếp use case Chuỗi thời gian 5). Có ba ứng viên: seasonal-naive, Prophet, DeepAR. Trước đây họ chỉ nhìn MAPE trên 7 ngày cuối và định chọn DeepAR. Đội Rủi ro yêu cầu đánh giá lại cho chặt.
Bước 1 — Bỏ MAPE, dùng MASE. Một số ATM ở khu công nghiệp có ngày Chủ nhật = 0 giao dịch → MAPE vô định, và những ngày rất ít giao dịch thổi MAPE lên 300%. Chuyển sang MASE (baseline seasonal-naive m=7), không bị bẫy chia 0.
Bước 2 — Backtesting đàng hoàng. Thay vì một hold-out, chạy walk-forward 12 fold (12 tuần gần nhất), mỗi fold train đến hết tuần trước, dự báo 7 ngày kế, tái huấn luyện mỗi tuần — đúng nhịp production dự kiến.
Bước 3 — Kết quả (MASE trung bình 12 fold):
| Mô hình | MASE | Bias (tỷ VND/ngày) | Coverage khoảng 90% |
|---|---|---|---|
| Seasonal-naive | 1.00 | +0.4 | — |
| Prophet | 0.71 | +0.1 | 88% |
| DeepAR | 0.63 | −0.6 | 82% |
Bước 4 — Đọc kết quả sâu hơn. DeepAR có MASE thấp nhất (thắng naive 37%), nhưng bias âm 0.6 tỷ — dự báo thừa có hệ thống, và coverage chỉ 82% cho khoảng danh nghĩa 90% (quá tự tin). Với chi phí bất đối xứng (thiếu tiền tệ hơn thừa), bias âm thực ra "an toàn" theo hướng ít hết tiền, nhưng coverage 82% nghĩa là khoảng P95 dùng để tiếp quỹ hẹp hơn thực tế → vẫn có rủi ro thiếu. Đội quyết định huấn luyện DeepAR theo pinball loss ở P95 để trực tiếp tối ưu mức tiếp quỹ, rồi kiểm lại coverage.
Bước 5 — Theo horizon. Tách MASE theo h: DeepAR ở h=1..3 rất tốt (0.55) nhưng h=6..7 xấu đi (0.78). Kết luận vận hành: tin dự báo 3 ngày đầu, và retrain 2 lần/tuần thay vì 1 lần để luôn ở gần horizon ngắn. Nhờ đánh giá đúng, đội tránh được việc triển khai một khoảng dự báo sai coverage lên toàn bộ 250 ATM.
Ghi nhớ
- Chọn độ đo là quyết định kinh doanh. MAE (đơn vị gốc, đều), RMSE (phạt nặng sai lớn), MAPE (%, nhưng vô định khi y=0 và bất đối xứng — đẩy dự báo thấp). sMAPE chỉ vá tạm.
- MASE là độ đo mặc định nên dùng: chuẩn hoá theo naive nên < 1 là tốt hơn naive, > 1 là tệ hơn cả đoán ngây thơ; không thứ nguyên, không bẫy chia 0, tự nhúng baseline.
- Luôn báo cáo bias cùng độ lớn sai số — để biết mô hình lệch thiếu hay thừa; MAE thấp mà bias lớn vẫn nguy hiểm.
- Chi phí bất đối xứng (thiếu vs thừa tiền) → dùng pinball/quantile loss ở phân vị phù hợp (P95 cho tiếp quỹ) để mã hoá tổn thất thật.
- Tách dữ liệu theo thời gian, TUYỆT ĐỐI không shuffle, không dùng tương lai đoán quá khứ, không để feature/scaler rò rỉ tương lai.
- Backtesting: hold-out cuối (nhanh) → time series CV rolling/expanding (
TimeSeriesSplit) → walk-forward (chặt nhất, mô phỏng retrain production). - Baseline naive/seasonal-naive là bắt buộc — mọi mô hình phải thắng nó trên cùng backtest mới đáng tồn tại.
- Đánh giá theo từng horizon (sai số tăng dần theo h) và kiểm coverage của khoảng dự báo (khoảng 90% có thật sự phủ ~90% không) — khoảng sai coverage nguy hiểm hơn không có khoảng.
Bài viết liên quan
Phân biệt AI/ML/DL, các kiểu học máy, quy trình ML end-to-end và thuật ngữ nền tảng.
Chất lượng dữ liệu, xử lý thiếu/ngoại lai, mã hoá, chuẩn hoá, tạo đặc trưng và tránh data leakage.
Mô hình ngôn ngữ lớn hoạt động ra sao, token, context window, tham số sinh, và kỹ thuật viết prompt.
Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.