Chuỗi thời gian 2 — EDA, tính dừng & phân rã

13 thg 7, 2026 6 lượt xem
#ai
#time-series
#decomposition
#acf-pacf
#stationarity

Chuỗi thời gian 2 — EDA, tính dừng & phân rã

Trước khi ném dữ liệu vào bất kỳ mô hình dự báo nào — từ ARIMA cổ điển đến LSTM — bạn phải hiểu chuỗi đang có trong tay. Với dữ liệu bảng, EDA (Exploratory Data Analysis — phân tích khám phá) xoay quanh phân phối và tương quan giữa các cột. Với chuỗi thời gian, câu hỏi trung tâm khác hẳn: thống kê của chuỗi có thay đổi theo thời gian không? Trung bình có trôi lên không? Biến động (variance) tháng 12 có lớn hơn tháng 6 không? Có chu kỳ lặp lại hằng tuần không? Trả lời được những câu này là bạn đã xong 60% việc mô hình hoá.

Bài này đi qua bốn khối: (1) EDA cho chuỗi thời gian, (2) tính dừng (stationarity) và kiểm định, (3) các biến đổi để đạt dừng, (4) tự tương quan ACF/PACF và phân rã. Bài tiếp nối Chuỗi thời gian 1 — Tổng quan; các khái niệm trend/seasonality/nhiễu đã định nghĩa ở đó được dùng lại ở đây.

Danh mục AI. Các đoạn Python (pandas/statsmodels) là minh hoạ cho thấy hình dạng phép tính, không phải sandbox chạy được. Chỉ block SQL đánh dấu ▶ Chạy được mới thực thi trên sandbox PostgreSQL read-only.


1. EDA cho chuỗi thời gian

1.1. Bước đầu tiên luôn là vẽ đường

Không có công cụ nào thay được biểu đồ đường (line plot) của chuỗi theo thời gian. Mắt người phát hiện trend, mùa vụ, đứt gãy và ngoại lai nhanh hơn bất kỳ thống kê nào. Với chuỗi ngân hàng — ví dụ tổng số tiền giao dịch theo ngày — nhìn đường là thấy ngay ba thứ: xu hướng dài hạn (trend), nhịp lặp (seasonality), và những cú nhảy bất thường (outlier).

# Minh hoạ — pandas
import pandas as pd
df = pd.read_csv("daily_txn.csv", parse_dates=["day"])
s = df.set_index("day")["total_amount"].asfreq("D")  # ép tần số hằng ngày
s.plot(figsize=(12, 4), title="Tổng tiền giao dịch theo ngày")

Bước .asfreq("D") rất quan trọng và hay bị bỏ quên: nó ép chuỗi về tần số đều (một điểm mỗi ngày). Nếu có ngày nghỉ không phát sinh giao dịch, chỗ đó thành NaN — tức là khoảng trống (gap) hiển hiện, thay vì bị nối liền âm thầm khiến bạn tưởng dữ liệu liên tục.

1.2. Rolling mean và rolling std

Đường gốc thường quá nhiễu để thấy xu hướng. Trung bình trượt (rolling mean) làm mượt để lộ trend; độ lệch chuẩn trượt (rolling std) cho thấy biến động có ổn định theo thời gian không.

# Minh hoạ
roll = s.rolling(window=30)          # cửa sổ 30 ngày
mean30 = roll.mean()
std30 = roll.std()

Cách đọc:

  • Rolling mean đi ngang → chuỗi có thể dừng về trung bình. Rolling mean trôi lên/xuống → có trend, chưa dừng.
  • Rolling std đi ngang → phương sai ổn định. Rolling std phình ra theo thời gian → phương sai không đồng nhất (heteroscedasticity), thường cần biến đổi log/Box-Cox (mục 4).

Chọn cửa sổ theo chu kỳ nghi ngờ: 7 để soi nhịp tuần, 30 cho nhịp tháng, 365 cho nhịp năm.

1.3. Bốn thứ cần phát hiện

Thành phầnDấu hiệu trên biểu đồKiểm chứng nhanh
TrendĐường trung bình đi lên/xuống dài hạnRolling mean không phẳng
SeasonalityMẫu lặp đúng chu kỳ cố định (tuần/tháng/năm)ACF có đỉnh tại lag = chu kỳ
OutlierĐiểm nhảy vọt rồi trở lạiLệch > 3σ so với rolling mean
Gap / missingĐoạn phẳng bất thường hoặc NaNs.isna().sum(), kiểm tra freq

Phân biệt seasonality (chu kỳ cố định, ví dụ đỉnh giao dịch mỗi cuối tháng do trả lương) với cyclicity (chu kỳ không cố định, ví dụ chu kỳ kinh tế vài năm). Mô hình chuỗi thời gian cổ điển xử lý tốt seasonality; cyclicity khó hơn nhiều.

1.4. Nền tảng SQL — biến động theo ngày

Trước khi kéo dữ liệu về pandas, phần lớn việc tổng hợp và làm mượt có thể làm ngay trong kho. Ví dụ tính tổng tiền giao dịch mỗi ngày kèm trung bình trượt 7 ngàybiến động ngày-qua-ngày bằng window function trên bảng transactions:

-- ▶ Chạy được
WITH daily AS (
  SELECT date_trunc('day', created_at) AS day,
         SUM(amount) AS total_amount,
         COUNT(*)    AS txn_count
  FROM transactions
  GROUP BY date_trunc('day', created_at)
)
SELECT day,
       total_amount,
       txn_count,
       AVG(total_amount) OVER (ORDER BY day
             ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)  AS rolling_avg_7d,
       total_amount - LAG(total_amount) OVER (ORDER BY day) AS diff_1d
FROM daily
ORDER BY day;

AVG(...) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) chính là rolling mean 7 ngày làm ngay trong SQL; LAG cho sai phân bậc 1 theo ngày — nền tảng của differencing ở mục 3. Đẩy phần nặng này xuống database rồi mới về pandas là mẫu tốt cho dữ liệu lớn.


2. Tính dừng (stationarity)

2.1. Trực giác

Một chuỗi dừng khi các tính chất thống kê của nó không đổi theo thời gian. Cụ thể (dừng yếu — weak stationarity):

  • Trung bình (mean) hằng số — không có trend.
  • Phương sai (variance) hằng số — biến động không phình/co theo thời gian.
  • Hiệp phương sai (autocovariance) chỉ phụ thuộc khoảng cách lag, không phụ thuộc thời điểm tuyệt đối.

Hình dung: nếu cắt chuỗi dừng thành nhiều đoạn, mọi đoạn "trông giống nhau" về mức trung bình và độ dao động. Ngược lại, chuỗi số dư tài khoản tăng đều theo năm không dừng (mean trôi lên); chuỗi biến động thị trường bùng nổ trong khủng hoảng rồi lặng xuống cũng không dừng (variance đổi).

2.2. Vì sao mô hình cần dừng?

Nhiều mô hình cổ điển — nổi bật là ARIMA — giả định chuỗi dừng vì chúng học một bộ tham số cố định (hệ số AR, MA) mô tả quan hệ giữa các điểm. Nếu bản chất chuỗi thay đổi theo thời gian, một bộ tham số không thể đúng cho cả quá khứ lẫn tương lai → dự báo lệch. Dừng cho phép giả định "quá khứ giống tương lai về mặt cấu trúc", nền tảng để ngoại suy.

Chữ I trong ARIMA (Integrated) chính là số lần differencing cần để biến chuỗi thành dừng — sẽ dùng trong Chuỗi thời gian 3 — Mô hình cổ điển. Ngay cả các mô hình ML/DL không đòi hỏi dừng nghiêm ngặt cũng học ổn định hơn khi bạn tách trend/mùa vụ ra trước.

2.3. Kiểm định: ADF và KPSS

Mắt nhìn là chưa đủ; ta cần kiểm định thống kê. Hai kiểm định phổ biến, và giả thuyết của chúng ngược nhau — đây là điểm hay nhầm:

ADF (Augmented Dickey-Fuller)KPSS (Kwiatkowski–Phillips–Schmidt–Shin)
H0 (giả thuyết gốc)Chuỗi không dừng (có unit root)Chuỗi dừng
H1Chuỗi dừngChuỗi không dừng
p-value nhỏ (< 0.05) nghĩa làBác bỏ H0 → dừngBác bỏ H0 → không dừng
# Minh hoạ — statsmodels
from statsmodels.tsa.stattools import adfuller, kpss
adf_stat, adf_p, *_ = adfuller(s.dropna())
kpss_stat, kpss_p, *_ = kpss(s.dropna(), regression="c", nlags="auto")

Dùng cả hai để tránh kết luận sai:

ADFKPSSKết luận
p < 0.05 (dừng)p > 0.05 (dừng)Dừng — yên tâm
p > 0.05 (không dừng)p < 0.05 (không dừng)Không dừng — cần biến đổi
p < 0.05p < 0.05Mâu thuẫn → thường là dừng sai phân (difference-stationary), thử differencing
p > 0.05p > 0.05Mâu thuẫn → thường là dừng theo trend (trend-stationary), thử khử trend

Lưu ý: kiểm định chỉ là bằng chứng bổ trợ, không thay được biểu đồ. Chuỗi có mùa vụ mạnh có thể "qua" ADF nhưng vẫn cần xử lý seasonality.


3. Biến đổi để đạt dừng

3.1. Sai phân (differencing) — khử trend

Sai phân bậc 1 thay mỗi giá trị bằng chênh lệch với giá trị liền trước: y'_t = y_t − y_{t−1}. Phép này khử trend tuyến tính. Nếu sau một lần vẫn chưa dừng, làm sai phân bậc 2 (sai phân của sai phân) — hiếm khi cần quá bậc 2.

# Minh hoạ
d1 = s.diff(1)          # sai phân bậc 1: khử trend

Sai phân mùa vụ (seasonal differencing) trừ giá trị của cùng kỳ chu kỳ trước: với chu kỳ m (7 cho tuần, 12 cho tháng), y'_t = y_t − y_{t−m}. Phép này khử seasonality.

# Minh hoạ — chuỗi có mùa vụ tuần
d7 = s.diff(7)          # sai phân mùa vụ chu kỳ 7 ngày

Chuỗi vừa có trend vừa có mùa vụ thường cần cả hai (áp dụng nối tiếp). Nhưng đừng sai phân quá tay (over-differencing): dấu hiệu là ACF có đỉnh âm lớn tại lag 1 và phương sai phình lên — lúc đó bạn đã thêm nhiễu chứ không khử.

3.2. Log / Box-Cox — ổn định phương sai

Differencing xử lý trung bình trôi, nhưng không sửa được phương sai không đồng nhất. Khi biến động tỷ lệ với mức giá trị (rolling std phình theo rolling mean — rất phổ biến ở dữ liệu tiền tệ, doanh số), hãy biến đổi trước khi sai phân:

  • Log: log(y). Đơn giản, biến quan hệ nhân thành cộng, nén biến động ở giá trị lớn. Yêu cầu y > 0 (tiền, số lượng thường thoả).
  • Box-Cox: họ biến đổi tổng quát tham số hoá bởi λ, tự chọn λ tối ưu (λ=0 tương đương log, λ=1 là không đổi). Cần y > 0.
# Minh hoạ
import numpy as np
from scipy.stats import boxcox
log_s = np.log(s)                    # ổn định phương sai
bc, lam = boxcox(s.dropna().values)  # Box-Cox, lam = lambda tối ưu

Thứ tự đúng: ổn định phương sai trước (log/Box-Cox) → rồi mới khử trung bình (differencing). Và nhớ lưu lại phép biến đổi để đảo ngược khi trả kết quả dự báo về thang gốc (ví dụ exp() sau khi mô hình trên thang log).

3.3. Sơ đồ quy trình


4. ACF, PACF và phân rã

4.1. ACF và PACF — đọc "trí nhớ" của chuỗi

ACF (Autocorrelation Function — hàm tự tương quan) đo tương quan giữa chuỗi và chính nó ở các độ trễ (lag) khác nhau: lag 1 là quan hệ với hôm qua, lag 7 với cùng thứ tuần trước. ACF phản ánh cả tương quan trực tiếp lẫn gián tiếp (nếu hôm nay liên hệ hôm qua, hôm qua liên hệ hôm kia, thì hôm nay "vọng" tới hôm kia qua trung gian).

PACF (Partial Autocorrelation Function — tự tương quan riêng phần) đo tương quan tại lag k sau khi đã loại bỏ ảnh hưởng của các lag trung gian — tức quan hệ trực tiếp thuần tuý.

# Minh hoạ
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
plot_acf(s.dropna(), lags=40)
plot_pacf(s.dropna(), lags=40)

Cách đọc để nhận diện thành phần và gợi ý bậc mô hình (dùng ở bài ARIMA):

Quan sátÝ nghĩa
ACF giảm rất chậm, gần tuyến tínhCòn trend → cần differencing
ACF có đỉnh tại lag = m (7, 12...) và bội số của mCó seasonality chu kỳ m
ACF tắt đột ngột sau lag q, PACF tắt dầnGợi ý thành phần MA bậc q
PACF tắt đột ngột sau lag p, ACF tắt dầnGợi ý thành phần AR bậc p

Vạch tin cậy (confidence band, thường ±1.96/√n) là ngưỡng: đỉnh vượt ra ngoài mới coi là đáng kể.

4.2. Phân rã (decomposition)

Phân rã tách chuỗi thành ba thành phần để nhìn riêng từng phần:

  • Trend (T) — xu hướng dài hạn.
  • Seasonal (S) — thành phần mùa vụ lặp lại.
  • Residual/Remainder (R) — phần còn lại (nhiễu + bất thường).

Hai kiểu kết hợp:

  • Cộng (additive): y = T + S + R — khi biên độ mùa vụ không đổi theo mức.
  • Nhân (multiplicative): y = T × S × R — khi biên độ mùa vụ tỷ lệ với mức (thường thấy ở dữ liệu tăng trưởng; log-hoá sẽ đưa về dạng cộng).

Classical decomposition dùng moving average để ước lượng trend rồi trung bình hoá theo chu kỳ để lấy seasonal. Đơn giản nhưng cứng: seasonal cố định theo thời gian, mất dữ liệu ở hai đầu, nhạy outlier.

STL (Seasonal-Trend decomposition using Loess) dùng hồi quy cục bộ (loess) và linh hoạt hơn hẳn: cho phép seasonal biến đổi dần theo thời gian, bền với outlier (qua tuỳ chọn robust), và kiểm soát được độ mượt của trend/seasonal. STL là lựa chọn mặc định tốt cho phần lớn dữ liệu ngân hàng.

# Minh hoạ — STL
from statsmodels.tsa.seasonal import STL
res = STL(s, period=7, robust=True).fit()
trend, seasonal, resid = res.trend, res.seasonal, res.resid

Kiểm tra chất lượng phân rã bằng cách nhìn residual: nếu nó còn mẫu rõ ràng (còn đỉnh trong ACF), tức là trend/seasonal chưa tách hết. Residual gần nhiễu trắng (white noise) là dấu hiệu tốt.

4.3. Missing và outlier trong chuỗi thời gian

Chuỗi thời gian không cho phép xử lý missing/outlier bừa như dữ liệu bảng, vì thứ tự và tính liên tục là bản chất.

Missing: đừng bỏ dòng (sẽ làm lệch tần số). Tuỳ ngữ cảnh:

  • Forward fill (ffill) — lặp giá trị cuối, hợp cho số dư tài khoản (giữ nguyên tới khi có thay đổi).
  • Nội suy tuyến tính/thời gian (interpolate) — hợp cho đại lượng biến thiên trơn.
  • Điền 0 — chỉ khi vắng nghĩa là "không có giao dịch" (ví dụ tổng tiền ngày nghỉ).

Outlier: phân biệt outlier thật cần loại (lỗi nhập liệu, sự cố hệ thống) với sự kiện thật cần giữ (đỉnh giao dịch cuối năm, gian lận). Phát hiện bằng lệch so với rolling mean (ví dụ > 3× rolling std) hoặc dựa trên residual của STL. Với chuỗi sẽ mô hình hoá, thay vì xoá thẳng, nên winsorize (cắt ngưỡng) hoặc thay bằng nội suy để giữ độ dài chuỗi.

# Minh hoạ — điền và phát hiện outlier
s_filled = s.interpolate(method="time")
resid = STL(s_filled, period=7, robust=True).fit().resid
outliers = resid[resid.abs() > 3 * resid.std()]

Use case thực tế

Bối cảnh: Phòng Vận hành NCB muốn dự báo tổng giá trị giao dịch theo ngày để cân đối thanh khoản và bố trí nhân lực xử lý. Trước khi chọn mô hình, nhóm data làm EDA + kiểm định dừng theo đúng quy trình trên.

Bước 1 — Tổng hợp trong kho. Chạy truy vấn ở mục 1.4 trên transactions: nhóm theo ngày, tính total_amount, rolling 7 ngày và diff_1d. Kết quả 730 điểm (2 năm), 11 ngày NaN trùng lễ Tết.

Bước 2 — EDA. Line plot lộ rõ: (a) trend tăng nhẹ theo năm (mở rộng khách hàng); (b) nhịp tuần rõ — thứ Hai và cuối tháng cao vọt do lương/thanh toán; (c) rolling std phình dần → phương sai không đồng nhất.

Bước 3 — Biến đổi. Vì rolling std tỷ lệ với mức, nhóm log-hoá trước. ADF trên chuỗi log: p = 0.31 (không dừng); KPSS: p = 0.01 (không dừng) → cần khử trend. Sai phân bậc 1 → ADF p = 0.002, KPSS p = 0.1 → dừng. ACF chuỗi đã sai phân vẫn còn đỉnh tại lag 7 → thêm sai phân mùa vụ lag 7, residual sạch.

Bước 4 — Phân rã & làm sạch. STL (period=7, robust) tách trend/seasonal/residual. Residual lộ 3 điểm > 3σ: 2 điểm trùng sự cố downtime hệ thống (điền lại bằng nội suy), 1 điểm là ngày khuyến mãi thật (giữ, đánh dấu để làm biến ngoại sinh sau).

Kết quả: Sau bước này nhóm biết chắc chuỗi cần d=1, D=1, m=7 — vào thẳng SARIMA ở Chuỗi thời gian 3 mà không phải dò mù. Thời gian chuẩn bị dữ liệu giảm từ vài ngày thử-sai xuống nửa buổi, và sai số dự báo backtest thấp hơn ~18% so với lần trước làm ẩu (chỉ số minh hoạ).

Ghi nhớ

  • Luôn vẽ đường trước tiên, kèm rolling mean (soi trend) và rolling std (soi phương sai). Ép .asfreq để lộ gap.
  • Dừng = thống kê không đổi theo thời gian (mean, variance, autocovariance). Nhiều mô hình cổ điển (ARIMA) yêu cầu dừng vì học một bộ tham số cố định.
  • ADF và KPSS ngược giả thuyết nhau — dùng cả hai. ADF H0 = không dừng; KPSS H0 = dừng.
  • Thứ tự biến đổi: ổn định phương sai (log/Box-Cox) → khử trend (sai phân bậc 1) → khử mùa vụ (sai phân lag m). Đừng over-differencing. Lưu phép biến đổi để đảo ngược.
  • ACF/PACF nhận diện thành phần và gợi bậc mô hình: ACF giảm chậm → còn trend; đỉnh tại lag m → mùa vụ; ACF/PACF tắt đột ngột → gợi bậc MA/AR.
  • STL > classical cho phần lớn ca thực tế: seasonal biến đổi được, bền với outlier. Residual gần nhiễu trắng là dấu hiệu phân rã tốt.
  • Missing/outlier trong TS phải giữ thứ tự và tần số: ffill/nội suy tuỳ ngữ cảnh, không xoá dòng; phân biệt outlier lỗi (loại/winsorize) với sự kiện thật (giữ làm biến ngoại sinh).

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.

13 thg 7, 2026 15

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.

13 thg 7, 2026 13

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.

13 thg 7, 2026 13

Retrieval-Augmented Generation, vector database, tool calling, agent và MCP để xây ứng dụng LLM thực tế.

13 thg 7, 2026 11