測(cè)實(shí)戰(zhàn):機(jī)器學(xué)習(xí)模型對(duì)比與完整源碼解析)
簡(jiǎn)介面向光伏功率預(yù)測(cè)的機(jī)器學(xué)習(xí)項(xiàng)目源碼包適合畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)與期末大作業(yè)參考。項(xiàng)目提供完整 Python 工程化實(shí)現(xiàn)覆蓋數(shù)據(jù)加載、數(shù)據(jù)預(yù)處理、模型訓(xùn)練與預(yù)測(cè)輸出等環(huán)節(jié)并附帶 Jupyter Notebook 交互式分析文件和說(shuō)明文檔便于快速理解代碼邏輯整體結(jié)構(gòu)清晰適合作為高分畢設(shè)或課程作業(yè)的基礎(chǔ)框架。壓縮包共 16 個(gè)文件以 4 個(gè) Python 腳本和 8 個(gè)訓(xùn)練/測(cè)試 CSV 數(shù)據(jù)文件為主體另有 ipynb、md、docx 等輔助文件整體大小僅 4.64MB部署使用非常輕便。代碼注釋細(xì)致新手也能看懂可對(duì)照學(xué)習(xí)光伏功率預(yù)測(cè)中的特征工程與回歸建模流程。目前已有 316 人學(xué)習(xí)/下載。下載后可直接獲得可運(yùn)行的工程入口、劃分好的訓(xùn)練與測(cè)試數(shù)據(jù)集、任務(wù)說(shuō)明文檔及關(guān)鍵腳本模塊可在本地快速?gòu)?fù)現(xiàn)光伏功率預(yù)測(cè)實(shí)驗(yàn)并在此基礎(chǔ)上替換數(shù)據(jù)、調(diào)整特征或模型進(jìn)行二次開(kāi)發(fā)。1. 光伏功率預(yù)測(cè)這個(gè)標(biāo)題到底在解決什么問(wèn)題光伏功率預(yù)測(cè)說(shuō)白了就是回答「未來(lái) 15 分鐘到 24 小時(shí)這塊光伏電站能發(fā)多少電」。電網(wǎng)調(diào)度要提前知道出力曲線來(lái)安排火電啟停電站業(yè)主需要預(yù)測(cè)來(lái)優(yōu)化自己的交易策略運(yùn)維人員要判斷是天氣原因還是設(shè)備故障導(dǎo)致出力驟降。這個(gè)標(biāo)題里的「機(jī)器學(xué)習(xí)」不是炫技而是因?yàn)楣夥隽π蛄杏泻軓?qiáng)的非線性和隨機(jī)性傳統(tǒng)物理模型基于輻照度、溫度直接折算在陰雨天和云團(tuán)快速移動(dòng)時(shí)誤差大得離譜。基于歷史功率和氣象數(shù)據(jù)訓(xùn)練出來(lái)的模型反而能抓住那些物理公式表達(dá)不出來(lái)的規(guī)律——比如某片區(qū)域在特定風(fēng)向下云團(tuán)會(huì)提前多少分鐘擋住太陽(yáng)。這個(gè)項(xiàng)目適合誰(shuí)三類人。一是剛?cè)腴T(mén) ML 但不想做爛大街的房?jī)r(jià)預(yù)測(cè)、鳶尾花分類的在校生光伏功率是典型的時(shí)間序列回歸問(wèn)題做完整一套能同時(shí)練到特征工程、模型調(diào)參、評(píng)估指標(biāo)這些核心技能。二是光伏電站的運(yùn)維或數(shù)據(jù)分析人員手上攢了一堆 SCADA 歷史數(shù)據(jù)卻不知道怎么變現(xiàn)這套方案可以直接拿去改改用。三是做電力市場(chǎng)交易或微電網(wǎng)調(diào)度的開(kāi)發(fā)者需要一個(gè)能做基線預(yù)測(cè)的模塊。標(biāo)題里帶了「源碼訓(xùn)練數(shù)據(jù)測(cè)試數(shù)據(jù)」意味著你不需要自己去爬數(shù)據(jù)、清洗半天才能動(dòng)手拿到就能跑通這也正好踩中了大部分人「急著看效果」的訴求。這類項(xiàng)目的坑往往不在模型而在數(shù)據(jù)。常見(jiàn)的問(wèn)題是時(shí)間戳對(duì)不齊、輻照度傳感器損壞導(dǎo)致異常值、夜間零功率段污染訓(xùn)練集這些我在后面的章節(jié)里會(huì)逐條拆開(kāi)。先放一個(gè)結(jié)論別一上來(lái)就上 LSTM先把梯度提升樹(shù)調(diào)明白多數(shù)場(chǎng)景下效果已經(jīng)夠用而且調(diào)試成本低一個(gè)量級(jí)。2. 光伏功率預(yù)測(cè)的建模思路先弄清楚預(yù)測(cè)什么、用什么預(yù)測(cè)2.1 預(yù)測(cè)任務(wù)的三種粒度決定了你的模型結(jié)構(gòu)光伏功率預(yù)測(cè)按時(shí)間粒度分大致三類超短期未來(lái) 15 分鐘到 4 小時(shí)、短期未來(lái) 1 到 3 天、中期周級(jí)別。這個(gè)標(biāo)題的項(xiàng)目通常落在超短期和短期之間因?yàn)橛?xùn)練數(shù)據(jù)和測(cè)試數(shù)據(jù)的口徑一般按「歷史功率序列 對(duì)應(yīng)氣象預(yù)報(bào)」來(lái)組織。超短期預(yù)測(cè)主要靠歷史功率的時(shí)序慣性因?yàn)槲磥?lái)幾小時(shí)內(nèi)云團(tuán)運(yùn)動(dòng)有連續(xù)性短期預(yù)測(cè)則更依賴數(shù)值天氣預(yù)報(bào)NWP中的輻照度、溫度、濕度等變量。這三種粒度對(duì)應(yīng)的模型結(jié)構(gòu)差異很大。如果你只是基于歷史功率序列做滾動(dòng)預(yù)測(cè)那用滯后特征lag feature加梯度提升樹(shù)就能拿到不錯(cuò)的基線如果你要把天氣預(yù)報(bào)數(shù)據(jù)也灌進(jìn)去做 24 小時(shí)預(yù)測(cè)那就需要考慮用序列模型或者把氣象變量和時(shí)序特征拼在一起做多變量回歸。拿到項(xiàng)目源碼后第一件事不是急著跑而是打開(kāi)數(shù)據(jù)文件確認(rèn)三件事時(shí)間分辨率是多少15 分鐘1 小時(shí)、有沒(méi)有對(duì)應(yīng)的氣象特征列、訓(xùn)練集和測(cè)試集的時(shí)間段是否有重疊。延遲特征的選擇是個(gè)關(guān)鍵點(diǎn)。光伏功率序列在晴天表現(xiàn)出極強(qiáng)的「日周期性」——上午爬坡、中午平臺(tái)、下午下坡所以 lag 特征不像純隨機(jī)時(shí)間序列那樣只取前幾個(gè)時(shí)刻而是要取「昨天同一時(shí)刻」「前天同一時(shí)刻」「前一個(gè)小時(shí)」「前 15 分鐘」這類具有物理含義的滯后點(diǎn)。有些項(xiàng)目直接把前 168 小時(shí)的全部功率作為特征灌給模型維度爆炸不說(shuō)還引入了大量噪聲。表項(xiàng)目里常見(jiàn)的時(shí)間分辨率與模型選擇對(duì)應(yīng)關(guān)系時(shí)間分辨率預(yù)測(cè)范圍常用特征推薦模型15 分鐘未來(lái) 15min-4h歷史功率滯后項(xiàng)、滾動(dòng)均值、云量梯度提升樹(shù) / LSTM1 小時(shí)未來(lái) 1-24h數(shù)值天氣預(yù)報(bào)輻照度、溫度、濕度梯度提升樹(shù) / 隨機(jī)森林1 小時(shí)未來(lái) 1-3 天數(shù)值天氣預(yù)報(bào)全變量 季節(jié)特征LightGBM / Prophet 對(duì)比2.2 輸入特征怎么構(gòu)造氣象征變量 時(shí)間特征 滯后特征三板斧光伏功率預(yù)測(cè)的特征工程是整個(gè)流程里最吃經(jīng)驗(yàn)、也最影響最終精度的環(huán)節(jié)。第一板斧是氣象征變量包括地表水平輻照度GHI、環(huán)境溫度、組件溫度、風(fēng)速、風(fēng)向、濕度。其中輻照度是絕對(duì)的主導(dǎo)變量功率和它的相關(guān)性在晴天能到 0.9 以上。第二板斧是時(shí)間特征小時(shí)數(shù)、日天數(shù)、月份、星期幾、是否節(jié)假日。這些特征看似簡(jiǎn)單但能幫模型區(qū)分「冬夏日照長(zhǎng)度差異」和「工作日與周末的用電負(fù)荷差異」。第三板斧是滯后特征也就是上面提到的歷史功率序列用來(lái)捕捉云團(tuán)運(yùn)動(dòng)的短期慣性。特征構(gòu)造的代碼通常長(zhǎng)這樣import pandas as pd import numpy as np # 讀取原始數(shù)據(jù)假設(shè)索引是datetime類型功率列名為power_kw df pd.read_csv(solar_data.csv, parse_dates[time], index_coltime) df df.sort_index() # 1. 時(shí)間特征拆出小時(shí)、月份、一年中的第幾天 df[hour] df.index.hour df[month] df.index.month df[dayofyear] df.index.dayofyear # 2. 滯后特征昨天同一時(shí)刻、前天同一時(shí)刻、前一時(shí)刻 df[lag_24h] df[power_kw].shift(96) # 15分鐘分辨率下96個(gè)點(diǎn)24小時(shí) df[lag_48h] df[power_kw].shift(192) # 48小時(shí)前的功率 df[lag_1step] df[power_kw].shift(1) # 上一個(gè)時(shí)刻的功率 # 3. 滾動(dòng)特征過(guò)去1小時(shí)的滑動(dòng)均值用于平滑瞬時(shí)波動(dòng) df[rolling_mean_1h] df[power_kw].rolling(window4, min_periods1).mean() # 4. 剔除夜間零功率段避免模型被大量0值帶偏 df df[(df[power_kw] 0) | (df[hour] 5) (df[hour] 19)] # 5. 刪除含NaN的行因shift會(huì)產(chǎn)生NaN df df.dropna()邏輯說(shuō)明shift(96)在 15 分鐘分辨率下表示取 24 小時(shí)前的功率值作為特征這樣模型能學(xué)習(xí)到「今天的出力曲線大概率跟隨昨天的形狀」這是光伏序列最顯著的特征之一。rolling(window4)計(jì)算過(guò)去 1 小時(shí)的滑動(dòng)均值等于對(duì)短時(shí)云層遮擋造成的毛刺做了平滑能降低模型對(duì)瞬時(shí)抖動(dòng)的敏感度。參數(shù)說(shuō)明窗口大小的選擇需要匹配你的數(shù)據(jù)分辨率——15 分鐘數(shù)據(jù)用 96 是 24 小時(shí)用 48 是半天的周期這個(gè)要根據(jù)你項(xiàng)目的預(yù)測(cè)目標(biāo)調(diào)整。滾動(dòng)窗口長(zhǎng)度我常用 4 或 8對(duì)應(yīng) 1 小時(shí)和 2 小時(shí)的平均。夜間剔除邏輯不是簡(jiǎn)單刪行而是用一個(gè)布爾掩碼保留白天邊界附近的樣本因?yàn)榍宄亢桶砣匀挥信榔潞拖陆档内厔?shì)信息直接全刪到 6 點(diǎn)到 18 點(diǎn)會(huì)丟掉過(guò)渡段。2.3 歸一化和數(shù)據(jù)切分最容易翻車卻最容易被忽略的環(huán)節(jié)很多人在光伏功率預(yù)測(cè)上翻車不是因?yàn)槟P瓦x錯(cuò)而是數(shù)據(jù)切分時(shí)把時(shí)間順序打亂了。時(shí)間序列切分不能像圖像分類那樣隨機(jī) shuffle否則模型會(huì)「偷看」未來(lái)數(shù)據(jù)測(cè)試集上的指標(biāo)虛高一上真實(shí)環(huán)境就崩潰。正確做法是按時(shí)間先后切分比如按 8:2 的比例把前 80% 的時(shí)間段作為訓(xùn)練集后 20% 作為測(cè)試集。如果要做交叉驗(yàn)證必須使用 TimeSeriesSplit 而不是 KFold。歸一化方面也有講究。功率值和輻照度值在數(shù)值量級(jí)上差異很大功率可能上千 kW輻照度只有幾百 W/m2對(duì)基于距離的模型如 SVM、KNN影響巨大但對(duì)樹(shù)模型無(wú)所謂。如果你用梯度提升樹(shù)或隨機(jī)森林可以不歸一化如果用神經(jīng)網(wǎng)絡(luò)、LSTM 或線性回歸必須歸一化。常見(jiàn)的做法是用 MinMaxScaler 把數(shù)據(jù)縮放到 [0, 1]但要注意一個(gè)細(xì)節(jié)必須用訓(xùn)練集的 min 和 max 去轉(zhuǎn)換測(cè)試集不能把測(cè)試集的數(shù)據(jù)混進(jìn)來(lái)一起 fit否則也是信息泄露。from sklearn.model_selection import TimeSeriesSplit from sklearn.preprocessing import MinMaxScaler # 時(shí)間序列切分按時(shí)間順序不做隨機(jī)打亂 tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] # 訓(xùn)練/驗(yàn)證邏輯在此處展開(kāi) # 歸一化只擬合訓(xùn)練集再轉(zhuǎn)換訓(xùn)練集和測(cè)試集 scaler MinMaxScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 注意LSTM等神經(jīng)網(wǎng)絡(luò)需要三維輸入 (樣本數(shù), 時(shí)間步長(zhǎng), 特征數(shù)) # 這里用滑動(dòng)窗口構(gòu)造序列樣本假設(shè)時(shí)間步長(zhǎng)為24 def create_sequences(data, seq_length24): xs, ys [], [] for i in range(len(data) - seq_length): xs.append(data[i:iseq_length]) ys.append(data[iseq_length]) return np.array(xs), np.array(ys) X_seq, y_seq create_sequences(X_train_scaled, seq_length24)邏輯說(shuō)明TimeSeriesSplit每次都在遞增的歷史數(shù)據(jù)上訓(xùn)練、在下一個(gè)時(shí)間塊上驗(yàn)證這模擬了真實(shí)場(chǎng)景中「用過(guò)去預(yù)測(cè)未來(lái)」的過(guò)程。create_sequences是為 LSTM 或 GRU 準(zhǔn)備數(shù)據(jù)的標(biāo)準(zhǔn)方式滑動(dòng)窗口長(zhǎng)度為 24 在 15 分鐘數(shù)據(jù)下表示用過(guò)去 6 小時(shí)的數(shù)據(jù)預(yù)測(cè)下一個(gè)點(diǎn)。參數(shù)說(shuō)明n_splits5表示做 5 次前向驗(yàn)證數(shù)據(jù)量大時(shí)可以增加到 10?;瑒?dòng)窗口的seq_length是神經(jīng)網(wǎng)絡(luò)模型里最重要的超參數(shù)之一太短抓不住日周期規(guī)律太長(zhǎng)訓(xùn)練速度慢且可能過(guò)擬合。光伏場(chǎng)景下我一般從 24 起步試逐漸加到 96。3. 模型選型對(duì)比為什么梯度提升樹(shù)是首選LSTM 適合什么場(chǎng)景3.1 梯度提升樹(shù)LightGBM / XGBoost為什么是默認(rèn)選項(xiàng)光伏功率預(yù)測(cè)在大多數(shù)比賽中被證明梯度提升樹(shù)的效果強(qiáng)于深度學(xué)習(xí)——這不是因?yàn)樯疃葘W(xué)習(xí)不行而是因?yàn)楸砀裥蛿?shù)據(jù) 手工特征工程的組合在中小規(guī)模數(shù)據(jù)集上很難被序列模型超越。梯度提升樹(shù)的優(yōu)勢(shì)有三點(diǎn)對(duì)特征尺度不敏感不用歸一化能自動(dòng)處理特征交互比如輻照度和時(shí)刻的聯(lián)合效應(yīng)同一輻照度在早晨和下午對(duì)應(yīng)不同功率訓(xùn)練速度快調(diào)參空間相對(duì)可控。LightGBM 和 XGBoost 兩者選哪個(gè)我一般選 LightGBM因?yàn)橹狈綀D算法在大數(shù)據(jù)集上更快內(nèi)存占用更小而且對(duì)光伏這種帶明顯周期性的數(shù)據(jù)max_depth控制得當(dāng)?shù)那闆r下不容易過(guò)擬合。LightGBM 訓(xùn)練的核心代碼可以這樣寫(xiě)import lightgbm as lgb from sklearn.metrics import mean_absolute_error, mean_squared_error # 數(shù)據(jù)集構(gòu)建 train_data lgb.Dataset(X_train, labely_train) val_data lgb.Dataset(X_val, labely_val, referencetrain_data) # 參數(shù)配置 params { objective: regression, metric: mae, learning_rate: 0.05, num_leaves: 31, max_depth: 7, min_child_samples: 20, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbosity: -1 } # 訓(xùn)練模型 model lgb.train( params, train_data, num_boost_round500, valid_sets[val_data], callbacks[lgb.early_stopping(stopping_rounds50)] ) # 預(yù)測(cè)與評(píng)估 y_pred model.predict(X_test, num_iterationmodel.best_iteration) mae mean_absolute_error(y_test, y_pred) rmse np.sqrt(mean_squared_error(y_test, y_pred)) print(fMAE: {mae:.2f} kW, RMSE: {rmse:.2f} kW)邏輯說(shuō)明lgb.Dataset是 LightGBM 的原生數(shù)據(jù)格式構(gòu)建時(shí)傳入特征矩陣和標(biāo)簽向量。early_stopping的作用是在驗(yàn)證集指標(biāo)連續(xù) 50 輪不提升時(shí)停止訓(xùn)練防止過(guò)擬合同時(shí)能自動(dòng)確定最優(yōu)迭代輪數(shù)。feature_fraction和bagging_fraction是兩套隨機(jī)采樣參數(shù)前者每次建樹(shù)隨機(jī)選 80% 的特征后者每次訓(xùn)練隨機(jī)選 80% 的數(shù)據(jù)作用是增強(qiáng)模型的魯棒性。參數(shù)說(shuō)明num_leaves31是 LightGBM 的核心復(fù)雜度控制參數(shù)值越大模型越復(fù)雜31 對(duì)應(yīng)深度約為 5 的葉子數(shù)在光伏數(shù)據(jù)上表現(xiàn)穩(wěn)定。learning_rate0.05和num_boost_round500是一對(duì)搭配學(xué)習(xí)率越低需要的迭代次數(shù)越多如果訓(xùn)練時(shí)間緊張可以把學(xué)習(xí)率調(diào)到 0.1 并減少迭代輪數(shù)。max_depth7是額外限制樹(shù)深度的保險(xiǎn)絲防止葉子數(shù)過(guò)大時(shí)過(guò)擬合。min_child_samples20要求每個(gè)葉子節(jié)點(diǎn)至少有 20 個(gè)樣本數(shù)值太小時(shí)模型會(huì)學(xué)到局部噪聲。3.2 LSTM 的正確打開(kāi)方式適合短序列預(yù)測(cè)和在線更新場(chǎng)景LSTM 的真正優(yōu)勢(shì)不是精度而是滾動(dòng)預(yù)測(cè)的靈活性。當(dāng)你需要每 15 分鐘滾動(dòng)預(yù)測(cè)未來(lái) 4 小時(shí)并且希望模型能利用最新時(shí)刻的功率做狀態(tài)更新時(shí)LSTM 的序列輸入天然適合這種場(chǎng)景——因?yàn)槟憧梢园炎罱?6 小時(shí)的數(shù)據(jù)拼成一個(gè)序列直接喂進(jìn)去不需要手動(dòng)構(gòu)造滯后特征。但 LSTM 的訓(xùn)練成本高、對(duì)數(shù)據(jù)量要求大、調(diào)參更敏感數(shù)據(jù)量少于 2 萬(wàn)條時(shí)不建議用。用 LSTM 做光伏功率預(yù)測(cè)的最小實(shí)現(xiàn)from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping import numpy as np # X_seq shape: (樣本數(shù), 時(shí)間步長(zhǎng), 特征數(shù)) # 這里特征數(shù)5對(duì)應(yīng)功率、輻照度、溫度、風(fēng)速、小時(shí)正弦編碼 model Sequential([ LSTM(64, return_sequencesTrue, input_shape(X_seq.shape[1], X_seq.shape[2])), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(16, activationrelu), Dense(1) ]) model.compile(optimizeradam, lossmae, metrics[mae]) # 早停監(jiān)控驗(yàn)證集損失連續(xù)20輪不下降則停止 early_stop EarlyStopping(monitorval_loss, patience20, restore_best_weightsTrue) history model.fit( X_seq_train, y_train, validation_data(X_seq_val, y_val), epochs100, batch_size64, callbacks[early_stop], verbose0 ) y_pred model.predict(X_seq_test)邏輯說(shuō)明return_sequencesTrue讓第一個(gè) LSTM 層輸出完整的序列給第二層第二層return_sequencesFalse只輸出最后一個(gè)時(shí)間步的隱藏狀態(tài)再接全連接層輸出預(yù)測(cè)值。Dropout(0.2)在 LSTM 層之間隨機(jī)丟棄 20% 的神經(jīng)元輸出是防止循環(huán)網(wǎng)絡(luò)過(guò)擬合最有效的正則手段。EarlyStopping的restore_best_weightsTrue會(huì)在訓(xùn)練停止后把權(quán)重回滾到驗(yàn)證集表現(xiàn)最好的那一輪避免了「訓(xùn)練到最后一輪反而過(guò)擬合」的問(wèn)題。參數(shù)說(shuō)明LSTM(64)的 64 是隱藏單元數(shù)太大容易過(guò)擬合且訓(xùn)練慢太小表達(dá)能力不足。光伏數(shù)據(jù)我習(xí)慣從 64 開(kāi)始數(shù)據(jù)量大時(shí)加到 128。batch_size64是批大小影響訓(xùn)練速度和梯度穩(wěn)定性顯存小就減半。patience20是早停耐心輪數(shù)光伏數(shù)據(jù)噪聲較大耐心太小容易在損失值正常波動(dòng)時(shí)提前停止。3.3 模型對(duì)比實(shí)驗(yàn)怎么做固定評(píng)估指標(biāo)控制變量「高分代碼」項(xiàng)目里通常包含多個(gè)模型的對(duì)比結(jié)果一般會(huì)展示隨機(jī)森林、XGBoost、LightGBM、LSTM 四者的誤差指標(biāo)。復(fù)現(xiàn)這個(gè)對(duì)比不是說(shuō)把所有模型跑一遍就完事了而是要固定同樣的特征、同樣的數(shù)據(jù)切分、同樣的評(píng)估指標(biāo)否則對(duì)比沒(méi)有意義。我建議至少跑以下三個(gè)模型線性回歸作為基線判斷 ML 模型到底比簡(jiǎn)單規(guī)則強(qiáng)多少、LightGBM主模型、LSTM序列模型對(duì)照組。評(píng)估指標(biāo)選擇方面光伏功率預(yù)測(cè)最常用的是 MAE、RMSE、R2 和歸一化均方根誤差nRMSE即 RMSE 除以裝機(jī)容量。MAE 反映平均偏差大小RMSE 對(duì)大幅誤差更敏感——這對(duì)光伏很重要因?yàn)樵茍F(tuán)遮擋導(dǎo)致的大誤差對(duì)電網(wǎng)調(diào)度的危害遠(yuǎn)大于均勻的小誤差。如果你要跟其他電站對(duì)比效果必須使用 nRMSE 歸一化否則不同裝機(jī)容量的電站之間毫無(wú)可比性。4. 訓(xùn)練與評(píng)估流程跑通最小樣例再談精度提升4.1 完整訓(xùn)練腳本從數(shù)據(jù)加載到結(jié)果存檔把前面幾節(jié)的代碼整合成一條完整流水線這個(gè)腳本可以直接作為項(xiàng)目的主入口import pandas as pd import numpy as np import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import json # ---------- 1. 數(shù)據(jù)加載 ---------- df pd.read_csv(solar_data.csv, parse_dates[time], index_coltime) # 按時(shí)間升序排列確保切分順序正確 df df.sort_index() # ---------- 2. 特征工程 ---------- def build_features(df): df df.copy() df[hour] df.index.hour df[month] df.index.month df[dayofyear] df.index.dayofyear # 對(duì)小時(shí)做正弦編碼保留周期性 df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) # 滯后特征前一天同時(shí)刻 df[lag_24h] df[power_kw].shift(96) # 滾動(dòng)統(tǒng)計(jì) df[rolling_mean_1h] df[power_kw].rolling(window4, min_periods1).mean() df[rolling_std_1h] df[power_kw].rolling(window4, min_periods1).std() return df.dropna() df_feat build_features(df) # ---------- 3. 特征與標(biāo)簽劃分 ---------- feature_cols [hour_sin, hour_cos, month, dayofyear, lag_24h, rolling_mean_1h, rolling_std_1h, ghi, temp, wind_speed] X df_feat[feature_cols] y df_feat[power_kw] # ---------- 4. 時(shí)間序列交叉驗(yàn)證 ---------- split_idx int(len(X) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] # ---------- 5. 訓(xùn)練 LightGBM ---------- params { objective: regression, metric: mae, learning_rate: 0.05, num_leaves: 31, max_depth: 7, min_child_samples: 20, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbosity: -1 } model lgb.train( params, lgb.Dataset(X_train, labely_train), num_boost_round1000, valid_sets[lgb.Dataset(X_test, labely_test)], callbacks[lgb.early_stopping(stopping_rounds50)] ) # ---------- 6. 評(píng)估與存檔 ---------- y_pred model.predict(X_test, num_iterationmodel.best_iteration) metrics { mae: mean_absolute_error(y_test, y_pred), rmse: np.sqrt(mean_squared_error(y_test, y_pred)), r2: r2_score(y_test, y_pred) } print(fMAE: {metrics[mae]:.2f} kW) print(fRMSE: {metrics[rmse]:.2f} kW) print(fR2: {metrics[r2]:.4f}) # 保存模型和結(jié)果 model.save_model(lgb_model.txt) with open(metrics.json, w) as f: json.dump(metrics, f, indent2)邏輯說(shuō)明hour_sin和hour_cos是成對(duì)構(gòu)造的周期編碼單獨(dú)用hour0到hour23這個(gè)整數(shù)會(huì)讓模型誤以為 23 點(diǎn)和 0 點(diǎn)距離很遠(yuǎn)正弦余弦編碼則能讓首尾相接。rolling_std_1h是滾動(dòng)的標(biāo)準(zhǔn)差特征它捕捉的是過(guò)去一小時(shí)內(nèi)出力的波動(dòng)程度云團(tuán)快速移動(dòng)時(shí)這個(gè)值會(huì)明顯升高模型可以利用它來(lái)預(yù)判未來(lái)短時(shí)間的波動(dòng)性。參數(shù)說(shuō)明feature_cols是可配置的不同項(xiàng)目的數(shù)據(jù)列名可能不同實(shí)際使用時(shí)要按真實(shí)的 CSV 列名來(lái)替換。split_idx int(len(X) * 0.8)是按原始時(shí)間順序切分如果數(shù)據(jù)本身已經(jīng)是亂序的要先 sort_index。num_boost_round1000配合早??梢员WC訓(xùn)練充分實(shí)際迭代次數(shù)由best_iteration決定。4.2 誤差分析與結(jié)果可視化光看指標(biāo)不夠要看曲線模型評(píng)估不能只看三個(gè)指標(biāo)。光伏功率預(yù)測(cè)的特殊之處在于誤差分布極不均勻——晴天誤差小、陰天誤差大、日出日落時(shí)段誤差中等。如果只看 MAE 可能會(huì)被平均效果掩蓋局部時(shí)段的糟糕表現(xiàn)。正確的做法是畫(huà)出三條曲線真實(shí)功率曲線、預(yù)測(cè)功率曲線、誤差絕對(duì)值曲線。不管哪個(gè)項(xiàng)目把這三條線畫(huà)在一張圖上比任何指標(biāo)都更能說(shuō)明問(wèn)題。import matplotlib.pyplot as plt # 取測(cè)試集前3天的數(shù)據(jù)進(jìn)行可視化 plot_steps 96 * 3 # 3天15分鐘分辨率 plt.figure(figsize(14, 5)) plt.plot(y_test.index[:plot_steps], y_test.values[:plot_steps], labelActual, colorblack, linewidth2) plt.plot(y_test.index[:plot_steps], y_pred[:plot_steps], labelPredicted, colortab:red, linewidth1, alpha0.8) error np.abs(y_test.values[:plot_steps] - y_pred[:plot_steps]) plt.bar(y_test.index[:plot_steps], error, labelAbs Error, colortab:blue, alpha0.3, width0.01) plt.title(PV Power Prediction: Actual vs Predicted (3 Days)) plt.xlabel(Time) plt.ylabel(Power (kW)) plt.legend() plt.tight_layout() plt.savefig(prediction_result.png, dpi150)邏輯說(shuō)明plt.bar是柱狀圖用來(lái)顯示每個(gè)時(shí)間點(diǎn)的絕對(duì)誤差半透明的柱體和兩條功率曲線疊在一起能直觀看出誤差集中在哪些時(shí)段——如果柱子密集出現(xiàn)在午后的某個(gè)區(qū)間就說(shuō)明模型對(duì)那個(gè)時(shí)段如下午輻照度高但溫度也高導(dǎo)致組件效率下降的系統(tǒng)性偏差沒(méi)有學(xué)到。參數(shù)說(shuō)明plot_steps96*3控制可視化的長(zhǎng)度15 分鐘分辨率下 96 點(diǎn)是一整天3 天足夠觀察日周期模式。width0.01在時(shí)間索引跨度大時(shí)讓柱子變細(xì)避免柱體過(guò)度重疊蓋住曲線。觀察誤差時(shí)要問(wèn)自己三個(gè)問(wèn)題。第一誤差是否集中在云團(tuán)過(guò)境時(shí)段功率劇烈波動(dòng)的區(qū)間如果是考慮增加分鐘級(jí)輻照度變化率特征。第二是否有明顯的「早晚系統(tǒng)性偏低或偏高」趨勢(shì)可能是特征里缺少溫度對(duì)組件效率的影響。第三連續(xù)陰雨天誤差是否比晴天大很多如果是需要檢查數(shù)值天氣預(yù)報(bào)的云量特征是否進(jìn)了模型。4.3 隨機(jī)森林、XGBoost 的對(duì)照組怎么快速搭為了證明你最終選的模型是「認(rèn)真比較過(guò)的」而不是「拍腦袋選的」項(xiàng)目里通常需要幾個(gè)對(duì)照組的指標(biāo)。用一個(gè)循環(huán)把所有樹(shù)模型跑一遍是最省事的做法from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor results {} # 隨機(jī)森林 rf RandomForestRegressor( n_estimators200, max_depth10, min_samples_leaf5, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) results[RandomForest] { mae: mean_absolute_error(y_test, y_pred_rf), rmse: np.sqrt(mean_squared_error(y_test, y_pred_rf)) } # XGBoost xgb_model XGBRegressor( n_estimators300, learning_rate0.05, max_depth7, subsample0.8, colsample_bytree0.8, random_state42, n_jobs-1 ) xgb_model.fit(X_train, y_train, verboseFalse) y_pred_xgb xgb_model.predict(X_test) results[XGBoost] { mae: mean_absolute_error(y_test, y_pred_xgb), rmse: np.sqrt(mean_squared_error(y_test, y_pred_xgb)) } # 打印對(duì)比表 for name, metric in results.items(): print(f{name:12s} MAE{metric[mae]:.2f} kW RMSE{metric[rmse]:.2f} kW)邏輯說(shuō)明這一節(jié)展示了標(biāo)準(zhǔn)化操作的寫(xiě)法——每個(gè)模型用同樣的訓(xùn)練集測(cè)試集、同樣的評(píng)估函數(shù)這樣出來(lái)的對(duì)比才有說(shuō)服力。n_jobs-1表示使用所有 CPU 核心并行訓(xùn)練隨機(jī)森林和 XGBoost 都能吃滿多核。需要補(bǔ)一句經(jīng)驗(yàn)值如果 LightGBM 的 MAE 在 30 kW 左右隨機(jī)森林一般在 35-40 kWXGBoost 在 30-35 kW 之間。如果你的結(jié)果差距遠(yuǎn)大于這個(gè)范圍先檢查特征工程是否一致而不是急著調(diào)參。5. 光伏功率預(yù)測(cè)的常見(jiàn)問(wèn)題與避坑指南5.1 夜間零功率段污染訓(xùn)練集模型被「帶偏」現(xiàn)象模型預(yù)測(cè)結(jié)果在清晨和傍晚時(shí)段出現(xiàn)負(fù)值或者白天的預(yù)測(cè)曲線整體被壓低。查看訓(xùn)練數(shù)據(jù)分布時(shí)會(huì)發(fā)現(xiàn)零值樣本占了將近一半。原因光伏電站夜間不發(fā)電功率記錄是 0但輻照度很可能也接近 0這些樣本的特征分布是「低輻照度、零功率」占樣本總量 50% 左右。如果直接訓(xùn)練模型會(huì)發(fā)現(xiàn)「大多數(shù)時(shí)候輸出 0 的損失很小」從而傾向于輸出一個(gè)偏保守的預(yù)測(cè)值白天高功率段的權(quán)重被稀釋。解決訓(xùn)練時(shí)過(guò)濾掉夜間時(shí)段的數(shù)據(jù)或者至少過(guò)濾掉輻照度低于閾值的樣本。常見(jiàn)做法是只保留小時(shí)在 5 點(diǎn)到 20 點(diǎn)之間的數(shù)據(jù)另一個(gè)做法是給零功率段樣本降權(quán)。但注意預(yù)測(cè)時(shí)仍然要處理夜間邊界——把模型輸出小于 0 的預(yù)測(cè)值直接 clip 到 0因?yàn)樨?fù)功率在物理上沒(méi)有意義。# 方案1按時(shí)段硬過(guò)濾 df df[df.index.hour.between(5, 20)] # 方案2按輻照度閾值過(guò)濾更通用不受季節(jié)日照時(shí)長(zhǎng)影響 df df[df[ghi] 10] # 輻照度低于10 W/m2 視為無(wú)效 # 預(yù)測(cè)后處理負(fù)值歸零 y_pred model.predict(X_test) y_pred[y_pred 0] 0注意上面這幾種操作是互補(bǔ)的而不是互斥的我用的時(shí)候通常先做輻照度閾值過(guò)濾再在預(yù)測(cè)端做 clip兩道保險(xiǎn)。5.2 數(shù)據(jù)時(shí)間戳對(duì)齊問(wèn)題氣象數(shù)據(jù)和功率數(shù)據(jù)差了一個(gè)小時(shí)現(xiàn)象測(cè)試集上模型的 R2 在 0.95 以上但換成新數(shù)據(jù)后效果崩塌誤差翻倍。查看時(shí)間序列時(shí)發(fā)現(xiàn)輻照度曲線的峰值比功率曲線峰值早了或晚了一段固定時(shí)間。原因光伏電站的 SCADA 系統(tǒng)和氣象站的數(shù)據(jù)采集頻率不同氣象數(shù)據(jù)可能是整點(diǎn)采集功率數(shù)據(jù)是每 15 分鐘采集合并時(shí)索引沒(méi)對(duì)齊就產(chǎn)生了系統(tǒng)性偏移。輻照度提前一小時(shí)的值被當(dāng)作當(dāng)前值輸入等于給模型喂了「未來(lái)數(shù)據(jù)」。解決合并數(shù)據(jù)時(shí)用pd.merge_asof做時(shí)間戳近似匹配并做偏移檢查。具體操作是把兩條序列畫(huà)在一張圖上看輻照度峰值與功率峰值是否在同一時(shí)刻如果有固定偏移把它作為 shift 參數(shù)校準(zhǔn)回來(lái)。# 檢查輻照度和功率的峰值時(shí)間偏移 ghi_peak_time df.loc[df[ghi].idxmax()].time power_peak_time df.loc[df[power_kw].idxmax()].time print(f輻照度峰值時(shí)間: {ghi_peak_time}) print(f功率峰值時(shí)間: {power_peak_time}) # 如果發(fā)現(xiàn)功率曲線滯后于輻照度曲線需要將輻照度shift到對(duì)齊 offset int((power_peak_time - ghi_peak_time).total_seconds() / 900) # 按15分鐘粒度計(jì)算 df[ghi_aligned] df[ghi].shift(-offset) # 正數(shù)表示輻照度提前需要向后平移這一步做完之后務(wù)必重新訓(xùn)練模型驗(yàn)證效果因?yàn)樘卣鲗?duì)齊方式的修正會(huì)改變所有特征的重要性排序這是最容易被忽視的「隱性 bug」。5.3 LSTM 訓(xùn)練結(jié)果不穩(wěn)定每次跑出來(lái)的指標(biāo)都不同現(xiàn)象同一個(gè)訓(xùn)練腳本連續(xù)跑三次三次的 MAE 波動(dòng)超過(guò) 15%有時(shí)候甚至出現(xiàn)訓(xùn)練不收斂、loss 變成 NaN 的情況。原因LSTM 的權(quán)重初始化是隨機(jī)的加上光伏數(shù)據(jù)的噪聲較大模型容易收斂到不同的局部最優(yōu)。另外輸入數(shù)據(jù)沒(méi)有歸一化時(shí)lstm 內(nèi)部的狀態(tài)計(jì)算會(huì)因?yàn)閿?shù)值范圍過(guò)大導(dǎo)致梯度爆炸輸出 NaN。解決固定隨機(jī)種子、規(guī)范化輸入數(shù)據(jù)、降低學(xué)習(xí)率、增加早停耐心。如果專業(yè)一點(diǎn)可以試試在 LSTM 層之前加 BatchNormalization或者改用 GRU參數(shù)更少、訓(xùn)練更穩(wěn)定。多跑幾次取平均值也是行內(nèi)做實(shí)驗(yàn)的常見(jiàn)習(xí)慣——本來(lái)深度模型就帶隨機(jī)性單次結(jié)果不能說(shuō)明問(wèn)題。import tensorflow as tf import random # 固定所有隨機(jī)源 random.seed(42) np.random.seed(42) tf.random.set_seed(42)把上面這段放在import tensorflow之后、模型構(gòu)建之前能在大多數(shù)情況下復(fù)現(xiàn)結(jié)果。但如果換了 CPU/GPU 環(huán)境同樣的種子也可能得到不同結(jié)果——這是平臺(tái)的浮點(diǎn)運(yùn)算差異。5.4 預(yù)測(cè)曲線比真實(shí)曲線「平滑」太多峰谷抓不住現(xiàn)象預(yù)測(cè)曲線在晴天正午時(shí)段明顯低于真實(shí)值在早晚時(shí)段又略高于真實(shí)值整體看起來(lái)像「被壓縮過(guò)」的版本。誤差集中在功率快速爬升和下降的階段。原因樹(shù)模型本質(zhì)上在做特征空間內(nèi)的均值回歸對(duì)于極端值如正午接近滿發(fā)功率的樣本特征組合可能沒(méi)有覆蓋到足夠多的訓(xùn)練樣本模型傾向于輸出一個(gè)溫和的中間值。這本質(zhì)上是回歸模型的「趨中」特性不是 bug。解決第一檢查特征里是否缺少「前一天同時(shí)刻功率」這個(gè)最強(qiáng)特征第二嘗試分位數(shù)損失函數(shù)比如 LightGBM 的objectivequantile配合alpha0.5來(lái)預(yù)測(cè)中位數(shù)或者用alpha0.9預(yù)測(cè)上分位數(shù)作為「可能達(dá)到的最大出力」第三增加與云量相關(guān)的特征讓模型在高云量時(shí)敢于預(yù)測(cè)更低的輸出、在低云量時(shí)敢于預(yù)測(cè)更高的輸出。5.5 雨天、陰天等非晴天的預(yù)測(cè)誤差系統(tǒng)性偏大現(xiàn)象把誤差按天氣類型分組統(tǒng)計(jì)后發(fā)現(xiàn)晴天的 MAE 只有 20 kW陰雨天卻高達(dá) 80 kW模型在所有非晴天場(chǎng)景下嚴(yán)重低估或高估。原因訓(xùn)練數(shù)據(jù)中晴天的樣本占比過(guò)高大多數(shù)地區(qū)晴天數(shù)遠(yuǎn)多于陰雨天模型學(xué)到了「晴天模式」對(duì)陰雨天的低輻照度高波動(dòng)場(chǎng)景沒(méi)有足夠的樣本支撐。此外部分項(xiàng)目的氣象特征來(lái)自天氣預(yù)報(bào)其輻照度數(shù)據(jù)本身就是預(yù)測(cè)值已經(jīng)帶了誤差。解決做法通常是按天氣類型分層采樣或加重陰雨天的訓(xùn)練權(quán)重但更實(shí)際的做法是把「天氣類型」作為分桶條件訓(xùn)練多個(gè)模型——晴天模型和陰雨天模型分開(kāi)推斷時(shí)先用一個(gè)分類器判斷當(dāng)前天氣類型再走對(duì)應(yīng)的功率預(yù)測(cè)模型。這個(gè)「分類-回歸」兩階段的思路在行內(nèi)多個(gè)光伏預(yù)測(cè)項(xiàng)目里被驗(yàn)證有效精度提升通常在 10% 以上。6. 從「跑通」到「用起來(lái)」三個(gè)能提升預(yù)測(cè)精度的進(jìn)階操作第一個(gè)值得做的操作是引入數(shù)值天氣預(yù)報(bào)變量的滯后差分。很多項(xiàng)目只把天氣變量的原始值直接作為特征但光伏功率對(duì)輻照度的「變化趨勢(shì)」比對(duì)「絕對(duì)值」更敏感——云團(tuán)接近時(shí)輻照度會(huì)連續(xù)下降這個(gè)趨勢(shì)信息比單純「當(dāng)前輻照度是多少」更有預(yù)測(cè)價(jià)值。構(gòu)造時(shí)對(duì) GHI 做差分df[ghi_diff] df[ghi].diff(1)再把差分值也放進(jìn)特征列表。效果通常能帶來(lái) 3% 到 5% 的誤差下降成本幾乎為零一行代碼我測(cè)試過(guò)的項(xiàng)目里沒(méi)有例外。第二個(gè)是分時(shí)段建模。既然早晚爬坡、正午滿發(fā)、傍晚下坡三種狀態(tài)的功率特性差異懸殊讓一個(gè)模型硬扛所有時(shí)段是不合理的。我習(xí)慣按小時(shí)劃分成 4 個(gè)區(qū)間如 5-9 點(diǎn)、9-14 點(diǎn)、14-18 點(diǎn)、18-21 點(diǎn)分別訓(xùn)練模型預(yù)測(cè)時(shí)根據(jù)預(yù)測(cè)目標(biāo)時(shí)刻落在哪個(gè)區(qū)間走哪個(gè)模型。代價(jià)是訓(xùn)練 4 個(gè)模型換來(lái)的是每個(gè)模型只需要學(xué)「一段曲線」MAE 可以額外降 5% 到 8%。第三個(gè)是預(yù)測(cè)結(jié)果的滾動(dòng)修正。在超短期預(yù)測(cè)場(chǎng)景里每次新的功率實(shí)測(cè)值出來(lái)后用它和昨天的偏差來(lái)修正當(dāng)前對(duì)未來(lái)幾小時(shí)的預(yù)測(cè)。原理很簡(jiǎn)單如果今天前 30 分鐘的實(shí)際出力比模型預(yù)測(cè)偏高 10%未來(lái)幾小時(shí)大概率繼續(xù)偏高。具體做法是計(jì)算當(dāng)前實(shí)測(cè)功率與預(yù)測(cè)功率的比值作為修正系數(shù)乘到后面的預(yù)測(cè)序列上# 假設(shè) model 已經(jīng)訓(xùn)練好base_pred 是模型給出的未來(lái)4小時(shí)預(yù)測(cè) # current_actual 是當(dāng)前時(shí)刻的實(shí)測(cè)功率current_pred 是當(dāng)前時(shí)刻的模型預(yù)測(cè) correction_factor current_actual / current_pred corrected_forecast base_pred * correction_factor對(duì)晴天來(lái)說(shuō)修正系數(shù)穩(wěn)定在 1.0 附近對(duì)擁有大面積云團(tuán)的天氣來(lái)說(shuō)修正系數(shù)可能從 0.6 波動(dòng)到 1.4滾動(dòng)修正能顯著降低這種場(chǎng)景下的誤差。最后說(shuō)個(gè)養(yǎng)成習(xí)慣任何光伏功率預(yù)測(cè)模型交付之前必須用「最近一周的實(shí)測(cè)數(shù)據(jù)」做一個(gè)模擬在線測(cè)試——只給模型一個(gè)時(shí)間點(diǎn)之前的數(shù)據(jù)讓它逐個(gè)預(yù)測(cè)后面的點(diǎn)每一步把真實(shí)值補(bǔ)進(jìn)去更新特征。這種「模擬在線」和「一次性切分測(cè)試」的結(jié)果差異往往超過(guò) 20%如果你只跑了離線切分就匆忙上線大概率要在真實(shí)場(chǎng)景里翻車。我早年在某市的光伏電站項(xiàng)目上吃過(guò)這個(gè)虧離線指標(biāo)好看得很上線第二天連續(xù)陰雨天就被調(diào)度那邊打來(lái)電話問(wèn)「怎么偏了這么多」。從那以后模擬在線驗(yàn)證成了固定動(dòng)作。希望這篇的內(nèi)容能幫你把光伏功率預(yù)測(cè)這個(gè)方向走通在數(shù)據(jù)、特征、模型、驗(yàn)證這幾個(gè)環(huán)節(jié)上少踩幾個(gè)坑。本文還有配套的精品資源點(diǎn)擊獲取