兩個特徵、一條鐵則:站在第 t 列,特徵只能用第 t 列(含)以前的資訊。本站 pandas 3.0.2 實跑(320 天銷售序列):A 的 rolling(7).mean()+shift(1) 全程合規——誠實成績 R² 0.933 / 0.677。B 的 shift(-1) 是偷明天:作弊模型 R² 1.0 / 1.0、係數 [−0.0, 1.0](模型只學會抄答案),而且最後一列 NaN——部署那天特徵不存在。C 的全期平均混了未來、diff 根本不是昨天;D 的 cumsum 只在第 7 天巧合、之後走鐘 32.8 倍。口訣:特徵日曆,不能翻到明天。
分析師想為預測模型建立兩個特徵:「7 日移動平均」與「前一日銷售量」。df 已依日期排序,下列何者正確且不會造成資料洩漏?
# df 欄位: date(已排序), sales # 目標:ma7 = 7 日移動平均、lag1 = 前一日銷售量——且不洩漏未來
想像你是每天傍晚寫「明日銷售預測」的分析師:桌上攤著今天以前的日曆——昨天賣多少(shift(1))、近七天平均如何(rolling(7)),都是合法情報。選項 B 的 shift(-1) 等於偷看「明天的報紙」——回測神準(實跑 R²=1.0),但上線那天報紙根本還沒印:shift(-1) 的最後一列必是 NaN,你唯一真正要預測的那一列,特徵不存在。
另外兩位的死法不同但同樣致命:C 的 mean() 是「全期平均」——寫第 3 天的日報時,那個數字裡混著第 320 天的銷售(統計量洩漏,第 15 頁的老戲在時間軸上重演);而 diff(1) 算的是「今天比昨天多多少」,不是「昨天賣多少」。D 的 cumsum()/7 是開帳以來的流水帳除以 7——跟「近 7 天」只在第 7 天那一天巧合相等,之後一路走鐘;lag1 = df['sales'] 更直接把今天本人當特徵。
rolling(7) 的視窗=第 t−6~t 天(含今天)——前 6 列湊不滿、NaN 起手;第 7 列=前 7 天平均 202.23(手算一字不差)。② shift(k) 的方向就是洩漏判準:正數往下推=看過去(合法)、負數往上拉=偷未來(洩漏)。③ 作弊模型有三個警訊:好到不像話的分數、係數集中在單一特徵、部署日特徵是 NaN——三個 B 全中。點擊後出現漸進式說明:白話說明 → 說清楚一點 → 常見錯誤與考點。
點擊後出現漸進式說明。
五段,一段一段走完。右上角的「看位置」可以把這一段放回完整程式裡看。
df['ma7'] = df['sales'].rolling(7).mean() # 視窗=第 t−6~t 天(含今天)的平均
rolling(7) 在每一列開一扇「往回看 7 天」的視窗(含今天:第 t−6~t 天),.mean() 對窗內取平均——這正是「7 日移動平均」的定義。關鍵在方向:視窗只往過去開,一格未來都不碰。新欄用 df['ma7'] = 回寫進表。
# 實跑:前 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 天就用手上有的」,仍然只用過去。
df['lag1'] = df['sales'].shift(1) # 第 t 列拿到第 t−1 天——「前一日銷售量」
shift(1) 把整欄往下推一列:第 t 列的 lag1 就是第 t−1 天的 sales——「前一日銷售量」的正身。首列沒有昨天可拿 → NaN。實跑驗收:第 8 列的 lag1 = 189.4 = 第 7 天的 sales,逐格吻合。這種「把過去搬到今天這一列」的欄位,行話叫 lag(落後)特徵。
# 判準:站在第 t 列,特徵只能用「第 t 列(含)以前」的資訊拿這把尺量四個選項:A 的 rolling 往回看、shift(1) 拿昨天——全合格;B 的 shift(-1) 把明天拉進今天這一列——直接偷未來;C 的 mean() 在第 3 列就用到第 320 天——統計量混了未來;D 的 lag1=df['sales'] 把今天本人放進特徵——目標若是今天,這叫把答案抄進考卷。
# 實跑(預測明天、250 天訓練 / 63 天測試):A 的 R² = 0.933 / 0.677用 A 的兩個特徵訓練 LinearRegression 預測「明天」:訓練 R² 0.933、測試 0.677——測試期在更遠的未來、難一點很正常,這才是誠實模型該有的長相。對照組 B 的「滿分」(1.0 / 1.0)反而是最大的警訊——04 節的作弊模型偵測器整場伺候。
把前 8 天的實跑資料攤開,看每一列的視窗框到哪幾天:
同一個 shift、一個正負號之差——把兩個方向都真的跑一遍:
shift(-1) 的最後一列必是 NaN——因為「明天」還沒發生。而部署上線那天,你要預測的正是最後一列:回測時神準的特徵,在唯一真正需要它的那一列不存在。這就是時序洩漏最殘酷的長相:考試滿分、上場交白卷。拿 A 與 B 的特徵各訓練一個模型預測「明天」(250 天訓練、63 天測試),成績與係數全公開:
C 和 D 的四個零件,每一個都真的跑給你看:
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'] 連位移都沒有——今天本人直接進特徵。時序特徵的一張總表——語意、方向、洩漏風險一次看清:
| 寫法 | 語意 | 用到未來嗎 |
|---|---|---|
| shift(k),k>0 | k 天前的值(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)
兩個特徵語意全對、方向全對:rolling 的視窗只往過去開(第 7 列 = 前 7 天平均 202.23,手算一字不差)、shift(1) 拿昨天(第 8 列 = 189.4 逐格吻合)。誠實成績 R² 0.933 / 0.677——不完美,但真的。站在第 t 列、只用 ≤ t 的資訊——鐵則全程成立。
一個負號毀掉一切:shift(-1) 把整欄往上拉——第 t 列拿到第 t+1 天。作弊模型實跑 R² 1.0 / 1.0、係數 [−0.0, 1.0]——模型只學會「抄答案」;而 shift(-1) 的最後一列必是 NaN——部署那天(你唯一要預測的那列)特徵不存在。考試滿分、上場交白卷。
兩個零件兩種死法:mean() 是全期平均純量 357.7 廣播成常數欄——第 3 天就用到第 320 天(實跑前 100 天平均 247.76,差距就是未來的資訊;第 15 頁統計量洩漏的時序版);diff(1) 是「今天−昨天」的變化量(實跑 0.4),跟「前一日銷售量」(189.4)語意完全不同。
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=拿昨天
八題,全部都是本題的延伸。答錯會直接告訴你錯在哪。
shift(1).rolling(7).mean();本題任務是預測未來,A 合法。「時序特徵工程與洩漏」是中級科目二大數據處理分析與應用的資料準備招牌考點,rolling、shift、洩漏判準輪流出場。最常見的六種問法:① 給四段程式選「正確且不洩漏」(本題——語意與方向雙關卡);② 問 rolling(n) 的視窗框住哪幾天(第 t−n+1~t,含今天;前 n−1 列 NaN);③ 問 shift 正負的差別(正看過去、負偷未來);④ 問 shift(-1) 拿來當特徵會怎樣(回測神準、部署日 NaN);⑤ 問 diff 與 shift 的差別(變化量 vs 昨天值);⑥ 問怎麼偵測洩漏(分數好到不像話、係數集中、部署日特徵缺席)。一句口訣:shift 正看過去、負偷未來——特徵日曆不能翻到明天。
含今天:第 t 列的視窗是第 t−6~t 天,共 7 天——「往回看、含當下」。實跑逐格驗收:第 7 列 = mean(第 1~7 天) = 202.23(與手算一字不差)、第 8 列滑一格 = mean(第 2~8 天) = 200.77。前 6 列因湊不滿 7 天而是 NaN——pandas 選擇誠實留白而不是偷拿未來湊數;想「不足就用手上有的」可加 min_periods=1,它仍然只用過去。「含今天」在本題合法(任務是預測未來、今天已發生);但若目標是「今天」本身,含今天的視窗就藏了 1/7 的答案——見「嚴格版」那一折。
方向相反:shift(1) 把整欄往下推一列——第 t 列拿到第 t−1 天(昨天,已發生、合法);shift(-1) 把整欄往上拉一列——第 t 列拿到第 t+1 天(明天,還沒發生、洩漏)。實跑對照:shift(1) 首列 NaN(第 1 天沒有昨天);shift(-1) 末列 NaN(最後一天沒有明天)——兩個 NaN 的位置就洩露了時間方向。順帶一提:shift(-1) 不是一無是處——它是造「目標欄」的標準工具(y = sales.shift(-1) =「明天的銷售」),放在等號左邊當答案合法、放進特徵當 X 就是作弊。
三幕劇(全部實跑)。第一幕・回測神話:R² 訓練/測試 1.0 / 1.0——因為特徵欄就是目標欄本人。第二幕・係數招供:LinearRegression 學出係數 [−0.0, 1.0]、截距 0——翻譯:「ma7 我不需要,答案直接抄 lag1」。第三幕・部署斷頭台:上線那天要預測最後一列,而 shift(-1) 在最後一列必是 NaN(明天還沒發生)——神準的特徵在唯一需要它的地方不存在。對照 A 的誠實成績 0.933/0.677:分數好到不像話,第一件事不是慶祝、是查洩漏。
兩個零件兩種死法。mean():回傳純量(全期平均 357.7),指派給欄位時廣播成整欄同一個值——問題不只「不是移動平均」,更是統計量洩漏:寫第 3 天的特徵時,357.7 裡混著第 320 天的銷售——實跑「前 100 天平均」只有 247.76,那 110 元的差距就是未來的資訊(序列有上升趨勢)。這是第 15 頁「scaler 只能 fit 訓練集」在時間軸上的重演:全樣本統計量=偷看未來。diff(1):算的是 sales(t) − sales(t−1)——「今天比昨天多多少」(實跑第 8 列 = 0.4),跟「昨天賣多少」(189.4)語意完全不同;diff 本身不洩漏、也是常用特徵,但它不是題目要的 lag1。
巧合的數學:第 7 天的 cumsum = 前 7 天合計,除以 7 正好是前 7 天平均——與 ma7 完全相等(實跑都是 202.23)。但這是唯一的巧合日:第 8 天起 cumsum 是「8 天合計」「9 天合計」……除以 7 之後越長越大——實跑第 100 天 3539.4 vs 真 ma7 271.9(13 倍)、第 320 天 16352.0 vs 498.9(32.8 倍)。cumsum 是「開帳以來的流水帳」、rolling 是「固定寬度的視窗」——一字之差,語意天差地遠。至於 lag1 = df['sales']:連 shift 都沒有,今天本人直接進特徵——若目標是今天,這叫把答案抄進考卷。順帶:想要「從第 1 天到今天的累積平均」,正規寫法是 expanding().mean()——合法、但那也不是題目要的 7 日視窗。
看目標是誰。本題任務是「預測(未來)」——站在第 t 列預測 t+1,特徵用到第 t 天(含今天)完全合法,A 直接對。但如果目標是「今天」(例如用同期特徵回填今天的銷售),含今天的 ma7 就藏著答案的 1/7——本站的實跑鐵證:給模型 ma7(含今)+前 6 天 lag,LinearRegression 代數還原出今天——R² = 1.000000、係數恰好 [7, −1, −1, −1, −1, −1, −1](它解出了 s(t) = 7×ma7 − Σ前六天);嚴格版(shift(1).rolling(7).mean(),視窗 t−7~t−1)同陣容只有 0.9847。考場判斷法:先問目標的時間點,再決定特徵日曆的截止日——截止日永遠要早於目標。
三條線。「洩漏家族第五案」:第 1/3 頁(測試集參與前處理)、第 10 頁(KFold 洗牌洩漏 94~100%)、第 15 頁(scaler 統計量洩漏)、第 21 頁(補值統計量的紀律)——本頁把戰場推進到特徵工程層:shift(-1) 與全期 mean() 都是「未來混進特徵」。「雙保險」:特徵層守「不用未來」(本頁)+驗證層守「過去訓練、未來驗證」(第 10 頁 TimeSeriesSplit)——兩層都守住,時序洩漏才真的絕跡。「不報錯的錯」:B/C/D 全都跑得動、欄位漂漂亮亮生出來——洩漏從來不是語法錯誤,是時間方向的錯;跟第 9/18/21 頁同款「程式跑得動 ≠ 語意正確」。