🗺️ AI 學習與考證地圖
中級科目二程式實戰 · 時序特徵工程

時間特徵的鐵則:
只能用「已經發生」的事

兩個特徵、一條鐵則:站在第 t 列,特徵只能用第 t 列(含)以前的資訊。本站 pandas 3.0.2 實跑(320 天銷售序列):Arolling(7).mean()shift(1) 全程合規——誠實成績 R² 0.933 / 0.677Bshift(-1)偷明天:作弊模型 R² 1.0 / 1.0、係數 [−0.0, 1.0](模型只學會抄答案),而且最後一列 NaN——部署那天特徵不存在C 的全期平均混了未來、diff 根本不是昨天;D 的 cumsum 只在第 7 天巧合、之後走鐘 32.8 倍。口訣:特徵日曆,不能翻到明天

閱讀模式

00題目

分析師想為預測模型建立兩個特徵:「7 日移動平均」與「前一日銷售量」。df 已依日期排序,下列何者正確且不會造成資料洩漏?

# df 欄位: date(已排序), sales
# 目標:ma7 = 7 日移動平均、lag1 = 前一日銷售量——且不洩漏未來

先說:寫日報的分析師與明天的報紙

想像你是每天傍晚寫「明日銷售預測」的分析師:桌上攤著今天以前的日曆——昨天賣多少(shift(1))、近七天平均如何(rolling(7)),都是合法情報。選項 B 的 shift(-1) 等於偷看「明天的報紙」——回測神準(實跑 R²=1.0),但上線那天報紙根本還沒印:shift(-1) 的最後一列必是 NaN,你唯一真正要預測的那一列,特徵不存在。

另外兩位的死法不同但同樣致命:Cmean() 是「全期平均」——寫第 3 天的日報時,那個數字裡混著第 320 天的銷售(統計量洩漏,第 15 頁的老戲在時間軸上重演);而 diff(1) 算的是「今天比昨天多多少」,不是「昨天賣多少」。Dcumsum()/7 是開帳以來的流水帳除以 7——跟「近 7 天」只在第 7 天那一天巧合相等,之後一路走鐘;lag1 = df['sales'] 更直接把今天本人當特徵。

三個先立好的事實(全部實跑): rolling(7) 的視窗=第 t−6~t 天(含今天)——前 6 列湊不滿、NaN 起手;第 7 列=前 7 天平均 202.23(手算一字不差)。 shift(k) 的方向就是洩漏判準:正數往下推=看過去(合法)、負數往上拉=偷未來(洩漏) 作弊模型有三個警訊:好到不像話的分數、係數集中在單一特徵、部署日特徵是 NaN——三個 B 全中。

先點開看:rolling 滑動視窗shift 的正負方向時序洩漏

不熟時序特徵?你需要先認識下列名詞

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

視窗的世界
時間特徵是什麼rolling 滑動視窗視窗含今天嗎前 6 列 NaN
位移的方向
shift 位移shift 的正負方向lag 落後特徵shift(-1)=偷明天
洩漏與偵測
時序洩漏部署日的 NaN作弊模型三警訊R²=1.0 照妖鏡
家族與嚴格版
expanding 累積窗diff 差分cumsum 流水帳全期平均的洩漏嚴格版再 shiftTimeSeriesSplit 呼應

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

點擊後出現漸進式說明

起手式
import pandas as pddf['ma7'] = 新欄回寫點點接龍已排序的前提
NaN 與建模
NaN 起手的意義建模前 dropna前段訓練後段測試
對照組工具
LinearRegression係數會說話shift(-1) 的語法
選項法醫室
C 的純量廣播D 的 lag1=本人

01逐行拆解:正解 A 的兩行與一條鐵則

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

第 1 段rolling(7).mean():開一扇往回看的視窗
df['ma7'] = df['sales'].rolling(7).mean()   # 視窗=第 t−6~t 天(含今天)的平均

rolling(7) 在每一列開一扇「往回看 7 天」的視窗(含今天:第 t−6~t 天),.mean() 對窗內取平均——這正是「7 日移動平均」的定義。關鍵在方向:視窗只往過去開,一格未來都不碰。新欄用 df['ma7'] = 回寫進表。

相關名詞:rolling 滑動視窗視窗含今天嗎df['ma7'] = 新欄回寫

第 2 段NaN 起手與滑動:實跑逐格驗收
# 實跑:前 6 列 NaN(湊不滿 7 天);第 7 列 = mean(第 1~7 天) = 202.23
# 第 8 列視窗滑一格 = mean(第 2~8 天) = 200.77——這就是「移動」二字

兩個實跑細節: 前 6 列是 NaN——資料不夠 7 天就誠實留白(不硬算、不偷看未來湊數); 第 7 列 = 前 7 天平均 202.23,與手算 mean(s[0:7]) 一字不差;第 8 列視窗滑一格變成第 2~8 天。想讓前 6 列也有值?rolling(7, min_periods=1)——「不足 7 天就用手上有的」,仍然只用過去。

相關名詞:前 6 列 NaNNaN 起手的意義

第 3 段shift(1):整欄往下推一列=拿到昨天
df['lag1'] = df['sales'].shift(1)   # 第 t 列拿到第 t−1 天——「前一日銷售量」

shift(1) 把整欄往下推一列:第 t 列的 lag1 就是第 t−1 天的 sales——「前一日銷售量」的正身。首列沒有昨天可拿 → NaN。實跑驗收:第 8 列的 lag1 = 189.4 = 第 7 天的 sales,逐格吻合。這種「把過去搬到今天這一列」的欄位,行話叫 lag(落後)特徵

相關名詞:shift 位移lag 落後特徵

第 4 段洩漏判準:一句話驗遍四個選項
# 判準:站在第 t 列,特徵只能用「第 t 列(含)以前」的資訊

拿這把尺量四個選項:A 的 rolling 往回看、shift(1) 拿昨天——全合格B 的 shift(-1) 把明天拉進今天這一列——直接偷未來C 的 mean() 在第 3 列就用到第 320 天——統計量混了未來D 的 lag1=df['sales'] 把今天本人放進特徵——目標若是今天,這叫把答案抄進考卷

「不報錯的錯」家族又聚首:B、C、D 三段程式全都跑得動——沒有任何警告,欄位漂漂亮亮地生出來了。洩漏從來不是語法錯誤,是時間方向的錯——所以要用判準去驗,不能等直譯器救你。

相關名詞:時序洩漏shift 的正負方向

第 5 段誠實成績單:不完美,但真的
# 實跑(預測明天、250 天訓練 / 63 天測試):A 的 R² = 0.933 / 0.677

用 A 的兩個特徵訓練 LinearRegression 預測「明天」:訓練 R² 0.933、測試 0.677——測試期在更遠的未來、難一點很正常,這才是誠實模型該有的長相。對照組 B 的「滿分」(1.0 / 1.0)反而是最大的警訊——04 節的作弊模型偵測器整場伺候。

一句話記住本題:shift 正看過去、負偷未來;rolling 開視窗、cumsum 是流水帳——特徵日曆不能翻到明天

相關名詞:LinearRegression前段訓練後段測試建模前 dropna

02視窗放大鏡:rolling 到底框住了誰

把前 8 天的實跑資料攤開,看每一列的視窗框到哪幾天:

互動實驗室:視窗放大鏡前 6 列 NaN → 第 7 列開窗 → 逐日滑動——實跑逐格
本站實跑判決:第 7 列 ma7 = 202.23 = mean(200.0, 199.8, 215.1, 198.7, 211.2, 201.4, 189.4)——與手算一字不差;第 8 列視窗滑一格 = 200.77。整個過程視窗只往過去開:第 t 列框的是第 t−6~t 天,一格明天都不碰——這就是 A 不洩漏的理由。

相關名詞:rolling 滑動視窗視窗含今天嗎前 6 列 NaN

03時間方向儀:shift 的正負是生死線

同一個 shift、一個正負號之差——把兩個方向都真的跑一遍:

互動實驗室:時間方向儀shift(1) 看過去 vs shift(-1) 偷未來——部署日見真章
斷頭台證據(實跑):shift(-1)最後一列必是 NaN——因為「明天」還沒發生。而部署上線那天,你要預測的正是最後一列:回測時神準的特徵,在唯一真正需要它的那一列不存在。這就是時序洩漏最殘酷的長相:考試滿分、上場交白卷

相關名詞:shift 的正負方向shift(-1)=偷明天部署日的 NaN

04作弊模型偵測器:R²=1.0 是照妖鏡

拿 A 與 B 的特徵各訓練一個模型預測「明天」(250 天訓練、63 天測試),成績與係數全公開:

互動實驗室:作弊模型偵測器誠實 0.677 vs 作弊 1.000——係數會說話
係數會說話(實跑):B 模型的係數是 [−0.0, 1.0]、截距 0——翻譯成白話:「ma7 我不需要,答案直接抄 lag1」(因為 B 的 lag1 就是明天本人)。A 模型的係數 [0.707, 0.278] 則是正常分工——兩個特徵各出一份力。分數好到不像話+係數集中在單一特徵=先查洩漏,這是實務上抓洩漏的第一直覺。

相關名詞:作弊模型三警訊R²=1.0 照妖鏡係數會說話

05假特徵驗屍臺:C 與 D 的零件逐一解剖

C 和 D 的四個零件,每一個都真的跑給你看:

互動實驗室:假特徵驗屍臺全期平均混未來、diff≠昨天、cumsum 走鐘、lag1=本人
驗屍摘要(實跑):C 的 mean() = 357.7 廣播成整欄同一個值——而「前 100 天平均」只有 247.76:那 110 元的差距就是未來的資訊(序列有上升趨勢);diff(1) 在第 8 列是 0.4(189.8−189.4 的變化量),跟「昨天 189.4」差了整整一個世界。D 的 cumsum()/7 只在第 7 天那一天巧合等於 ma7(202.23——七個值的流水帳÷7 剛好是前七天平均),之後一路走鐘:第 100 天差 13 倍、第 320 天差 32.8 倍;而 lag1 = df['sales'] 連位移都沒有——今天本人直接進特徵

相關名詞:全期平均的洩漏diff 差分cumsum 流水帳D 的 lag1=本人

06考場加碼:時間特徵速查表與嚴格版

時序特徵的一張總表——語意、方向、洩漏風險一次看清:

寫法語意用到未來嗎
shift(k),k>0k 天前的值(lag 特徵)×(合法)
shift(-k)k 天後的值(造「目標」用,不能當特徵)○(特徵用=洩漏)
rolling(n).mean()近 n 天平均(含今天;第 t−n+1~t)×(合法)
expanding().mean()從第 1 天到今天的累積平均×(合法)
diff(1)今天 − 昨天(變化量,不是昨天)×(但語意別搞混)
cumsum()開帳以來合計(流水帳)×(但 ÷7 不是 7 日平均)
整欄 mean()/std()全期統計量(純量)○(第 3 天就用到第 320 天)
# 嚴格版:若目標是「今天」,連 rolling 都要先 shift——特徵日曆只寫到昨天
df['ma7_strict'] = df['sales'].shift(1).rolling(7).mean()   # 昨天往回 7 天(t−7~t−1)
嚴格版的實跑鐵證:目標若是「今天」,A 的 ma7(含今天)藏著答案的 1/7——給模型 ma7(含今)+前 6 天 lag,LinearRegression 直接代數還原出今天:R² = 1.000000、係數恰好 [7, −1, −1, −1, −1, −1, −1](解出 s(t) = 7×ma7 − Σ前六天);嚴格版同陣容只有 0.9847。本題的任務是「預測(未來)」——A 用到今天為止,合法;但目標一旦是「今天」,就要換嚴格版。這與第 10 頁是雙保險:特徵層守「不用未來」(本頁)、驗證層守「過去訓練未來驗證」(TimeSeriesSplit)——兩層都守住,洩漏才真的絕跡。

相關名詞:嚴格版再 shiftTimeSeriesSplit 呼應expanding 累積窗

07四個選項收工

Arolling(7).mean() + shift(1)正確

兩個特徵語意全對、方向全對:rolling 的視窗只往過去開(第 7 列 = 前 7 天平均 202.23,手算一字不差)、shift(1) 拿昨天(第 8 列 = 189.4 逐格吻合)。誠實成績 R² 0.933 / 0.677——不完美,但真的。站在第 t 列、只用 ≤ t 的資訊——鐵則全程成立。

Brolling(7).mean() + shift(-1)偷明天

一個負號毀掉一切:shift(-1) 把整欄往上拉——第 t 列拿到第 t+1 天。作弊模型實跑 R² 1.0 / 1.0、係數 [−0.0, 1.0]——模型只學會「抄答案」;而 shift(-1) 的最後一列必是 NaN——部署那天(你唯一要預測的那列)特徵不存在。考試滿分、上場交白卷

Cmean() + diff(1)全期平均混未來

兩個零件兩種死法:mean() 是全期平均純量 357.7 廣播成常數欄——第 3 天就用到第 320 天(實跑前 100 天平均 247.76,差距就是未來的資訊;第 15 頁統計量洩漏的時序版);diff(1) 是「今天−昨天」的變化量(實跑 0.4),跟「前一日銷售量」(189.4)語意完全不同

Dcumsum()/7 + df['sales']流水帳+本人

cumsum()/7 是開帳以來流水帳除以 7——只在第 7 天巧合等於 ma7(202.23),之後一路走鐘:第 100 天差 13 倍、第 320 天差 32.8 倍lag1 = df['sales'] 連位移都沒有——今天本人直接當特徵:目標若是今天,等於把答案抄進考卷。兩個零件都與題目要的特徵無關。

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

再看一次同一道題。這次你手上有一句口訣了:shift 正看過去、負偷未來——特徵日曆不能翻到明天

df['ma7'] = df['sales'].rolling(7).mean()   # 視窗只往過去開(含今天)
df['lag1'] = df['sales'].shift(1)          # 正的 shift=拿昨天

08自我檢測

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

09重點整理

  1. 兩個特徵的正身:「7 日移動平均」= rolling(7).mean()(視窗第 t−6~t 天);「前一日銷售量」= shift(1)(整欄往下推一列)——答案 A。
  2. 洩漏判準一句話:站在第 t 列,特徵只能用「第 t 列(含)以前」的資訊——特徵日曆不能翻到明天。
  3. rolling 實跑:前 6 列 NaN(湊不滿 7 天、誠實留白);第 7 列 = 前 7 天平均 202.23 與手算一字不差;第 8 列滑一格 = 200.77;min_periods=1 可讓前 6 列用「手上有的」——仍只用過去。
  4. shift 的正負是生死線:shift(1) 第 t 列拿到昨天(合法);shift(-1) 第 t 列拿到明天(洩漏)——一個負號毀掉一切。
  5. B 的作弊成績(實跑):R² 1.0 / 1.0、係數 [−0.0, 1.0]、截距 0——模型只學會「抄答案」(B 的 lag1 就是目標本人)。
  6. B 的斷頭台(實跑):shift(-1) 最後一列必是 NaN——部署那天(你唯一要預測的那列)特徵不存在:考試滿分、上場交白卷
  7. A 的誠實成績(實跑):R² 0.933 / 0.677、係數 [0.707, 0.278] 正常分工——不完美,但真的。
  8. C 的死因(實跑):mean() 全期平均 357.7 廣播成常數欄——前 100 天平均只有 247.76,差距就是未來(第 15 頁統計量洩漏的時序版);diff(1) 是「今昨差」0.4,不是「昨天」189.4。
  9. D 的死因(實跑):cumsum()/7 只在第 7 天巧合等於 ma7、第 320 天走鐘 32.8 倍;lag1=df['sales'] 是今天本人——答案抄進考卷。
  10. 嚴格版(實跑鐵證):目標若是「今天」,ma7(含今)+前 6 天 lag 可代數還原答案(R²=1.000000、係數 [7, −1×6])——此時要用 shift(1).rolling(7).mean();本題任務是預測未來,A 合法。
  11. 雙保險:特徵層守「不用未來」(本頁)+驗證層守「過去訓練未來驗證」(第 10 頁 TimeSeriesSplit)——洩漏家族第五案落網。
  12. 考場口訣shift 正看過去、負偷未來;rolling 開視窗、cumsum 是流水帳——特徵日曆不能翻到明天
完整程式碼