模型上線後只做一件事:拿過去,預測未來。驗證如果不照這個劇本演——隨機打散、讓未來混進訓練——分數就是假的。本頁用一份會漲的銷量資料實跑給你看:同一個隨機森林,隨機 K-fold 打 0.980、TimeSeriesSplit 卻是 -1.828(比亂猜還糟)——差距就是「偷看未來」的幅度。答案 D 的每個字(保持時間順序、rolling window、驗證晚於訓練、不可打散)都會在四個實驗室裡逐一驗證。
對依日期排序的銷量資料進行驗證,使用下列切分器。下列對此驗證方式的描述何者最正確?
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 月驗證,一折一折往後推。
隨機打散是什麼?是把下個月的考卷混進這個月的講義。模型背了未來的答案再去考未來,分數當然漂亮——但那是作弊的成績,上線立刻現形。
max(train_idx) < min(valid_idx)——訓練最晚的一天,都早於驗證最早的一天(本站實跑五折逐一 assert 過)。點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
九行,一段一段走完。右上角的「看位置」可以把這一段放回完整程式裡看。
import numpy as np # 整箱搬進來,取小名 np
numpy 這次的任務不是造資料,是把資料轉成「能用整數位置取值」的陣列(第 4、5 行)——這是後面 X[train_idx] 那種取法的前提。
from sklearn.model_selection import TimeSeriesSplit # 「照時間排隊」的切分器
sklearn.model_selection 是「怎麼切資料」的抽屜:KFold、train_test_split、GridSearchCV(系列第 4 頁)都住這裡。TimeSeriesSplit 是專為時間資料設計的成員——它跟 KFold 最大的差別:切出來的每一折,驗證都排在訓練之後,而且從頭到尾不洗牌。
名字就是提示:Time(時間)Series(序列)Split(切分)——看到題目說「依日期排序」,就該想到它。
# 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] 才能一次按一串位置取值。
asarray 跟 array 的差別:已經是陣列就不再複製、原樣返回——「確保是陣列」的禮貌寫法。若 X 是 pandas DataFrame,直接 X[train_idx] 會被當成「取欄名」而出錯——這就是題目特別聲明「明確轉為 NumPy arrays」的原因(DataFrame 派的寫法是 X.iloc[train_idx])。
tscv = TimeSeriesSplit(n_splits=5) # 5 折:驗證段一折一折往後推
n_splits=5:切 5 折。每折的驗證段大小預設是 n ÷ (n_splits+1)——本站實跑 120 天資料:驗證段 120 ÷ 6 = 20 天,五折的訓練集依序是 20、40、60、80、100 天(擴張視窗:訓練集一折比一折長,驗證段緊跟在後面)。
max_train_size;想留緩衝,加 gap(02 節實驗室都能玩)。for train_idx, valid_idx in tscv.split(X): # 每圈吐一對位置名單
tscv.split(X) 每一圈吐出一對位置名單:train_idx(這一折用來訓練的位置)與 valid_idx(用來驗證的位置)——for 迴圈用「雙變數解包」一次接住兩個。迴圈跑 5 圈=5 折。
注意 split 只給「位置編號」不給資料本身——像發座位表不發考卷。真正取資料是下一行的事。
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 上評分(完整流程見「完整程式碼」)。
把 120 天的銷量資料交給題目的切分器,五折的長相實跑畫出來——藍色是訓練、紅色是驗證。三種走法切換著看:
assert max(train_idx) < min(valid_idx)——訓練最晚的一天,永遠早於驗證最早的一天。這就是選項 D 說的「驗證期間晚於訓練期間」的程式版本。驗證段大小 = 120 ÷ (5+1) = 20 天,五折驗證段首尾相接、一路推到最後 20 天。選項 C 說「隨機 K-fold 與 TimeSeriesSplit 對時間洩漏的風險完全相同」。把兩個切分器都對準同一份 120 天資料,看第 3 折的訓練(藍)與驗證(紅)落在時間軸的哪裡:
洩漏會付出什麼代價?拿這份 120 天銷量(有上升趨勢+週期)做特徵(昨天、上週同日、星期幾),同一個模型考兩種考卷,再用「上線預演」(前 100 天訓練、最後 20 天盲測)當裁判:
為什麼兩張考卷差這麼多?把銷量點畫在時間軸上(本站實跑資料:一路向上的趨勢+週期起伏),看兩種切法把「驗證」放在哪裡:
選項 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)——考題看到這個詞,指的就是本頁的紀律。
shuffle 跟「保證晚於」是反義詞——打散正是把順序毀掉的動作。本站實跑:先洗牌再切,五折的訓練集裡分別有 18/39/60/69/93 筆(約九成)日期晚於驗證最早那天——「保證」變成不可能。想保證驗證晚於訓練,唯一的路是不打散+照時間切,跟 A 的處方完全相反。
未來進訓練集,分數確實會「變好」——實跑就是那個 0.980。但提升的是考卷分數,不是預測能力:上線那天未來資料根本還不存在,模型被打回 -0.949 的原形。這正是時間洩漏的定義——B 把 bug 當成 feature 在推薦。
實跑逐折數給你看:TimeSeriesSplit 每折「來自未來的訓練樣本」0%(鐵律 max(train) < min(valid));隨機 K-fold 五折是 100%、95%、97%、94%、99%。一個從結構上杜絕時間洩漏、一個幾乎整包都在洩漏——「完全相同」是本題最容易排除的選項。
每個片語都有實跑對應:保持時間順序(資料已依日期排序、切分器只認位置);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] # 未來當驗證
八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。
max(train_idx) < min(valid_idx)——實跑五折逐一 assert 通過。預設擴張視窗:120 天切 5 折 → 訓練 20/40/60/80/100 天、驗證各 20 天。max_train_size=40 → 訓練固定 40 天、整段右滑(適合舊規律會過期的資料);gap:加 gap=7 → 訓練與驗證之間空 7 天緩衝(特徵含滯後彙總、標籤延遲確定時用)。「時間序列驗證」是中級科目二資料準備 × 科目三模型評估的交叉考點,近年出題率明顯上升。最常見的六種問法:① 給一段 TimeSeriesSplit 程式問「哪個描述正確」(本題這種——答案永遠往「保持順序、驗證晚於訓練、不打散」靠);② 問時序資料能不能用一般 K-fold/要不要 shuffle(不能、不要——未來會混進訓練);③ 問 expanding 與 rolling window 的差別(起點固定變長 vs 長度固定右滑);④ 問 gap 參數的用途(特徵含滯後或標籤延遲時留緩衝);⑤ 給 n 與 n_splits 問各折大小(test_size = n ÷ (n_splits+1));⑥ 問為什麼隨機切分的分數比較高(時間洩漏=內插考卷,分數虛高不可信)。一句口訣:過去訓練、未來驗證、不可洗牌——上線是外推,驗證就要考外推。
關鍵在樣本之間獨不獨立。一般表格資料(例如 178 瓶酒)每筆互相獨立,洗牌不破壞任何結構,隨機 K-fold 公平又高效。時間序列的樣本彼此相依(自相關):今天的銷量跟昨天高度相關、整條序列還有趨勢與季節性。shuffle 之後兩件事同時發生:① 訓練集混進驗證期之後的「未來」樣本——模型等於先看過答案的鄰居;② 驗證從「外推」變「內插」——每個驗證點左右都有訓練點撐腰,考不出上線真正要做的事。本站實跑:shuffle 讓隨機森林的 R² 從 -1.828(誠實)膨脹到 0.980(假象)。
預設驗證段大小 test_size = n ÷ (n_splits+1)(整數除法),驗證段從資料尾端往前排 5 段、首尾相接;每折的訓練集是「該驗證段之前的全部資料」(擴張視窗)。實跑 120 天:test_size = 20,五折為訓練 0~19/驗證 20~39、訓練 0~39/驗證 40~59⋯⋯訓練 0~99/驗證 100~119——訓練集 20→40→60→80→100 天越來越長,且每折 max(train_idx) < min(valid_idx)。可用 test_size= 參數改驗證段大小。注意它只認「位置」不認日期欄——資料必須先依日期排序,這正是題目第 3 行註解的意義。
擴張視窗(TimeSeriesSplit 預設):訓練起點固定在第 1 天,一折比一折長——歷史越多學越穩,適合規律穩定的資料。滾動視窗(加 max_train_size):訓練長度固定、整段往右滑——實跑 max_train_size=40 的五折訓練集是 0~19(不足 40 就全用)、0~39、20~59、40~79、60~99,各折最多 40 天。適合「舊資料會過期」的場景:消費習慣改變、促銷檔期、疫情前後——太久以前的規律反而是雜訊。兩者都保持「驗證晚於訓練」,都是選項 D 認可的合法時序驗證;rolling 另一個好處是各折訓練量一致,分數之間更可比。
當「訓練最後一天」與「驗證第一天」靠太近會偷渡資訊時。兩個典型:① 特徵含滯後彙總——例如特徵用了「過去 7 天平均銷量」,驗證第 1 天的這個特徵會用到訓練期最後幾天的資料,訓練與驗證間空 7 天(gap=7)才乾淨;② 標籤延遲確定——例如「30 天內是否退貨」,訓練期最後 30 天的標籤在切分當下其實還沒定案。實跑 gap=7:每折訓練提早 7 天收尾(第一折訓練只剩 0~12),驗證段位置不變——犧牲一點資料換取乾淨的邊界。
本站實跑(120 天上升趨勢銷量、特徵=昨天/上週同日/星期幾):隨機森林在隨機 K-fold 拿 0.980、在 TimeSeriesSplit 拿 -1.828(五折 -1.116/-1.914/-2.820/-2.341/-0.949,全負)、上線預演 -0.949。差距的來源有二:① 隨機切讓每個驗證點的時間鄰居都在訓練集(內插);② 樹模型的預測不會超出訓練時見過的數值範圍——銷量一路向上,每一折的驗證期水位都比訓練期高,隨機森林只能預測「訓練期的高點」,於是比猜平均還糟。Ridge 線性迴歸會外推,同一份時序考卷拿 0.658、上線預演 0.875——時序驗證不只誠實,還會幫你發現「這個資料該用會外推的模型」。
不是,負值是合法輸出。R² = 1 −(模型誤差平方和 ÷「永遠猜驗證集平均」的誤差平方和)——模型比「猜平均」還糟時分子大於分母,R² 就轉負,沒有下限。外推失敗是最常見的成因:實跑隨機森林在驗證期只能預測訓練期的水位,銷量卻已經漲上去,誤差比猜平均大 2~3 倍,R² 落在 -1 到 -3。看到負 R² 的正確反應不是改程式,是回頭檢查:模型會不會外推?特徵有沒有捕捉趨勢?(例如加入「差分」或「距離起點的天數」特徵,或改用線性/統計模型。)
是——這是本系列第三次遇到洩漏家族。第 1 頁:StandardScaler 先對全部資料 fit 再切分——測試集的分布洩進訓練(解法:縮放進 Pipeline、折內 fit)。第 3 頁:SMOTE 在切分前做——合成樣本帶著測試集的資訊(解法:只對訓練折做)。本頁:隨機打散讓未來混進訓練(解法:TimeSeriesSplit/rolling window)。三個場景一條紀律:驗證環境必須長得跟上線環境一模一樣——上線時你拿不到的東西(測試分布、少數類全貌、未來資料),驗證時就一樣不准碰。