序預(yù)測(cè)實(shí)戰(zhàn):從特征工程到滾動(dòng)回測(cè)的完整工程)
簡介這份資源面向時(shí)間序列預(yù)測(cè)與機(jī)器學(xué)習(xí)的入門及進(jìn)階學(xué)習(xí)者用Xgboost算法打通從數(shù)據(jù)分析、特征工程到模型訓(xùn)練、保存與預(yù)測(cè)評(píng)估的完整鏈路可應(yīng)用于金融、氣象、股票等趨勢(shì)預(yù)測(cè)場(chǎng)景。壓縮包共3個(gè)文件以2個(gè)Python腳本和1個(gè)CSV數(shù)據(jù)集為主腳本分別承擔(dān)預(yù)測(cè)流程與共享邏輯CSV提供ETTh1實(shí)測(cè)數(shù)據(jù)整體約407KB輕量易跑點(diǎn)擊即可運(yùn)行。目前已有6030人學(xué)習(xí)下載說明其在實(shí)戰(zhàn)練習(xí)中具備一定參考價(jià)值。讀者可從中獲得一份可直接復(fù)用的Xgboost時(shí)間序列方案代碼含逐步注釋覆蓋輸入特征選擇、模型本地保存、未來數(shù)值序列生成以及預(yù)測(cè)值與實(shí)際觀測(cè)值的對(duì)比評(píng)估同時(shí)能順帶練習(xí)數(shù)據(jù)分析與特征工程的具體寫法適合對(duì)照代碼理解每一步意圖并遷移到自己的數(shù)據(jù)集上。1. 拿到一份能直接跑的時(shí)序預(yù)測(cè)工程先看它解決了什么很多做時(shí)序項(xiàng)目的開發(fā)者都有過這種經(jīng)歷數(shù)據(jù)清洗、特征工程、模型調(diào)參的代碼散落在幾個(gè) notebook 里換臺(tái)機(jī)器就跑不起來想復(fù)現(xiàn)某次實(shí)驗(yàn)結(jié)果得翻半天聊天記錄。這份基于 XGBoost 的 Python 機(jī)器學(xué)習(xí)實(shí)戰(zhàn)工程把時(shí)間序列預(yù)測(cè)和時(shí)間序列分類兩條主線都收進(jìn)了一套可點(diǎn)擊運(yùn)行的代碼里從數(shù)據(jù)加載、滑動(dòng)窗口構(gòu)造、特征生成到模型訓(xùn)練與評(píng)估鏈路是完整的。它適合兩類人一類是剛接觸時(shí)序建模、想找一個(gè)能跑通的最小閉環(huán)照著改的新手另一類是手里有業(yè)務(wù)數(shù)據(jù)、需要快速搭一個(gè)基線模型做對(duì)比的熟手。下面按「能干什么、怎么跑、怎么改」的順序拆開講。2. 工程結(jié)構(gòu)與技術(shù)選型為什么是 XGBoost 而不是 LSTM2.1 目錄組織與模塊職責(zé)這類實(shí)戰(zhàn)工程通常不會(huì)做成一個(gè)巨型腳本而是按職責(zé)切成幾塊方便單獨(dú)調(diào)試。常見的目錄組織大致如下timeseries_xgb/ ├── data/ │ ├── raw/ # 原始時(shí)序數(shù)據(jù)csv │ └── processed/ # 滑窗構(gòu)造后的訓(xùn)練/驗(yàn)證集 ├── src/ │ ├── config.py # 路徑、窗口長度、超參數(shù)集中管理 │ ├── data_loader.py # 讀取 csv、缺失值處理、時(shí)間索引對(duì)齊 │ ├── features.py # 滯后特征、滾動(dòng)統(tǒng)計(jì)、時(shí)間特征 │ ├── train_regressor.py # 回歸任務(wù)預(yù)測(cè)未來數(shù)值 │ ├── train_classifier.py # 分類任務(wù)預(yù)測(cè)趨勢(shì)方向/狀態(tài) │ └── evaluate.py # RMSE、MAE、準(zhǔn)確率、混淆矩陣 ├── notebooks/ │ └── quick_start.ipynb # 點(diǎn)擊即可運(yùn)行的入口 ├── requirements.txt └── README.mdconfig.py集中放窗口長度、預(yù)測(cè)步長、學(xué)習(xí)率這些參數(shù)是這類工程最值得保留的設(shè)計(jì)。原因很實(shí)際時(shí)序模型對(duì)窗口長度極其敏感把參數(shù)散落在各個(gè)腳本里改一次要全局搜索很容易漏改導(dǎo)致訓(xùn)練和評(píng)估用的不是同一套配置。數(shù)據(jù)層和特征層分開則是因?yàn)轭A(yù)測(cè)任務(wù)和分類任務(wù)共用同一套特征構(gòu)造邏輯只是標(biāo)簽生成方式不同——回歸用未來第 N 步的真實(shí)值分類用未來漲跌方向或狀態(tài)標(biāo)簽。2.2 選 XGBoost 的工程理由時(shí)序預(yù)測(cè)領(lǐng)域現(xiàn)在提得最多的是 LSTM、Transformer 這類深度模型但這份工程選 XGBoost 做主線是有明確取舍的。XGBoost 在表格型特征上的表現(xiàn)穩(wěn)定訓(xùn)練速度快單機(jī) CPU 就能跑不需要 GPU 環(huán)境對(duì)中小規(guī)模時(shí)序數(shù)據(jù)幾千到幾十萬條把時(shí)序問題轉(zhuǎn)成監(jiān)督學(xué)習(xí)問題后樹模型的基線往往不弱于深度模型而且可解釋性好——能直接看特征重要性知道哪個(gè)滯后項(xiàng)貢獻(xiàn)最大。代價(jià)是它不會(huì)自動(dòng)捕捉序列的長期依賴需要人工構(gòu)造滯后特征和滾動(dòng)統(tǒng)計(jì)量。這也是這份工程的核心工作量所在特征工程做得好不好直接決定模型上限。常見做法是把原始序列轉(zhuǎn)成「用過去 k 個(gè)時(shí)刻預(yù)測(cè)未來第 h 個(gè)時(shí)刻」的樣本矩陣再疊加均值、標(biāo)準(zhǔn)差、最大最小值這類滾動(dòng)窗口統(tǒng)計(jì)以及小時(shí)、星期、月份這類周期特征。提示如果你的數(shù)據(jù)量在百萬級(jí)以上、序列周期性強(qiáng)且算力充足可以把 XGBoost 當(dāng)基線再上深度模型對(duì)比如果只是做業(yè)務(wù)預(yù)警或趨勢(shì)判斷樹模型加好特征通常夠用。3. 從原始序列到監(jiān)督學(xué)習(xí)樣本特征工程的完整鏈路3.1 滑動(dòng)窗口構(gòu)造樣本矩陣時(shí)序預(yù)測(cè)的第一個(gè)坎是把一維序列變成「X 是過去、y 是未來」的二維表。核心邏輯是滑動(dòng)窗口窗口長度lookback決定用多少歷史步預(yù)測(cè)步長horizon決定預(yù)測(cè)未來第幾步。import numpy as np import pandas as pd def make_supervised(series, lookback24, horizon1): 把一維序列轉(zhuǎn)成監(jiān)督學(xué)習(xí)樣本矩陣 series: 按時(shí)間排序的一維數(shù)值序列 lookback: 用過去多少個(gè)時(shí)刻作為特征 horizon: 預(yù)測(cè)未來第幾個(gè)時(shí)刻 X, y [], [] n len(series) for i in range(lookback, n - horizon 1): # 過去 lookback 個(gè)點(diǎn)作為一行特征 X.append(series[i - lookback:i]) # 未來第 horizon 個(gè)點(diǎn)作為標(biāo)簽 y.append(series[i horizon - 1]) cols [flag_{j} for j in range(lookback, 0, -1)] return pd.DataFrame(X, columnscols), pd.Series(y, nametarget)這段代碼的關(guān)鍵在索引邊界range(lookback, n - horizon 1)保證每一行都有完整的lookback個(gè)歷史點(diǎn)同時(shí)標(biāo)簽不會(huì)越界。lag_列的命名從遠(yuǎn)到近排列方便后續(xù)按時(shí)間順序理解特征重要性。參數(shù)上lookback一般取一個(gè)完整周期的長度比如日數(shù)據(jù)取 7 或 30小時(shí)數(shù)據(jù)取 24horizon按業(yè)務(wù)需求定預(yù)測(cè)越遠(yuǎn)誤差越大通常不建議超過lookback的一半。3.2 滾動(dòng)統(tǒng)計(jì)與時(shí)間特征疊加只靠滯后項(xiàng)模型學(xué)不到趨勢(shì)和波動(dòng)性。補(bǔ)一層滾動(dòng)統(tǒng)計(jì)是常規(guī)操作def add_rolling_features(df, window7): 在原始序列上疊加滾動(dòng)均值、標(biāo)準(zhǔn)差、極值 df df.copy() df[roll_mean] df[value].rolling(window).mean() df[roll_std] df[value].rolling(window).std() df[roll_max] df[value].rolling(window).max() df[roll_min] df[value].rolling(window).min() # 時(shí)間周期特征前提是索引為 DatetimeIndex df[hour] df.index.hour df[weekday] df.index.weekday df[is_weekend] (df.index.weekday 5).astype(int) return df.dropna()rolling(window)會(huì)產(chǎn)生前window-1行的空值dropna()必須放在特征構(gòu)造之后、切分訓(xùn)練集之前否則空值會(huì)污染模型輸入。時(shí)間特征對(duì)有明顯周期性的數(shù)據(jù)用電量、流量、銷量提升明顯但如果數(shù)據(jù)本身沒有周期規(guī)律這幾個(gè)特征會(huì)被模型判為低重要性不會(huì)造成傷害屬于低成本高上限的加法。3.3 訓(xùn)練集與驗(yàn)證集的時(shí)間切分時(shí)序數(shù)據(jù)不能隨機(jī)打亂切分這是血淚經(jīng)驗(yàn)。隨機(jī)切分會(huì)讓未來信息泄漏到訓(xùn)練集驗(yàn)證指標(biāo)虛高上線后直接翻車。正確做法是按時(shí)間順序切def time_split(X, y, val_ratio0.2): 按時(shí)間順序切分前 80% 訓(xùn)練后 20% 驗(yàn)證 split int(len(X) * (1 - val_ratio)) return X.iloc[:split], X.iloc[split:], y.iloc[:split], y.iloc[split:]val_ratio一般取 0.15 到 0.25驗(yàn)證集要覆蓋至少一個(gè)完整業(yè)務(wù)周期否則評(píng)估結(jié)果波動(dòng)大。如果數(shù)據(jù)量小可以改用滾動(dòng)窗口交叉驗(yàn)證每次用前一段訓(xùn)練、后一段驗(yàn)證逐步前移這樣能更充分地利用樣本。4. 模型訓(xùn)練與評(píng)估回歸和分類兩條線怎么落地4.1 回歸任務(wù)預(yù)測(cè)未來數(shù)值回歸線用 XGBoost 的XGBRegressor目標(biāo)是預(yù)測(cè)未來某個(gè)時(shí)刻的具體數(shù)值。訓(xùn)練代碼不長但參數(shù)和評(píng)估方式有講究import xgboost as xgb from sklearn.metrics import mean_squared_error, mean_absolute_error def train_regressor(X_train, y_train, X_val, y_val): model xgb.XGBRegressor( n_estimators500, # 樹的數(shù)量配合早停使用 learning_rate0.05, # 學(xué)習(xí)率越小越穩(wěn)但需要更多樹 max_depth6, # 樹深控制過擬合 subsample0.8, # 行采樣增加泛化 colsample_bytree0.8, # 列采樣 early_stopping_rounds50, eval_metricrmse, random_state42 ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], verboseFalse ) pred model.predict(X_val) rmse mean_squared_error(y_val, pred, squaredFalse) mae mean_absolute_error(y_val, pred) return model, {rmse: rmse, mae: mae}early_stopping_rounds50表示驗(yàn)證集指標(biāo)連續(xù) 50 輪不下降就停止能有效防止樹數(shù)量過多導(dǎo)致的過擬合。learning_rate和n_estimators是一對(duì)聯(lián)動(dòng)參數(shù)學(xué)習(xí)率調(diào)小樹的數(shù)量要相應(yīng)加大常見組合是 0.05 配 500 到 1000。評(píng)估時(shí) RMSE 對(duì)異常值敏感MAE 更穩(wěn)健兩個(gè)都看能判斷誤差分布是否被少數(shù)極端點(diǎn)拉偏。4.2 分類任務(wù)預(yù)測(cè)趨勢(shì)方向分類線把連續(xù)值轉(zhuǎn)成離散標(biāo)簽常見做法是預(yù)測(cè)未來漲跌方向或狀態(tài)區(qū)間def make_classification_label(series, horizon1, threshold0.0): 未來第 horizon 步相對(duì)當(dāng)前的變化方向作為標(biāo)簽 1 表示上漲超過閾值0 表示下跌超過閾值 future series.shift(-horizon) diff future - series label (diff threshold).astype(int) return label.dropna() def train_classifier(X_train, y_train, X_val, y_val): model xgb.XGBClassifier( n_estimators400, learning_rate0.05, max_depth5, subsample0.8, colsample_bytree0.8, eval_metriclogloss, early_stopping_rounds50, random_state42 ) model.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse) return modelthreshold參數(shù)用來過濾微小波動(dòng)避免把噪聲當(dāng)趨勢(shì)。分類任務(wù)最容易踩的坑是類別不平衡如果上漲樣本遠(yuǎn)多于下跌樣本模型會(huì)傾向于全預(yù)測(cè)上漲準(zhǔn)確率看起來不低但毫無用處。評(píng)估時(shí)必須看混淆矩陣和少數(shù)類的召回率不能只看總體準(zhǔn)確率。4.3 特征重要性與模型解釋樹模型的一大優(yōu)勢(shì)是能直接輸出特征重要性這對(duì)業(yè)務(wù)解釋很關(guān)鍵def show_importance(model, feature_names, top_n10): imp model.feature_importances_ pairs sorted(zip(feature_names, imp), keylambda x: x[1], reverseTrue) for name, score in pairs[:top_n]: print(f{name}: {score:.4f})如果lag_1和lag_2重要性遠(yuǎn)高于其他特征說明序列短期自相關(guān)強(qiáng)可以適當(dāng)縮短lookback減少冗余特征如果滾動(dòng)統(tǒng)計(jì)量重要性高說明趨勢(shì)和波動(dòng)是主要信號(hào)可以考慮加更多窗口長度做多尺度統(tǒng)計(jì)。5. 避坑與排查跑不通時(shí)先看這幾處5.1 時(shí)間索引沒對(duì)齊導(dǎo)致特征錯(cuò)位現(xiàn)象模型在驗(yàn)證集上指標(biāo)很好但預(yù)測(cè)結(jié)果整體滯后一個(gè)時(shí)間步。原因通常是讀取 csv 后沒有把時(shí)間列設(shè)為索引或者排序沒做導(dǎo)致rolling和shift按行號(hào)而非時(shí)間順序計(jì)算。解決方式是讀數(shù)據(jù)后立刻pd.to_datetime轉(zhuǎn)時(shí)間列、set_index設(shè)索引、sort_index()排序三步缺一不可。5.2 缺失值處理順序錯(cuò)誤現(xiàn)象訓(xùn)練時(shí)報(bào)錯(cuò)或指標(biāo)異常低。原因是先做了dropna再構(gòu)造滯后特征導(dǎo)致樣本量驟減或者滾動(dòng)統(tǒng)計(jì)產(chǎn)生的空值沒清理就進(jìn)了模型。正確順序是先補(bǔ)齊或插值原始缺失值再構(gòu)造所有特征最后統(tǒng)一dropna。插值方式上短缺口用線性插值長缺口建議直接截?cái)嗷騿为?dú)標(biāo)記不要盲目填充。5.3 分類標(biāo)簽泄漏現(xiàn)象分類準(zhǔn)確率高得離譜。常見原因是標(biāo)簽生成時(shí)用了未來信息比如用整個(gè)序列的均值做閾值或者特征里混入了未來時(shí)刻的統(tǒng)計(jì)量。檢查方法是逐列核對(duì)特征構(gòu)造的時(shí)間范圍確保每一行特征只包含該時(shí)刻之前的信息。這個(gè)坑很隱蔽建議在特征函數(shù)里加斷言檢查最大時(shí)間戳不超過標(biāo)簽對(duì)應(yīng)時(shí)刻。5.4 早停參數(shù)與評(píng)估集不匹配現(xiàn)象訓(xùn)練正常但早停從不觸發(fā)或者很早就停。原因是eval_set傳的是訓(xùn)練集本身或者驗(yàn)證集太小波動(dòng)大。解決方式是確保eval_set用獨(dú)立的驗(yàn)證集且驗(yàn)證集樣本量足夠至少幾百條early_stopping_rounds不要設(shè)得太小50 到 100 是比較穩(wěn)的范圍。5.5 環(huán)境依賴版本沖突現(xiàn)象XGBRegressor初始化報(bào)參數(shù)錯(cuò)誤。原因是 xgboost 版本差異老版本不支持early_stopping_rounds作為構(gòu)造參數(shù)需要放到fit里。解決方式是固定requirements.txt里的版本或者在代碼里做兼容判斷。這類問題在新機(jī)器上復(fù)現(xiàn)時(shí)最常見建議把環(huán)境版本寫進(jìn) README。6. 二次開發(fā)把基線模型改成能上業(yè)務(wù)的預(yù)測(cè)服務(wù)拿到這份工程后最實(shí)際的改造方向不是換模型而是把「離線訓(xùn)練腳本」變成「可復(fù)用的預(yù)測(cè)流程」。我一般會(huì)做三件事。第一把特征構(gòu)造抽成獨(dú)立的FeatureBuilder類訓(xùn)練和推理共用同一套邏輯。離線訓(xùn)練時(shí)用全量歷史構(gòu)造特征在線推理時(shí)只需要最近lookback個(gè)點(diǎn)接口統(tǒng)一成build(series) - DataFrame避免訓(xùn)練和推理特征不一致這個(gè)經(jīng)典翻車點(diǎn)。第二加一層模型持久化和版本管理。訓(xùn)練完用model.save_model(model_v1.json)存下來推理時(shí)加載同時(shí)在文件名或配置里記錄訓(xùn)練數(shù)據(jù)的時(shí)間范圍和關(guān)鍵超參數(shù)。沒有這層記錄過幾個(gè)月回頭查某次預(yù)測(cè)為什么偏基本只能靠猜。第三把評(píng)估從單點(diǎn)指標(biāo)改成滾動(dòng)回測(cè)。單次切分驗(yàn)證的偶然性太大改成每次前移一個(gè)窗口、重復(fù)評(píng)估看指標(biāo)的均值和方差才能判斷模型是否穩(wěn)定。下面是一個(gè)簡化的滾動(dòng)回測(cè)骨架def rolling_backtest(series, lookback24, horizon1, n_splits5): 滾動(dòng)回測(cè)每次用前一段訓(xùn)練后一段驗(yàn)證逐步前移 results [] step len(series) // (n_splits 1) for k in range(1, n_splits 1): train_end step * k train series[:train_end] test series[train_end:train_end step] if len(test) lookback horizon: break X_tr, y_tr make_supervised(train, lookback, horizon) X_te, y_te make_supervised(test, lookback, horizon) model, metrics train_regressor(X_tr, y_tr, X_te, y_te) results.append(metrics) return results這個(gè)骨架里n_splits控制回測(cè)輪數(shù)step是每輪前移的樣本量。跑完看各輪 RMSE 的分布如果某幾輪明顯偏高說明模型對(duì)那段時(shí)期的數(shù)據(jù)不適應(yīng)需要回頭查是不是有異常事件或分布漂移。改造時(shí)還有一個(gè)容易忽略的點(diǎn)預(yù)測(cè)步長horizon大于 1 時(shí)誤差會(huì)累積單步評(píng)估指標(biāo)不能直接套用。常見做法是分別評(píng)估每個(gè)步長的誤差畫一條誤差隨步長增長的曲線業(yè)務(wù)上能接受的最大步長就是模型的實(shí)際可用范圍。從那以后我每次接手時(shí)序項(xiàng)目都強(qiáng)制先跑一遍滾動(dòng)回測(cè)再談?wù){(diào)參單次驗(yàn)證的漂亮數(shù)字騙過太多次。希望這份拆解能幫你少走幾步彎路把這份工程跑起來、改成自己業(yè)務(wù)能用的樣子。本文還有配套的精品資源點(diǎn)擊獲取