🗺️ AI 學習與考證地圖
中級科目三程式實戰 · 時間序列驗證

銷量資料的考卷不能洗牌:
驗證期間,永遠要晚於訓練期間

模型上線後只做一件事:拿過去,預測未來。驗證如果不照這個劇本演——隨機打散、讓未來混進訓練——分數就是假的。本頁用一份會漲的銷量資料實跑給你看:同一個隨機森林,隨機 K-fold 打 0.980、TimeSeriesSplit 卻是 -1.828(比亂猜還糟)——差距就是「偷看未來」的幅度。答案 D 的每個字(保持時間順序、rolling window、驗證晚於訓練、不可打散)都會在四個實驗室裡逐一驗證。

閱讀模式

00題目

對依日期排序的銷量資料進行驗證,使用下列切分器。下列對此驗證方式的描述何者最正確?

import numpy as np                                    # 拿出陣列工具
from sklearn.model_selection import TimeSeriesSplit    # 時間序列專用切分器
# X 與 y 已依日期排序,明確轉為 NumPy arrays 後再以整數位置索引  # 前提:資料照日期排好
X = np.asarray(X)                                     # 轉成 NumPy 陣列
y = np.asarray(y)                                     # 同上
tscv = TimeSeriesSplit(n_splits=5)                   # 切 5 折,各折驗證都在訓練之後
for train_idx, valid_idx in tscv.split(X):            # 逐折拿到「訓練/驗證」位置
    X_train, X_valid = X[train_idx], X[valid_idx]     # 按位置取出兩份特徵
    y_train, y_valid = y[train_idx], y[valid_idx]     # 按位置取出兩份答案

先說:為什麼時間資料的考卷不能洗牌

一般資料的交叉驗證像洗撲克牌——每筆資料獨立,怎麼洗都公平。但銷量是時間的產物:昨天影響今天、今天影響明天。模型上線那天面對的處境永遠是「只知道過去,要猜未來」——所以驗證必須照這個處境彩排:用 1~3 月訓練、拿 4 月驗證,一折一折往後推。

隨機打散是什麼?是把下個月的考卷混進這個月的講義。模型背了未來的答案再去考未來,分數當然漂亮——但那是作弊的成績,上線立刻現形。

兩個先立好的事實: 驗證的唯一使命是預演上線——上線是「過去 → 未來」的外推,驗證就必須是外推。 TimeSeriesSplit 的鐵律:每一折都保證 max(train_idx) < min(valid_idx)——訓練最晚的一天,都早於驗證最早的一天(本站實跑五折逐一 assert 過)。

先點開看:TimeSeriesSplit 是什麼?時間洩漏內插與外推

不熟 AI 專有名詞?你需要先認識下列名詞

點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。

切分器家族
TimeSeriesSplit交叉驗證K-fold 對照shuffle 打散
視窗的走法
擴張視窗rolling windowgap 參數test_size 公式
洩漏與陷阱
時間洩漏洩漏家族(第三次見)內插與外推樹模型與趨勢
時序資料的性質
自相關滯後特徵回測 backtestingR² 是負的怎麼讀

不熟 Python?你需要先認識下列名詞

點擊後出現漸進式說明

把工具拿進來
importfrom模組 module
存東西、轉陣列
= 指派變數與命名np.asarray關鍵字引數# 註解
迴圈與取資料
for 迴圈雙變數解包X[train_idx] 整數陣列索引( ) 呼叫括號. 點運算子方法 method

01逐行拆解:第 1 行到第 9 行

九行,一段一段走完。右上角的「看位置」可以把這一段放回完整程式裡看。

第 1 行拿出陣列工具
import numpy as np   # 整箱搬進來,取小名 np

numpy 這次的任務不是造資料,是把資料轉成「能用整數位置取值」的陣列(第 4、5 行)——這是後面 X[train_idx] 那種取法的前提。

相關名詞:import模組 module

第 2 行拿出時間序列專用切分器
from sklearn.model_selection import TimeSeriesSplit   # 「照時間排隊」的切分器

sklearn.model_selection 是「怎麼切資料」的抽屜:KFold、train_test_split、GridSearchCV(系列第 4 頁)都住這裡。TimeSeriesSplit 是專為時間資料設計的成員——它跟 KFold 最大的差別:切出來的每一折,驗證都排在訓練之後,而且從頭到尾不洗牌。

名字就是提示:Time(時間)Series(序列)Split(切分)——看到題目說「依日期排序」,就該想到它。

相關名詞:TimeSeriesSplitK-fold 對照

第 3–5 行前提聲明+轉成 NumPy 陣列
# X 與 y 已依日期排序,明確轉為 NumPy arrays 後再以整數位置索引  # 這行註解是題眼
X = np.asarray(X)   # 轉成 NumPy 陣列(已是陣列就原樣返回)
y = np.asarray(y)   # 同上

第 3 行的註解是題眼,講了兩個前提:①「已依日期排序」——TimeSeriesSplit 只認「位置」不認「日期」,它假設第 0 筆最早、最後一筆最晚;資料沒排序,切出來的「過去/未來」就是錯的。②「以整數位置索引」——切分器吐出的是位置編號(0、1、2⋯⋯),用 np.asarray 轉成 NumPy 陣列後,X[train_idx] 才能一次按一串位置取值。

asarrayarray 的差別:已經是陣列就不再複製、原樣返回——「確保是陣列」的禮貌寫法。若 X 是 pandas DataFrame,直接 X[train_idx] 會被當成「取欄名」而出錯——這就是題目特別聲明「明確轉為 NumPy arrays」的原因(DataFrame 派的寫法是 X.iloc[train_idx])。

相關名詞:np.asarray# 註解整數陣列索引

第 6 行組一台切 5 折的時序切分器
tscv = TimeSeriesSplit(n_splits=5)   # 5 折:驗證段一折一折往後推

n_splits=5:切 5 折。每折的驗證段大小預設是 n ÷ (n_splits+1)——本站實跑 120 天資料:驗證段 120 ÷ 6 = 20 天,五折的訓練集依序是 20、40、60、80、100 天(擴張視窗:訓練集一折比一折長,驗證段緊跟在後面)。

本站實跑(120 天):五折依序為「訓練第 0~19 天 → 驗證第 20~39 天」、「訓練 0~39 → 驗證 40~59」⋯⋯「訓練 0~99 → 驗證 100~119」。每一折都 assert 過 max(train_idx) < min(valid_idx)——訓練最晚的一天永遠早於驗證最早的一天。想改成 rolling window,加 max_train_size;想留緩衝,加 gap(02 節實驗室都能玩)。

相關名詞:擴張視窗test_size 公式rolling windowgap 參數

第 7 行逐折拿到「訓練/驗證」的位置名單
for train_idx, valid_idx in tscv.split(X):   # 每圈吐一對位置名單

tscv.split(X) 每一圈吐出一對位置名單train_idx(這一折用來訓練的位置)與 valid_idx(用來驗證的位置)——for 迴圈用「雙變數解包」一次接住兩個。迴圈跑 5 圈=5 折。

注意 split 只給「位置編號」不給資料本身——像發座位表不發考卷。真正取資料是下一行的事。

相關名詞:for 迴圈雙變數解包方法 method

第 8–9 行按位置名單,取出這一折的四份資料
    X_train, X_valid = X[train_idx], X[valid_idx]   # 特徵:訓練一份、驗證一份
    y_train, y_valid = y[train_idx], y[valid_idx]   # 答案:訓練一份、驗證一份

X[train_idx] 是 NumPy 的整數陣列索引:中括號裡放一串位置,一次取回一疊資料——這正是第 4、5 行先轉 NumPy 的原因。兩行各做一次雙變數解包,湊出這一折的四件套:X_train / X_valid / y_train / y_valid——接著就能 fit(X_train, y_train)、在 X_valid 上評分(完整流程見「完整程式碼」)。

整段程式最重要的「沒寫的字」:沒有 shuffle。從頭到尾沒有任何洗牌動作——TimeSeriesSplit 沒有 shuffle 參數,這不是疏忽,是設計:時間資料的驗證,順序就是生命線。

相關名詞:整數陣列索引雙變數解包shuffle 打散

02摺疊檢視器:五折長什麼樣?

把 120 天的銷量資料交給題目的切分器,五折的長相實跑畫出來——藍色是訓練、紅色是驗證。三種走法切換著看:

互動實驗室:摺疊檢視器expanding/rolling/gap 三種走法
訓練 train驗證 valid斜紋=gap 緩衝(兩邊都不用)
本站實跑鐵證:三種走法的每一折都通過 assert max(train_idx) < min(valid_idx)——訓練最晚的一天,永遠早於驗證最早的一天。這就是選項 D 說的「驗證期間晚於訓練期間」的程式版本。驗證段大小 = 120 ÷ (5+1) = 20 天,五折驗證段首尾相接、一路推到最後 20 天。

相關名詞:擴張視窗rolling windowgap 參數

03洩漏顯微鏡:隨機 K-fold 的訓練集裡,多少是未來?

選項 C 說「隨機 K-fold 與 TimeSeriesSplit 對時間洩漏的風險完全相同」。把兩個切分器都對準同一份 120 天資料,看第 3 折的訓練(藍)與驗證(紅)落在時間軸的哪裡:

互動實驗室:洩漏顯微鏡同一折,兩種切法的時間分布
← 第 1 天(最早)第 120 天(最晚)→
訓練集大小
60
這一折用來學的天數
訓練集裡「來自未來」的樣本
0%
晚於驗證最早那天的訓練樣本
五折平均
0%
未來混入訓練的比例
本站實跑(五折逐一數):TimeSeriesSplit 每一折「來自未來的訓練樣本」都是 0%;隨機 K-fold(shuffle=True)五折依序是 100%、95%、97%、94%、99%——訓練集幾乎整包都晚於該折最早的驗證樣本。「風險完全相同」?一邊是 0%、一邊是 94%~100%——選項 C 陣亡。

相關名詞:時間洩漏K-fold 對照shuffle 打散

04假安心 vs 誠實:同一個模型,兩張成績單

洩漏會付出什麼代價?拿這份 120 天銷量(有上升趨勢+週期)做特徵(昨天、上週同日、星期幾),同一個模型考兩種考卷,再用「上線預演」(前 100 天訓練、最後 20 天盲測)當裁判:

互動實驗室:兩張成績單隨機森林 vs Ridge,誰被隨機 CV 騙了
隨機 K-fold 平均 R²
0.980
洗牌考卷:假安心
TimeSeriesSplit 平均 R²
-1.828
時序考卷:誠實
上線預演(最後 20 天盲測)
-0.949
真相:誰的估計接近它?
兩課一次上: 隨機森林的 0.980 是假的——隨機切分讓每個驗證點的「左鄰右舍」都在訓練集裡,模型只要內插就能拿高分;時序考卷一來(-1.828,比永遠猜平均還糟),才揭穿它根本外推不了上升趨勢(樹模型預測不會超出訓練時見過的範圍)。 換成會外推的線性 Ridge,時序分數回到 0.658、上線預演 0.875——正確的驗證方式不只給誠實分數,還會幫你挑對模型:被隨機 CV 評分時兩個模型看起來一樣好(0.980 vs 0.987),真相是一個能用、一個不能。

相關名詞:樹模型與趨勢R² 是負的怎麼讀滯後特徵

05內插考卷 vs 外推考卷:把 120 天畫出來

為什麼兩張考卷差這麼多?把銷量點畫在時間軸上(本站實跑資料:一路向上的趨勢+週期起伏),看兩種切法把「驗證」放在哪裡:

互動實驗室:考卷位置圖紅點=驗證;它站在哪,決定考的是什麼
訓練日驗證日
紅點的位置就是考卷的性質:時序切分的紅點全部站在訓練資料的右邊(未來)、而且銷量水位比訓練期間都高——考的是外推,跟上線一模一樣。隨機切分的紅點散在訓練點中間,每個紅點左右都有藍點撐腰——考的是內插,一種上線後永遠不會出現的輕鬆題型。0.980 就是這樣考出來的。

06考場加碼:expanding、rolling、gap 怎麼選

選項 D 提到「TimeSeriesSplit 或 rolling window」——兩個都是合法的時序驗證,差在訓練視窗的走法:

走法訓練視窗sklearn 寫法適合
擴張視窗 expanding起點固定、越來越長(20→40→⋯→100 天)TimeSeriesSplit(n_splits=5)歷史越多越好、規律穩定的資料(題目這種)
滾動視窗 rolling長度固定、整段往右滑(各折都 40 天)TimeSeriesSplit(n_splits=5, max_train_size=40)規律會變(舊資料會過期):促銷、疫情前後
留緩衝 gap訓練與驗證之間空 7 天(兩邊都不用)TimeSeriesSplit(n_splits=5, gap=7)特徵含滯後彙總、或標籤要幾天後才確定

三個一起記的公式:驗證段大小預設 test_size = n ÷ (n_splits+1)(整數除法)——120 天切 5 折,每折驗證 20 天、第一折訓練也是 20 天。金融與銷量預測圈把「沿時間軸一折一折往後考」這整件事叫回測(backtesting)——考題看到這個詞,指的就是本頁的紀律。

相關名詞:test_size 公式回測 backtestinggap 參數

07四個選項收工

A應先 shuffle 全部日期,才能保證驗證資料晚於訓練資料自相矛盾

shuffle 跟「保證晚於」是反義詞——打散正是把順序毀掉的動作。本站實跑:先洗牌再切,五折的訓練集裡分別有 18/39/60/69/93 筆(約九成)日期晚於驗證最早那天——「保證」變成不可能。想保證驗證晚於訓練,唯一的路是不打散+照時間切,跟 A 的處方完全相反。

B每一折都應讓未來資料進入訓練集,以提升預測準確率作弊宣言

未來進訓練集,分數確實會「變好」——實跑就是那個 0.980。但提升的是考卷分數,不是預測能力:上線那天未來資料根本還不存在,模型被打回 -0.949 的原形。這正是時間洩漏的定義——B 把 bug 當成 feature 在推薦。

C一般隨機 K-fold 與 TimeSeriesSplit 對時間洩漏的風險完全相同0% vs 94~100%

實跑逐折數給你看:TimeSeriesSplit 每折「來自未來的訓練樣本」0%(鐵律 max(train) < min(valid));隨機 K-fold 五折是 100%、95%、97%、94%、99%。一個從結構上杜絕時間洩漏、一個幾乎整包都在洩漏——「完全相同」是本題最容易排除的選項。

D應保持時間順序,以 TimeSeriesSplit 或 rolling window 讓驗證期間晚於訓練期間,且不可隨機打散正確

每個片語都有實跑對應:保持時間順序(資料已依日期排序、切分器只認位置);TimeSeriesSplit 或 rolling window(expanding 是預設、加 max_train_size 就是 rolling——都合法);驗證期間晚於訓練期間(五折逐一 assert max(train) < min(valid));不可隨機打散(打散=未來混進訓練,0.980 的假安心 vs -0.949 的真相)。上線是外推,驗證就必須是外推。

回到題目:現在再作答一次

再看一次同一段程式。這次你知道「沒寫 shuffle」不是疏忽,是骨架。

對依日期排序的銷量資料進行驗證,使用下列切分器。下列對此驗證方式的描述何者最正確?

tscv = TimeSeriesSplit(n_splits=5)                   # 驗證段一折一折往後推
for train_idx, valid_idx in tscv.split(X):            # 每折保證 max(train) < min(valid)
    X_train, X_valid = X[train_idx], X[valid_idx]     # 過去當訓練
    y_train, y_valid = y[train_idx], y[valid_idx]     # 未來當驗證

08自我檢測

八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。

09重點整理

  1. 九行程式的分工:第 1–2 行拿工具、第 3–5 行「已依日期排序+轉 NumPy 陣列」(題眼)、第 6 行組 5 折切分器、第 7–9 行逐折用位置名單取出四件套。全程沒有任何 shuffle——這是設計不是疏忽。
  2. 正確答案 D:保持時間順序,以 TimeSeriesSplit 或 rolling window 讓驗證期間晚於訓練期間,且不可隨機打散。一句話:上線是外推,驗證就必須是外推
  3. TimeSeriesSplit 的鐵律:每折 max(train_idx) < min(valid_idx)——實跑五折逐一 assert 通過。預設擴張視窗:120 天切 5 折 → 訓練 20/40/60/80/100 天、驗證各 20 天。
  4. 公式:驗證段大小預設 test_size = n ÷ (n_splits+1)(整數除法);120 ÷ 6 = 20。第一折訓練集也是 20 天。
  5. rolling window:加 max_train_size=40 → 訓練固定 40 天、整段右滑(適合舊規律會過期的資料);gap:加 gap=7 → 訓練與驗證之間空 7 天緩衝(特徵含滯後彙總、標籤延遲確定時用)。
  6. 隨機 K-fold 的時間洩漏(C 的死因):實跑五折,訓練集裡晚於「驗證最早樣本」的比例 100%/95%/97%/94%/99%;TimeSeriesSplit 全部 0%。「風險完全相同」直接被數字否決。
  7. shuffle 與「保證晚於」互斥(A 的死因):實跑先洗牌再切,各折訓練集約九成日期晚於驗證最早日——想保證順序,唯一的路是不打散。
  8. 假安心的代價(B 的死因):同一個隨機森林,隨機 K-fold 平均 R² 0.980、TimeSeriesSplit -1.828(五折全負)、上線預演 -0.949——未來進訓練集「提升」的是考卷分數,不是預測能力。
  9. 隨機 CV 藏住的致命傷:樹模型預測不會超出訓練時見過的範圍——外推不了上升趨勢。內插考卷(隨機切)考不出這件事;時序考卷一考就現形。
  10. 換會外推的模型:Ridge 線性迴歸在時序考卷拿 0.658、上線預演 0.875——正確的驗證不只誠實,還會幫你挑對模型(隨機 CV 下兩個模型都 0.98+,分不出誰能用)。
  11. R² 負值的讀法:比「永遠猜訓練平均」還糟——外推失敗的典型長相,不是程式錯了。
  12. 洩漏家族第三次見:系列第 1 頁(縮放先 fit 全資料)、第 3 頁(SMOTE 先於切分)、本頁(未來混進訓練)——同一條紀律:驗證要長得跟上線一模一樣
完整程式碼