據(jù)泄漏排查)
DataFrame 的時間序列處理我前后踩過三輪坑才算真正用順。第一輪是把時間列當(dāng)普通字符串排序結(jié)果亂序數(shù)據(jù)喂進(jìn)模型指標(biāo)飄得沒法復(fù)現(xiàn)第二輪是重采樣窗口邊界沒對齊日匯總和原始分鐘數(shù)據(jù)對不上賬排查到半夜第三輪最狠構(gòu)造的特征里混進(jìn)了未來信息離線評估漂亮得離譜實際跑起來直接失效。這三個坑基本覆蓋了時間序列 DataFrame 的絕大多數(shù)陷阱也正好說明了為什么這一塊必須單獨拎出來講。它是 DataFrame 系列的收尾篇也是銜接機器學(xué)習(xí)前置技術(shù)棧的關(guān)鍵一環(huán)——前面講的索引、切片、合并、聚合本質(zhì)都是把數(shù)據(jù)擺整齊而時間序列這一篇要解決的是讓數(shù)據(jù)帶著時間語義動起來。如果你正在做設(shè)備采集分析、業(yè)務(wù)指標(biāo)監(jiān)控、銷量預(yù)測或者準(zhǔn)備把一張帶時間戳的表轉(zhuǎn)成模型能吃的特征矩陣這篇的內(nèi)容基本可以照著抄。下文所有代碼都基于常規(guī)實踐整理環(huán)境是 Python 3.10 以上配 Pandas 2.x2.x 和 1.x 在部分時間序列行為上有差異我會在涉及的地方標(biāo)出來。1. 時間序列 DataFrame 的底層邏輯與設(shè)計取舍1.1 為什么時間列不能只當(dāng)普通列用很多人第一次處理時間數(shù)據(jù)的直覺是時間不就是一列字符串嗎排序、篩選、分組照做就行。這個做法在數(shù)據(jù)量小、需求單純的時候確實能跑一旦涉及按天匯總、取最近七天均值、補齊缺失的分鐘點這類操作就會立刻暴露出問題。原因在于 Pandas 的時間序列能力并不是掛在列上的而是掛在索引上的——DatetimeIndex是一個帶有頻率語義的特殊索引對象它能回答下一個時間點是誰這種普通列回答不了的問題。你寫df.resample(D)Pandas 需要知道當(dāng)前索引的粒度、有沒有空洞、邊界怎么取這些都依賴索引是DatetimeIndex這個前提。把時間留在普通列里resample直接報錯rolling的時間窗口寫法也用不了只能退化成按行數(shù)滾動的近似方案精度和語義雙輸。我自己習(xí)慣的做法是原始數(shù)據(jù)讀進(jìn)來之后第一時間把時間列轉(zhuǎn)成datetime64[ns]然后set_index提到索引位置再sort_index()排一次序。這三步做完后面所有操作才有意義。排序這一步特別容易被跳過因為大多數(shù)采集數(shù)據(jù)天然是順序?qū)懭氲目雌饋聿挥门?。但只要中間有過補數(shù)、合并、多源拼接順序就可能被打亂而resample、rolling、shift全都是順序敏感的——順序錯了結(jié)果不會報錯只會靜默算錯這是最難查的一類問題。1.2 頻率語義freq 到底約束了什么freq這個參數(shù)經(jīng)常被誤解成數(shù)據(jù)實際的間隔其實它表達(dá)的是時間軸上的柵格規(guī)格。舉個例子一張表里記錄的是設(shè)備觸發(fā)事件的時間戳間隔完全不規(guī)則你可以給它設(shè)freqNone默認(rèn)也可以強行設(shè)成1min。設(shè)成1min之后Pandas 會認(rèn)為這條時間軸是按分鐘對齊的asfreq(1min)就會把不在整分鐘上的點丟掉或者對齊行為完全不同。所以freq是一個聲明不是一個檢測——你聲明了什么Pandas 就按什么處理聲明錯了它也不會提醒你。這個設(shè)計取舍背后其實很合理自動推斷頻率在數(shù)據(jù)有噪聲時極不可靠與其猜錯不如讓使用者顯式聲明。代價就是我們必須自己確認(rèn)數(shù)據(jù)的真實粒度。我常用的檢查手段是把索引轉(zhuǎn)成 Series 求差分然后看眾數(shù)import pandas as pd gap df.index.to_series().diff().value_counts().head(5) print(gap)輸出的前幾名基本就反映了真實粒度。如果第一名是0 days 00:01:00且占比九成以上那freq1min就是安全的如果前幾名分散得很厲害說明數(shù)據(jù)有大量缺失或者本身就是事件流這時候強行設(shè)freq只會給自己埋雷不如保持None用resample的時候再顯式指定目標(biāo)頻率。這個習(xí)慣幫我省過很多次排查時間——頻率聲明錯誤導(dǎo)致的問題往往要等到下游聚合結(jié)果明顯不合理才被發(fā)現(xiàn)。1.3 幾種常見方案的橫向?qū)Ρ葧r間序列的建模方式不止一種選錯了后面全是返工。我把實際項目里用過的幾種方案列個表方便對照自己的場景挑。方案時間位置適用場景主要優(yōu)勢主要代價普通列 手動分組普通列一次性報表、低頻分析無需學(xué)習(xí)成本無法用 resample/時間窗口 rollingDatetimeIndex resample索引固定頻率聚合、降采樣語義清晰、代碼短要求數(shù)據(jù)大致規(guī)整整數(shù)序號 時間列普通列不規(guī)則事件流、序列模型順序可控、便于對齊時間語義要自己維護(hù)PeriodIndex索引按月/季等周期統(tǒng)計周期邊界天然正確跨周期運算受限第三行那個方案值得多說一句。做序列模型比如基于滑動窗口的回歸時我有時會故意不用DatetimeIndex而是保留一列整數(shù)seq當(dāng)索引時間作為普通列掛著。原因是模型訓(xùn)練階段更關(guān)心第 t 步到第 t-1 步的嚴(yán)格先后關(guān)系用整數(shù)序號可以避免時間空洞帶來的隱式錯位特征對齊也更直觀。這不是說DatetimeIndex不好而是說工具要匹配階段——探索分析和特征工程用時間索引喂模型前轉(zhuǎn)成規(guī)整的二維矩陣。分清楚這個邊界很多到底該用哪種的糾結(jié)就沒了。2. 時間索引的建立與清洗臟數(shù)據(jù)的第一道閘2.1 to_datetime 的參數(shù)怎么選才不出事pd.to_datetime是入口函數(shù)也是最容易埋雷的地方。最常見的坑是不指定format讓它自己猜。Pandas 的推斷邏輯在混合格式下會退化成逐行解析速度慢幾十倍不說遇到2024-03-01和01/03/2024混在一起的情況它可能把后者解析成 1 月 3 日——月日顛倒結(jié)果全錯而且不報錯。我在一個多源合并的表上吃過這個虧兩個數(shù)據(jù)源一個用 ISO 格式一個用斜杠格式合并后時間線整體錯位了半個月查了兩天才定位到。所以我現(xiàn)在的固定寫法是能確定格式就顯式寫format格式不統(tǒng)一就先按來源拆開分別轉(zhuǎn)轉(zhuǎn)完再合并。import pandas as pd raw pd.DataFrame({ ts: [2024-03-01 08:00:00, 2024-03-01 08:01:00, 2024-03-01 08:02:00], value: [1.2, 1.5, 1.1], }) raw[ts] pd.to_datetime(raw[ts], format%Y-%m-%d %H:%M:%S, errorsraise)errors參數(shù)也值得展開說。默認(rèn)是raise遇到解析不了的值直接拋異常改成coerce會把壞值變成NaT流程能繼續(xù)跑下去。我傾向于在探索階段用raise逼自己把臟數(shù)據(jù)全部找出來只有在已經(jīng)確認(rèn)臟值占比極低、且能接受丟棄的場景下才用coerce。見過不少代碼一上來就errorscoerce結(jié)果整列里有三成解析失敗全變成NaT后續(xù)dropna一刪樣本少了一大截還沒人察覺。另一類坑來自時間戳里的意外成分比如帶毫秒、帶時區(qū)偏移、帶中文時分秒。帶時區(qū)的字符串轉(zhuǎn)出來是datetime64[ns, tz]和本地時間的不同列運算會報錯需要統(tǒng)一tz_localize或tz_convert??鐣r區(qū)采集的場景里我的建議是一律先轉(zhuǎn)成 UTC 存著展示和業(yè)務(wù)判斷的時候再轉(zhuǎn)回本地偏移。這樣算差值、算間隔時永遠(yuǎn)不會因為切換偏移量而出現(xiàn)一小時誤差也避免了短暫偏移調(diào)整期內(nèi)的重復(fù)或缺失時間點——這類問題在日志里極難發(fā)現(xiàn)表現(xiàn)就是某個小時的樣本數(shù)莫名其妙多出或少掉一批。2.2 set_index、排序與重復(fù)時間戳的處理轉(zhuǎn)完類型就可以提索引了。這里有個順序細(xì)節(jié)先set_index再sort_index和先sort_values再set_index結(jié)果一樣但后者更慢因為sort_values走的是普通列排序路徑。直接排序索引效率更高df raw.set_index(ts).sort_index()真實數(shù)據(jù)里重復(fù)時間戳非常普遍尤其是多設(shè)備、多渠道寫入同一張表的時候。重復(fù)索引會讓resample的某些聚合口徑變得微妙也會讓reindex報錯。處理方法要看業(yè)務(wù)含義如果是重復(fù)上報保留最后一條更合理后寫覆蓋先寫如果是同一時刻不同維度那就不該合并應(yīng)該加維度列做成MultiIndex。dup_cnt df.index.duplicated().sum() if dup_cnt: df df[~df.index.duplicated(keeplast)]keep取first、last、False三種False是把所有重復(fù)項全刪掉慎用——它會連帶丟掉本來有效的那條。我一般先統(tǒng)計重復(fù)數(shù)量占比很低千分之幾才直接去重占比高的話說明數(shù)據(jù)源有問題得回頭查采集鏈路不能靠去重掩過去。還有一個容易被忽略的點MultiIndex下的時間序列操作。當(dāng)索引是(設(shè)備編號, 時間)兩層時resample需要配合level參數(shù)指定在哪一層做df_multi df_multi.resample(1h, levelts).mean()level寫錯或者不寫會得到完全不相關(guān)的結(jié)果或直接報錯。這個用法在處理多序列數(shù)據(jù)多臺設(shè)備、多個門店時幾乎是必備的比循環(huán)groupby快一個量級我做過一次對比二十萬行、200 個分組resample(level)比groupby().apply()快大約四倍。2.3 補齊時間軸reindex 的正確打開方式缺失時間點有兩種處理哲學(xué)一種是沒有就是沒有保持稀疏一種是補齊柵格再填值保持規(guī)整。分析階段我偏向后者因為整條時間軸完整之后rolling、diff、shift的語義才準(zhǔn)確。補齊用reindex配合date_range比resample更可控因為它不會做任何聚合full_idx pd.date_range( startdf.index.min(), enddf.index.max(), freq1min, namets, ) df_full df.reindex(full_idx)namets這個參數(shù)別漏不寫的話新索引沒名字后面reset_index會得到index這個列名接下游容易對不上。補出來的空位是NaN怎么填取決于數(shù)據(jù)性質(zhì)測量值可以前向填充ffill或線性插值計數(shù)類指標(biāo)應(yīng)該填 0比率類指標(biāo)最好別亂填寧可標(biāo)成缺失讓模型自己處理。我見過把缺失的比率直接ffill二十多個點的做法等于人為制造了一段平坦的假數(shù)據(jù)模型學(xué)到的就是這個假規(guī)律評估階段看起來還行實際預(yù)測時就會在同樣的位置給出可疑的平穩(wěn)輸出。注意ffill一定要設(shè)limit否則長時間斷流會被一個值填滿整段看起來像真的連續(xù)觀測。我的習(xí)慣是limit3最多補三個點超出的保持NaN交給下游丟棄或單獨標(biāo)記。3. 重采樣與頻率轉(zhuǎn)換降采樣和升采樣的實操細(xì)節(jié)3.1 resample 的聚合字典與命名規(guī)范降采樣高頻轉(zhuǎn)低頻最常用的就是resample?;A(chǔ)寫法很簡單但真正用起來聚合方式往往不是一個函數(shù)能打發(fā)的——平均值、最大值、樣本數(shù)、缺失率一次要算好幾個指標(biāo)。這時候用命名聚合Pandas 0.25 之后支持比傳字典更清晰daily df.resample(D).agg( val_mean(value, mean), val_max(value, max), val_min(value, min), sample_cnt(value, count), missing_cnt(value, lambda s: s.isna().sum()), )(列名, 函數(shù))這種元組寫法直接產(chǎn)出扁平列名比agg({value: [mean,max]})出來的多級列名好處理得多。多級列名后續(xù)要droplevel或者手動拼名字很容易出錯尤其是下游要按列名取特征的時候。這一點我在給模型準(zhǔn)備特征表時深有體會——多級列名導(dǎo)成 CSV 再讀回來列名會變成(value, mean)這樣的元組字符串再想取某列就非常難受。所以從聚合那一步就把名字定干凈后面省事。3.2 窗口邊界closed 和 label 到底怎么影響結(jié)果resample里最容易算錯的就是窗口邊界。默認(rèn)closedleft、labelleft意思是左閉右開、標(biāo)簽用左端點。舉個具體例子resample(5min)在08:00到08:05之間默認(rèn)口徑包含08:00:00到08:04:59的數(shù)據(jù)時間標(biāo)簽記為08:00。這個默認(rèn)值符合大多數(shù)人的直覺但當(dāng)你要和另一套系統(tǒng)對齊時就必須顯式確認(rèn)——對方是左閉右開還是左開右閉標(biāo)簽打在區(qū)間起點還是終點差一點就是整體偏移。left_closed df.resample(5min, closedleft, labelleft).mean() right_closed df.resample(5min, closedright, labelright).mean()這兩行結(jié)果看起來只差一個時間標(biāo)簽但實際數(shù)值分布完全不同因為落到每個桶里的原始點不一樣。我在一次對賬里就是因為這個差了一整個時間桶上游按右閉算、我按左閉算兩邊的日匯總在跨天時刻差了整整五分鐘的數(shù)據(jù)量級不大但對不上賬就是問題。還有一個參數(shù)origin在按天、按周聚合時很關(guān)鍵。默認(rèn)originstart_day也就是按自然日切如果要按從某個時間點開始的 24 小時切就得傳originstart或具體時間戳。做班次統(tǒng)計、按批次時長聚合的場景這個參數(shù)直接決定結(jié)果對不對。3.3 asfreq 和 resample 的差別別混著用這兩個函數(shù)經(jīng)常被當(dāng)成等價物其實定位完全不同。resample是分組聚合必須給一個聚合方式或者默認(rèn)取均值asfreq是重采樣到新柵格只做取樣不聚合多出來的點直接丟空出來的位置給NaN。做升采樣低頻轉(zhuǎn)高頻的時候我?guī)缀踔挥胊sfreq因為升采樣本來的語義就是放大柵格、保留原值、填空缺用resample反而會引入不必要的聚合邏輯。up df.asfreq(30s) up[value] up[value].interpolate(methodtime)interpolate(methodtime)這個寫法值得單獨說。普通的interpolate()是按位置線性插值methodtime是按時間距離加權(quán)。數(shù)據(jù)間隔不均勻的時候兩者差別明顯比如08:00值是 1、08:04值是 5中間08:01那個點按位置插值給 2等距假設(shè)按時間插值同樣給 2看起來一樣但如果中間點是08:03按位置給 3按時間給 4。顯然按時間更符合物理意義。不規(guī)則采集的數(shù)據(jù)做插值一定要加methodtime否則時間間隔的信息白丟了。3.4 一個完整的分層聚合實戰(zhàn)把上面這些串起來走一遍從分鐘級原始數(shù)據(jù)到日級特征表的完整流程。假設(shè)手頭是某設(shè)備采集項目導(dǎo)出的一份記錄字段是時間和讀數(shù)存在亂序、少量重復(fù)、部分時段斷流。import numpy as np import pandas as pd # 1) 解析與索引 df (raw .assign(tslambda d: pd.to_datetime(d[ts], format%Y-%m-%d %H:%M:%S)) .set_index(ts) .sort_index()) # 2) 去重 df df[~df.index.duplicated(keeplast)] # 3) 補齊分鐘柵格 full_idx pd.date_range(df.index.min(), df.index.max(), freq1min, namets) df df.reindex(full_idx) # 4) 短缺口前向填充長缺口保留 NaN df[value] df[value].ffill(limit3) # 5) 日級聚合 daily df.resample(D).agg( val_mean(value, mean), val_std(value, std), val_max(value, max), sample_cnt(value, count), missing_cnt(value, lambda s: int(s.isna().sum())), ) # 6) 派生指標(biāo) daily[coverage] daily[sample_cnt] / 1440 daily[range] daily[val_max] - daily[val_min]這里第 6 步的coverage覆蓋率是我每次都會加的一列。它等于有效樣本數(shù)除以理論樣本數(shù)直接反映當(dāng)天數(shù)據(jù)質(zhì)量。覆蓋率低于某個閾值的日期后面建模時我會單獨標(biāo)記甚至剔除——用覆蓋率只有三成的天算出來的日均值噪聲比信號大得多混進(jìn)訓(xùn)練集只會拉低模型表現(xiàn)。這個指標(biāo)沒有任何技術(shù)難度但很多人的特征表里就是沒有導(dǎo)致他們永遠(yuǎn)不知道自己的日均值里有多少天是殘缺的。4. 滾動窗口與滯后特征機器學(xué)習(xí)前置的核心武器4.1 rolling 的窗口類型與 min_periods 陷阱rolling有兩種窗口定義方式整數(shù)窗口按行數(shù)和時間窗口按時間跨度。按行數(shù)寫rolling(7)就是前 7 行前提是數(shù)據(jù)已經(jīng)按固定頻率補齊否則7 行可能對應(yīng)三天也可能對應(yīng)七小時語義完全不同。時間窗口寫rolling(7D)才是字面意思上的過去七天Pandas 內(nèi)部按時間戳判斷不看行數(shù)。做業(yè)務(wù)特征我強烈建議用時間窗口因為業(yè)務(wù)口徑通常按自然時間定義最近七天而不是最近七條記錄。df[ma_7d] df[value].rolling(7D, min_periods1).mean() df[std_7d] df[value].rolling(7D, min_periods3).std()min_periods這個參數(shù)是個大坑。默認(rèn)值等于窗口長度意味著前六個點的滾動均值全是NaN——數(shù)據(jù)頭部直接損失。設(shè)成min_periods1能讓頭部也算出值但早期的值是兩三個點算出來的方差極大和后面的值不在一個尺度上。我的處理方式是折中設(shè)min_periods為窗口長度的一半左右早期那一段直接在下游截掉不用。這樣既不會因為頭部NaN導(dǎo)致整行被dropna刪掉也避免了用兩三個點冒充七天均值。窗口的center參數(shù)也要留意默認(rèn)False表示窗口右端對齊當(dāng)前點只用歷史設(shè)True會讓窗口居中等于用了未來數(shù)據(jù)。做特征工程時center必須是False一旦設(shè)成True你的滾動均值里就包含了未來信息離線指標(biāo)會虛高。這個參數(shù)在畫圖平滑曲線時很好用在建模特征里是禁用項兩種場景要分開記別寫混。4.2 expanding 與 ewm長記憶特征的兩條路線rolling是固定窗口expanding是從開頭累積到當(dāng)前點窗口長度一直變大。適合刻畫累計表現(xiàn)和歷史全量統(tǒng)計df[cum_mean] df[value].expanding(min_periods10).mean() df[cum_max] df[value].expanding(min_periods10).max()ewm是指數(shù)加權(quán)越近的點權(quán)重越大數(shù)學(xué)上等價于一個無限長但權(quán)重衰減的窗口。做有明顯趨勢和突變的指標(biāo)比如流量、活躍度ewm通常比簡單滾動均值跟得更緊滯后更小。關(guān)鍵是span和alpha的換算關(guān)系alpha 2 / (span 1)span7對應(yīng)alpha0.25。我一般直接用span因為它和多少天的直覺對應(yīng)得更直接。df[ewm_7] df[value].ewm(span7, adjustFalse, min_periods3).mean()adjustFalse這個參數(shù)務(wù)必加上。默認(rèn)True時早期結(jié)果會做歸一化修正數(shù)值形態(tài)和False有明顯差別而且在數(shù)據(jù)頭部差異最大。做特征時統(tǒng)一用False因為它的遞推形式更接近在線更新的語義也便于你在流式計算里復(fù)現(xiàn)同樣的值。如果訓(xùn)練時用了默認(rèn)值、推理時自己手寫遞推公式兩邊必然對不上這類線上線下不一致的問題排查起來極其折磨。4.3 shift / diff / pct_change滯后特征的正確姿勢滯后特征是時間序列建模的主力三個函數(shù)各管一段shift平移、diff差分、pct_change變化率。用法簡單但有兩個細(xì)節(jié)必須講清楚。第一個是shift的方向。shift(1)是把數(shù)據(jù)往下推一行也就是當(dāng)前行拿到的是上一行的值——這才是用歷史預(yù)測現(xiàn)在的正確構(gòu)造。shift(-1)是把未來值拉到當(dāng)前行這是標(biāo)簽構(gòu)造用的當(dāng)前特征對應(yīng)下期目標(biāo)絕對不能出現(xiàn)在特征里。我在代碼審查時見過把shift(-1)當(dāng)特征寫進(jìn)流水線的那基本等于作弊。第二個是pct_change的除零和負(fù)值。當(dāng)上期值為 0 時變化率是inf當(dāng)上期值為負(fù)時變化率的符號會翻過來語義完全變味。穩(wěn)妥做法是先檢查分母分布必要時改用diff或者加一個極小偏移df[diff_1] df[value].diff(1) df[lag_1] df[value].shift(1) safe_base df[value].shift(1).where(lambda s: s.abs() 1e-9) df[pct_1] (df[value] - safe_base) / safe_base用where把接近 0 的分母替換成NaN比直接replace(0, np.nan)更安全因為后者只處理精確的 0處理不了1e-12這種實際上等效于 0 的值。除出來的inf一旦進(jìn)了模型標(biāo)準(zhǔn)化會被直接帶偏整列特征失效。4.4 一套可直接套用的特征構(gòu)造流水線把上面的零件組裝成一個函數(shù)這是我在項目里反復(fù)用的骨架。核心原則只有一條所有用到歷史統(tǒng)計量的特征都要先在原始序列上shift(1)再計算否則當(dāng)前點的值會進(jìn)到自己的特征里。def build_ts_features(df, targetvalue, lags(1, 2, 3), windows(7, 14)): out df.copy() base out[target] # 滯后特征 for k in lags: out[flag_{k}] base.shift(k) # 滾動統(tǒng)計先 shift 再 roll防泄漏 for w in windows: shifted base.shift(1) out[froll_mean_{w}] shifted.rolling(w, min_periodsmax(2, w // 2)).mean() out[froll_std_{w}] shifted.rolling(w, min_periodsmax(2, w // 2)).std() out[froll_max_{w}] shifted.rolling(w, min_periodsmax(2, w // 2)).max() # 差分與變化率 out[diff_1] base.diff(1) safe base.shift(1).where(lambda s: s.abs() 1e-9) out[pct_1] (base - safe) / safe # 時間特征 idx out.index out[hour] idx.hour out[weekday] idx.weekday out[is_weekend] (idx.weekday 5).astype(int) # 周期性編碼 out[hour_sin] np.sin(2 * np.pi * idx.hour / 24) out[hour_cos] np.cos(2 * np.pi * idx.hour / 24) return outshift(1)再rolling這個組合是整段代碼的靈魂。很多人寫特征時只對lag做了shift滾動統(tǒng)計直接在原序列上算結(jié)果第 t 行的roll_mean_7里包含了第 t 行的真實值——模型只要盯著這個特征就能看到答案離線評估自然漂亮。這個錯誤的隱蔽性在于單看代碼base.rolling(7).mean()完全合理根本不像有 bug。唯一的防法是養(yǎng)成條件反射——看到滾動統(tǒng)計先問一句這個窗口的右端點包不包含當(dāng)前時刻。時間特征的周期性編碼也值得說一下。hour直接當(dāng)數(shù)值喂進(jìn)去模型會認(rèn)為 23 點和 0 點相差 23實際它們只差 1。用sin/cos做環(huán)形編碼把 24 小時映射到單位圓上距離關(guān)系就正確了。這個技巧對星期、月份同樣適用代價是多了幾列特征收益是模型不用再自己學(xué)這個映射關(guān)系小樣本下提升挺明顯。4.5 切分與防泄漏最后一道關(guān)卡特征表造好之后切訓(xùn)練集和測試集這一步必須按時間切不能隨機切。隨機切會讓測試集的時間點散落在訓(xùn)練集內(nèi)部模型實際是在插值而不是外推評估結(jié)果虛高。正確做法是按時間點截斷split_point int(len(feature_df) * 0.8) train feature_df.iloc[:split_point] test feature_df.iloc[split_point:]如果要做多折驗證別用默認(rèn)的 K 折用時間序列專用的滾動切分保證每折的驗證集都在訓(xùn)練集之后。還有一個細(xì)節(jié)標(biāo)準(zhǔn)化、歸一化這類操作統(tǒng)計量只能用訓(xùn)練集算然后應(yīng)用到測試集。很多人在整張表上fit完再切均值方差里已經(jīng)包含了測試集信息這是典型的隱性泄漏。我習(xí)慣在流水線里把fit嚴(yán)格限定在訓(xùn)練切片上寧可多寫兩行也不圖省事。提示造完特征先用一句feature_df.isna().mean().sort_values(ascendingFalse).head()看一眼各列缺失率。頭部那幾行被shift挖空是正常的如果有中間段落的列缺失率異常高多半是原始時間軸沒補齊或者聚合口徑寫錯了別急著往下走。5. 常見問題與排查實錄5.1 問題速查表下面這張表是我這幾年遇到頻率最高的一批問題按癥狀—原因—處理三列整理遇到類似現(xiàn)象可以直接對號入座。癥狀大概率原因處理方式resample 報TypeError索引不是 DatetimeIndex檢查set_index后有沒有to_datetime聚合結(jié)果比原始數(shù)據(jù)少很多時間列解析失敗變NaT后被丟用errorsraise重跑定位壞值日匯總和手工核對差一截closed/label口徑不一致顯式指定兩個參數(shù)并與上游對齊rolling 頭部全NaNmin_periods默認(rèn)等于窗口長度降到窗口長度一半頭部截斷rolling 中間出現(xiàn)大段NaN時間軸有空洞時間窗口跨不過去先reindex補齊再滾動pct_change出inf上期為 0分母where掉近零值模型離線指標(biāo)好得反常特征含未來信息排查shift(-1)、centerTrue、未 shift 的 rolling推理結(jié)果與訓(xùn)練分布不符ewm的adjust不一致統(tǒng)一adjustFalse合并后時間線整體偏移多源時間格式不統(tǒng)一分源解析后再合并統(tǒng)一時區(qū)5.2 幾個只有踩過才知道的細(xì)節(jié)第一個是resample對空桶的處理。如果某個時間窗口內(nèi)一個樣本都沒有聚合結(jié)果里這一行會存在但全是NaNcount會是 0。這在做特征時很危險——如果你用count當(dāng)樣本量特征模型會看到一堆 0如果用mean當(dāng)特征那是一堆NaN。我的做法是單獨保留count列作為質(zhì)量指標(biāo)其他統(tǒng)計量遇到NaN直接保留不填讓下游根據(jù)count決定這條樣本用不用。一刀切fillna(0)是最糟的選擇會把沒有數(shù)據(jù)偽裝成數(shù)值為零。第二個是Grouper和groupby的配合。當(dāng)你要按周聚合、但周的定義不是自然周比如從周一開始算resample(W)可能不符合預(yù)期pd.Grouper(freqW-MON, closedleft, labelleft)更靈活。另外多列分組時Grouper可以只對時間列生效其他列正常分組weekly df.groupby([ pd.Grouper(freqW-MON, closedleft, labelleft), device_id, ]).agg(val_mean(value, mean))這個寫法比先resample再unstack干凈得多尤其是在分組維度多的時候。第三個是性能。resample().agg()在千萬行級別會明顯變慢我實測過幾種寫法用命名聚合比傳字典快用內(nèi)置聚合函數(shù)比lambda快很多l(xiāng)ambda里的s.isna().sum()這類操作會退化成逐組 Python 調(diào)用。如果數(shù)據(jù)量大把自定義邏輯挪到聚合之后用向量化算能省掉一大半時間。另外一個思路是先降采樣到粗粒度再算比如日級特征不需要從分鐘級逐點算可以先聚合到小時再往上滾誤差在可接受范圍內(nèi)而速度提升明顯。第四個是時間序列的對齊問題多序列合并時尤其明顯。兩張表的采樣時刻不完全一致df1 df2會產(chǎn)生大量NaN因為 Pandas 按索引精確對齊。這時候用resample先統(tǒng)一到同一柵格或者用merge_asof做近似匹配。merge_asof要求兩邊都按時間排序direction參數(shù)控制取最近的過去還是最近的未來做傳感器數(shù)據(jù)與事件打標(biāo)對齊時特別好用比reindexffill更精確因為它可以選擇最近鄰而不是單純的前向。最后分享一個我自己養(yǎng)成的習(xí)慣任何一次時間序列處理開頭先打印三行——索引類型、索引是否單調(diào)遞增、時間跨度。這三行幾乎能提前暴露八成的潛在問題。print(type(df.index).__name__) print(df.index.is_monotonic_increasing) print(df.index.min(), -, df.index.max(), | rows:, len(df))看起來是廢話但我確實靠這三行抓到過好幾次數(shù)據(jù)只導(dǎo)入了半天的低級錯誤——文件截斷、查詢條件漏了時間上限這類問題不會報錯只會讓你在幾小時后對著一堆反常的聚合結(jié)果發(fā)懵?;ㄈ腌姶蛴”然ㄈr排查劃算得多。