:從數(shù)據清洗到LSTM集成建模全流程)
簡介這是一份圍繞“智造揚中”電力AI大賽的完整Python實戰(zhàn)源碼包面向電氣電子類競賽參賽者及電力AI學習者旨在展示從數(shù)據拆分、特征提取到模型訓練與預測的完整工程鏈路。壓縮包共18個文件約835KB以6個Python腳本、4個CSV數(shù)據表、2個XML工程配置及readme、sh運行腳本等構成其中data_split、feature_extract、model.py等模塊清晰劃分了數(shù)據處理、特征工程與建模環(huán)節(jié)run_all.sh可用于一鍵復現(xiàn)流程。目前已學習44人適合具備一定Python基礎、希望快速上手電力負荷預測或電網智能管理項目的讀者。通過閱讀源碼可掌握天池電力預測表的清洗與特征構造思路、模型調參邏輯及輸出預測結果csv的完整做法為參賽或畢業(yè)設計提供可移植的代碼模板。1. 電賽電力AI賽題這份壓縮包把從數(shù)據清洗到提交的全流程攤開了電賽這幾年最讓隊伍頭疼的不是控制題而是帶“AI”字樣的電力方向題目25年B題、E題和26年G題、F題里都能看到負荷辨識、電力預測、故障診斷的影子。拿到一份不知道從哪下手的JSON或CSV很多隊伍第一反應是套個LSTM上去結果驗證集刷得挺高提交分數(shù)卻掉得離譜。這份「大航杯智造揚中電力AI大賽_1.zip」的完整解題包正好把這條鏈路補齊賽題數(shù)據長什么樣、特征怎么做、模型怎么選、結果按什么格式交。適合三類人正在備賽電賽電力AI方向的學生隊伍、要做電力數(shù)據分析的課題組、以及想拿真實電力數(shù)據集練手但被數(shù)據清洗勸退的工程師。下文所有操作我按這套資源里最典型的流程拆開講。2. 從賽題到方案選型電力數(shù)據為什么不能直接套通用機器學習流程2.1 電力AI賽題到底在考什么電力AI賽題和普通的數(shù)據競賽有個明顯區(qū)別數(shù)據帶有強時間依賴性和周期性而且采集端經常出問題。負荷預測、臺區(qū)辨識、電壓異常檢測這幾類題數(shù)據通常來自智能電表或SCADA系統(tǒng)采樣間隔從1分鐘到15分鐘不等文件里除了負荷值還有時間戳、溫度、濕度、電價等多維字段。賽題表面讓你“預測未來時段負荷”實際考察的是兩件事能不能把時間序列特征吃透以及你的提交格式能不能被評分腳本正確解析。我在拆這份資源時先看了它的數(shù)據目錄結構典型的電賽-style布局訓練集按日期分片存CSV測試集是連續(xù)時間段標簽列單獨放一個文件。這種“分段給數(shù)據”的模式在電賽里非常常見意圖就是逼你手動對齊時間索引而不是直接讀進來就訓練。數(shù)據里還留了不少空值和尖峰毛刺這是故意埋的預處理考點。2.2 模型選型時序深度模型與樹模型的取舍很多隊伍一上來就上Transformer但電力負荷數(shù)據的樣本量通常只有幾萬條時間長度也短Transformer在小樣本時序上很容易過擬合收斂還慢。我一般建議先做兩組基線一組是LSTM/GRU這類循環(huán)模型另一組是把時序數(shù)據展平成特征表丟給LightGBM。兩份資源里給的參考方案也是這個思路深度學習模型負責捕捉短期依賴樹模型負責吃統(tǒng)計特征、外部因素特征。選型判斷標準看三點數(shù)據量、序列長度、外部特征占比。數(shù)據量低于5萬條時LSTM比Transformer穩(wěn)GRU又比LSTM輕量序列長度超過48個時間步考慮注意力機制或直接上TCN溫度、濕度、電價這些外部特征占比重時樹模型的上限往往更高因為深度模型對這類異構特征的擬合效率不高。具體取舍可以直接參照表2-1。模型適合場景訓練成本電賽電力題常見用途LightGBM特征工程充足、外部變量多低負荷預測、異常檢測LSTM/GRU序列模式強、樣本量適中中短期負荷預測、電壓序列建模Transformer長序列、樣本量大高多變量長時序預測集成LSTMLightGBM追求穩(wěn)定排名中高最終提交主力方案2.3 評價指標與提交格式先對齊評價指標決定了損失函數(shù)怎么設計。電力預測題用得最多的是MAPE平均絕對百分比誤差和RMSE個別題會用分類準確率或F1-score判斷“辨識”型任務。這份資源在文檔里明確寫了按MAPE打分這點非常關鍵——如果按RMSE做損失函數(shù)模型會對峰值負荷投入過多注意力而MAPE對近零值的小絕對誤差非常敏感兩者的最優(yōu)解完全不同。實操上損失函數(shù)用HuberLoss或MAPE的平滑版本輸出層不加激活函數(shù)訓練時監(jiān)控驗證集的MAPE早停閾值設在20個epoch內不下降即停。提交格式一般是一個CSV兩列預測時間點、預測值。行數(shù)必須和測試集嚴格一致索引錯位是電賽提交最常見的問題第5章我會專門講。3. 數(shù)據預處理與特征工程把現(xiàn)場采集的電力數(shù)據變成能訓的樣本3.1 缺失值與異常值處理電力數(shù)據里的缺失值分三種單點缺失、連續(xù)缺失、整段缺失。單點缺失用前后時刻線性插值就能補連續(xù)缺失超過3個采樣點時線性插值會抹掉趨勢我一般用同星期類型、同時段的均值填充整段缺失比如某一天文件整個沒有只能放棄這一段不要讓模型自己去“腦補”一整天。異常值要區(qū)分是采集噪聲還是真實尖峰。負荷突增可能是設備啟動也可能是抄表錯誤。一個穩(wěn)妥的判斷方法是滾動中位數(shù)濾波某個時刻的值偏離前后3小時中位數(shù)超過3倍IQR就標記為異常替換成中位數(shù)。代碼實現(xiàn)很簡單import pandas as pd import numpy as np def median_filter_outliers(series, window_hours3, iqr_multiplier3, freq15min): # 計算滾動中位數(shù)和IQRwindow由時間決定而非固定行數(shù) rolling series.rolling(windowf{window_hours}h, min_periods6) median rolling.median() q1 series.rolling(windowf{window_hours}h, min_periods6).quantile(0.25) q3 series.rolling(windowf{window_hours}h, min_periods6).quantile(0.75) iqr q3 - q1 lower median - iqr_multiplier * iqr upper median iqr_multiplier * iqr # 超出邊界的置為NaN后續(xù)統(tǒng)一插值 cleaned series.where((series lower) (series upper)) return cleaned.interpolate(methodlinear) df[load_clean] median_filter_outliers(df[load])這里有兩個參數(shù)要注意window_hours要按數(shù)據集的采樣頻率調整如果數(shù)據是1分鐘一條3小時窗口是180條記錄滾動計算量不小iqr_multiplier在電力場景下用3比較合適——我們驗證過用2會把正常的早晚高峰尖峰全砍掉損失掉模型最需要的峰值信息。min_periods6保證窗口內至少有一小時數(shù)據支撐統(tǒng)計量否則前幾個點會被置空。3.2 時間特征與滑窗構造電力負荷有典型的“三周期性”一天24小時、一周7天、一年四季。模型沒法直接理解時間戳字符串必須拆成數(shù)值特征。我習慣先把時間戳拆出小時、星期、是否工作日、距離最近節(jié)假日的天數(shù)再加一個hour_sin和hour_cos這樣的周期編碼。注意星期特征不要用順序整數(shù)周一到周日是1到7用one-hot或者周期編碼更好因為周六和周一在語義上不相鄰。滑窗構造是時序模型的第二步。對LSTM來說窗口長度我們試過6、12、24、48電賽這種15分鐘粒度的數(shù)據24步即6小時效果最好太長反而把噪聲也裝進去?;按a要注意步長訓練時滑動步長設為1來最大化樣本數(shù)但構造測試集預測時步長必須等于預測長度否則會產生重疊預測。def make_sequences(data, seq_len24, pred_len6): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data[i:iseq_len]) y.append(data[iseq_len:iseq_lenpred_len]) return np.array(X), np.array(y) # data是二維數(shù)組 [樣本數(shù), 特征數(shù)] # seq_len24 表示用過去24個時間步預測未來6個時間步 X_train, y_train make_sequences(df[[load,hour_sin,hour_cos]].values, 24, 6) print(X_train.shape, y_train.shape) # 輸出類似 (89352, 24, 3) (89352, 6, 3)這段代碼有個容易踩的地方pred_len是預測步數(shù)它決定了輸出層的神經元個數(shù)和MAPE的計算方式。如果賽題要求預測未來一天而數(shù)據是15分鐘粒度pred_len96此時LSTM輸出層的96個神經元直接對應96個時間點的值這種映射關系要提前憋清楚否則后面對齊標簽時必亂。3.3 歸一化與數(shù)據劃分防止正態(tài)分布統(tǒng)計量泄漏歸一化是電賽數(shù)據處理里翻車率最高的環(huán)節(jié)。最大的坑用全量數(shù)據的均值、方差去做歸一化再劃分訓練集和驗證集。這等于把驗證集和測試集的統(tǒng)計信息泄露給了模型驗證集上永遠好看但一到真正未知的測試集就露出原形。正確做法是先切分、再單獨對訓練集擬合scaler然后transform驗證集和測試集。此外電力數(shù)據是時間序列劃分不能隨機打亂必須按時間順序前80%作訓練、后20%作驗證。隨機shuffle在你第一次拿到數(shù)據時幾乎一定會手滑一旦shuffle模型學到的是“記住了所有時間點的統(tǒng)計規(guī)律”而不是“根據過去推未來”的能力。from sklearn.preprocessing import StandardScaler # 先按時間切分再做歸一化 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx].copy() val_df df.iloc[split_idx:].copy() scaler StandardScaler() # 只fit訓練集transform全部 train_df[[load]] scaler.fit_transform(train_df[[load]]) val_df[[load]] scaler.transform(val_df[[load]])一個我每次都檢查的操作fit_transform和transform別寫反寫反了等于用驗證集的均值方差去歸一化訓練集整個特征分布直接錯亂。這種錯誤在代碼review時很難一眼看出來因為訓練loss還是能降只是驗證曲線會產生異常的抖動。4. 模型訓練與調參把基準方案跑到榜單中位以上4.1 訓練腳本骨架與關鍵參數(shù)這份資源的訓練腳本主體是PyTorch寫的一個雙層LSTM輸出層接全連接。選PyTorch而不是Keras或純sklearn的理由是電賽環(huán)境經常要求選手在本地離線GPU上跑題PyTorch的顯存控制和斷點續(xù)訓機制比Keras好調試出錯時能直接看到梯度流向。整個腳本分五段配置讀取、數(shù)據處理、模型定義、訓練循環(huán)、驗證保存。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class ElectricityLSTM(nn.Module): def __init__(self, input_size, hidden_size128, num_layers2, pred_len6): super().__init__() # 雙層LSTM第一層輸出給第二層hidden_size從64調到此值后MAPE降了約1.2 self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 # 兩層之間的dropout最后一層后的輸出不加 ) self.fc nn.Linear(hidden_size, pred_len) def forward(self, x): out, _ self.lstm(x) # 取最后一個時間步的hidden state 做預測 return self.fc(out[:, -1, :])hidden_size128和num_layers2是電賽這種數(shù)據規(guī)模下的甜點值再往上加到256和3層驗證集MAPE幾乎不動訓練時間翻倍。dropout0.2只加在兩層LSTM之間如果加到輸入層會抑制模型學習短時波動。訓練時我一般固定batch_size256學習率1e-3配AdamW權重衰減1e-5跑30個epoch后手動降學習率到3e-4再跑20輪。4.2 損失函數(shù)與評估指標的對齊邏輯剛才第2章提過評分是MAPE但直接用MAPE值做損失函數(shù)有個問題當真實值非常接近0時MAPE的梯度會爆炸。電賽數(shù)據里凌晨時段的負荷確實會掉到很低的量級所以這份資源用的是平滑MAPE變體在分母上加一個小常數(shù)防止除零。def smooth_mape_loss(y_pred, y_true, eps1.0): # eps取1.0量級和負荷值域(幾十到幾千)對齊 # 如果直接把eps設成1e-6凌晨低負荷樣本的loss會主導整個訓練 diff torch.abs(y_true - y_pred) return torch.mean(diff / torch.clamp(torch.abs(y_true) eps, mineps))eps1.0這個值不是隨手填的。如果把它設成1e-6低負荷時段的樣本每個都能產生巨大梯度模型會為了擬合凌晨的低值犧牲白天高峰段的精度而白天高峰段恰好是評分權重最大的部分。反過來eps設得過大100以上所有樣本的誤差都被壓縮成一個量級模型學不到區(qū)分度。跑實驗時可以先統(tǒng)計一下訓練集負荷的5%分位數(shù)把eps設成比這個值再小一檔我們這套數(shù)據5%分位約在8.5左右取1.0這個保守值效果最好。4.3 本地評測腳本與模型保存訓練結束不急著提交本地先出一個評測報告。驗證集上的MAPE、RMSE、以及按小時分段的誤差曲線都要看一眼。如果某個小時段的誤差明顯高于其他段說明特征和模型對該時段捕捉不足回3.2補特征。# 按小時統(tǒng)計驗證集誤差 val_df[hour] val_df.index.hour val_df[abs_err_ratio] (val_df[pred] - val_df[load]).abs() / val_df[load] hourly_mape val_df.groupby(hour)[abs_err_ratio].mean() # 常見的異常早上6-8點誤差飆高說明模型沒學到早高峰斜坡特征 print(hourly_mape) # 保存時同時存模型結構和參數(shù)避免PyTorch版本不一致導致load失敗 torch.save({ model_state: model.state_dict(), config: {input_size: 13, hidden_size: 128, pred_len: 6} }, best_model.pth)保存時順手把模型配置寫進同一個文件這點很值得養(yǎng)成習慣。電賽現(xiàn)場經常換機器跑PyTorch版本一換直接load模型權重會報奇怪的鍵名錯誤有了config字段可以隨時重建網絡結構再load。模型用哪個epoch的文件也有講究保存驗證集MAPE最優(yōu)的那個不保存最后一個epoch——時序模型最后一個epoch大概率已經過擬合。5. 電賽電力AI實戰(zhàn)避坑訓練泄漏、時區(qū)對齊與結果格式三個老大難5.1 五條真實踩坑記錄第一條驗證集MAPE只有8%提交分數(shù)卻到15%。原因出在數(shù)據劃分時用了train_test_split默認的shuffle把未來數(shù)據泄露給了訓練集。解決無條件按時間順序劃分劃分后分別打印首尾時間戳確認沒有重疊。從那以后我對每個隊伍都強調時間序列的驗證集不允許隨機抽樣這是紅線。第二條提交CSV的行數(shù)始終比官方樣例多出來幾十行。原因是生成預測時數(shù)據里有重復索引groupby后沒去重導致多行。解決提交前做一次行數(shù)校驗。wc -l submit.csv wc -l sample_submit.csv python -c import pandas as pd; apd.read_csv(submit.csv); bpd.read_csv(sample_submit.csv); print(a.shape, b.shape, a[time].is_unique)第三個命令里is_unique是False的話說明索引重復先把duplicated的行找出來刪掉再復檢。第三條預測值整體比真實值偏低15%。原因是歸一化時對訓練集和測試集分別做了fit_transform測試集里出現(xiàn)訓練集沒見過的峰最大值域transform后標準值被壓到異常區(qū)間。解決用訓練集擬合scaler后保存測試集只transform并檢查測試集最大值是否超出訓練集最大值3個標準差。超出的話說明測試數(shù)據分布漂移需要加日志特征或做差分。第四條模型訓練loss下降但驗證MAPE在20個epoch后開始鋸齒狀震蕩。原因是學習率偏大AdamW雖然自帶自適應但1e-3對這個數(shù)據集還是太高。解決換成余弦退火調度器從1e-3降到1e-4周期長度設為總epoch數(shù)的1/2我們實測震蕩完全消失最終MAPE還降了0.8。第五條提交格式被拒系統(tǒng)報“列名不匹配”。原因是官方要求列名是timestamp,predict我們輸出成了time,load。解決寫一個格式對齊函數(shù)讀取官方樣例的列名和順序強制把結果的列名改成樣例名再輸出。每次提交前都跑這個校驗腳本列名、行數(shù)、數(shù)據類型、有無NaN全查一遍。5.2 評分腳本黑匣子怎么應對電賽的評分腳本通常不公開你只能在提交后拿到一個總分。這個黑匣子讓很多人焦慮但換個角度想它也有規(guī)律可循。你可以在本地盡量仿真它按官方給的樣例格式寫一個自己的評分函數(shù)用驗證集跑一遍記錄本地MAPE提交后再對比官方分數(shù)。兩次分數(shù)的差異如果穩(wěn)定在一個固定偏差比如本地10%、官方12.5%說明官方的測試集和你的驗證集分布接近你完全可以把這個偏差當作校準常數(shù)。反過來如果兩次差異忽大忽小多半是提交結果里存在異常預測值回頭檢查凌晨時段的輸出看看有沒有負值或離譜的尖峰。6. 進階把單模型換成集成與后處理白撿兩三個百分點6.1 集成策略LSTM、LightGBM、GRU三個模型加權單模型的提升空間有限集成才是電賽拿穩(wěn)排名的關鍵。最省事的方案是三模型加權平均LSTM和GRU在序列建模上各有側重LightGBM則擅長捕捉外部特征的交叉效應。權重的搜參方式不復雜網格搜索就能搞定LSTM從0.2到0.5步長0.05GRU同區(qū)間余下權重全給LightGBM以驗證集MAPE為最低目標。def ensemble_predict(models, weights, x_seq, x_tab): preds np.zeros(len(x_seq)) for i, (model, w) in enumerate(zip(models, weights)): if isinstance(model, torch.nn.Module): # 深度模型喂序列數(shù)據樹模型喂展平特征表 model.eval() with torch.no_grad(): pred model(x_seq).cpu().numpy().flatten() else: pred model.predict(x_tab).flatten() preds w * pred return preds # 權重示例先各自單獨跑出MAPE按誤差反比初始化再微調 # lstm_mape10.2, gru_mape10.5, lgb_mape11.8 # 初始權重約 [0.38, 0.36, 0.26] best_weights [0.37, 0.35, 0.28] pred ensemble_predict([lstm, gru, lgb], best_weights, x_seq, x_tab)集成有個前提條件三個模型必須保證用同一套時間步劃分邏輯否則預測值在時間軸上對不齊。我實踐下來最簡單的是三個模型共享同一個訓練集和歸一化參數(shù)深度模型吃原始序列LightGBM吃經過時序統(tǒng)計聚合的特征表最后在預測階段求和。6.2 后處理規(guī)則修正與輸出平滑電賽電力題還有一個容易得分的小技巧物理約束后處理。電力負荷有明確的物理邊界——不能為負不會瞬間跳變過大。模型預測值偶爾會違反這些常識直接提交就扣分。我會在輸出后加兩道修正負值截斷為0相鄰時間步跳變量超過前值40%的按前值40%封頂。def rule_postprocess(pred, max_jump_ratio0.4): pred np.maximum(pred, 0) for i in range(1, len(pred)): prev pred[i-1] diff pred[i] - prev limit prev * max_jump_ratio if diff limit: pred[i] prev limit return pred pred rule_postprocess(pred, 0.4)max_jump_ratio0.4這個閾值對15分鐘粒度的負荷數(shù)據是我們的驗證集調出來的。設太大起不到修正作用設太小比如0.1會削平真實的高峰爬坡在帶儲能或電動汽車接入的臺區(qū)數(shù)據上反而制造誤差。如果你不知道賽題數(shù)據里有沒有這類突發(fā)負荷先按0.4跑一遍再看修正前后本地MAPE的變化方向若修正后變差就調回0.6。6.3 自建評測腳本電賽專屬的“后悔藥”最后分享一個我自己的習慣專門為每次電賽寫一個evaluate.py把數(shù)據加載、格式校驗、指標計算全封裝起來。每次訓完模型、生成完預測都強制走一遍這個腳本才允許自己提交。腳本里除了算MAPE還會自動檢查預測值有沒有NaN、有沒有負值、行數(shù)對不對、列名是否匹配。這個腳本在這份資源里也被我整理好附帶了建議你拿到后先跑一遍示例驗證數(shù)據確認腳本輸出正常再替換成自己的模型和真實測試數(shù)據路徑。從那以后我每次提交前都強制走一遍評測腳本確認行數(shù)、列名、值域全過才敢點提交按鈕。這套流程幫我規(guī)避了至少三次因格式問題被記零分的事故希望幫到你。本文還有配套的精品資源點擊獲取