光伏发电量预测竞赛实战:时序特征、模型集成与避坑指南
简介面向风电光伏功率预测与人工智能竞赛的综合资源包整合DataFountain光伏发电量预测、百度KDD杯2022、国能日新光伏竞赛等赛题内容适合参赛学生、算法工程师及新能源预测研究人员使用。压缩包共258个文件大小约139.69MB以120个Python脚本和38个CSV数据文件为核心另有多个Jupyter Notebook、模型权重文件pth/h5、pkl序列化对象及其他配置文件覆盖从气象与发电数据预处理、特征构造到Seq2Seq等深度学习模型训练评估的完整链路。已有212人学习下载。资源中的photovoltaics、Wind_predict_seq2seq、PVPredict等代码库提供了可直接运行的基线方案包含电站气象数据、历史发电数据及训练好的模型权重便于读者复现竞赛思路并用于二次开发同时多类文件也便于初学者按模块学习数据分析和时序预测的具体实现在较短周期内搭建起自己的功率预测实验并验证不同算法效果。1. 风电光伏人工智能竞赛一场把预测精度卷到小数点后两位的持久战风电光伏人工智能竞赛DataFountain光伏发电量预测、百度KDD杯2022这类新能源功率预测赛题这几年几乎成了数据竞赛的常规项目。表面看是电气题实际比的是时序特征处理和模型集成的功夫同一份公开数据、同一条RMSE指标、同一批卡线技巧。第一次参加的人往往抱着LSTM冲进去跑完才发现前排方案大多是LightGBM加残差修正。下面按我自己的参赛路径来拆赛题怎么读、数据怎么清洗、光伏第三方库怎么用、模型怎么选、哪些踩坑会让你翻车。适合想找人工智能项目实战选题或把毕业设计押在新能源预测赛道上的人。2. 赛题拆解光伏发电量预测到底在预测什么2.1 从气象变量到功率曲线赛题给了什么、要预测什么我见过的光伏发电量预测赛题数据基本分成三块历史功率序列、气象站观测数据、数值天气预报数据。历史功率是按15分钟一个点记录的场站实际出力单位一般是kW或MW。气象站观测给的是同一时刻的温度、湿度、风速、辐照度这些实测值。数值天气预报给的是未来一段时间的预报值这是测试集里唯一可靠的输入决定了模型最终要吃的是“预报气象”而不是“实测气象”。赛题要求一般是这样给你过去N天的数据预测未来M个小时的功率曲线分辨率和训练数据一致。有的还带场站编号多个场站放在一个数据集里要求按场站分别预测。这种多场站结构带来的坑是不同场站的光伏板朝向、装机容量、天气差异全混在一起如果把所有场站拼成一个模型而不加场站特征预测值会被平均到一条平淡的曲线上。目标变量的形态也值得先看一眼。光伏功率有一个硬性边界白天有出力夜间归零出力还受云遮挡影响上下抖动。预测目标不是一条光滑曲线而是一条带毛刺的、每天近似单峰的形状。先画出几天的实际功率序列再动手建模能帮你少走很多弯路。2.2 RMSE与MAE竞赛取分逻辑绝大多数这类赛题用RMSE作为主指标部分会附带MAE。RMSE对每个预测点算误差平方取平均后再开方。它和MAE最大的区别是RMSE对大的误差点惩罚更重。在光伏场景里一个预测点偏差100kW比十个点各偏10kW在RMSE上的代价高得多所以优化RMSE的模型会自动去“顾全”那些容易出大错的时间段——通常是天气剧变的时刻。如果你只看MAE调参最后RMSE往往会偏高反过来用RMSE调参MAE一般也不会太差。我一般会同时打印两个指标但早停和调参全看RMSE。还有一个细节是赛题指标有时会折算成容量MW或者百分比提交前要确认预测的单位到底要不要除以装机容量。单位错了分数会差一个量级而且这种情况在本地验证时不容易暴露。2.3 基线选择为什么LightGBM/XGBoost是多数队伍的起点面对时序数据新手第一反应是LSTM。但这类竞赛的公开榜前排却长期被LightGBM、XGBoost这类梯度提升树占据。原因是这样的表格型时序任务特征工程做好之后树模型对非线性、缺失值、量纲差异的容忍度很高训练速度快调参也不容易过拟合。先在LightGBM上跑出一个稳定分数再去尝试LSTM/GRU或者集成是性价比最高的路径。模型优点短板何时用线性回归快、可解释抓不住天气突变的非线性只做分数下限LightGBM训练快、特征友好外推能力弱主基线多数方案选它LSTM/GRU能学长时间依赖训练慢、调参玄学特征工程做完后锦上添花多模型集成稳定提分工程复杂、调试难冲榜阶段import pandas as pd import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit df pd.read_csv(train.csv, parse_dates[time]) df[hour] df[time].dt.hour df[month] df[time].dt.month feature_cols [hour, month, temperature, irradiance, humidity, wind_speed] X df[feature_cols] y df[power] tscv TimeSeriesSplit(n_splits5) for train_idx, valid_idx in tscv.split(X): model lgb.LGBMRegressor( objectiveregression, n_estimators800, learning_rate0.05, num_leaves63, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit( X.iloc[train_idx], y.iloc[train_idx], eval_set[(X.iloc[valid_idx], y.iloc[valid_idx])], callbacks[lgb.early_stopping(50)] ) break # 先切一刀看分数表现再决定要不要跑完整5折这段代码分两步。第一步从时间列拆出小时和月份把温度、辐照度、湿度、风速这几个常见气象特征接进来。第二步用TimeSeriesSplit做时序切分训练LightGBM确保训练集时间全部早于验证集。n_estimators800只作为上限实际迭代次数由early_stopping(50)决定也就是连续50轮验证集RMSE不下降就停。num_leaves63控制树的复杂度叶子太多容易拟合噪声太少则欠拟合。subsample和colsample_bytree都取0.8是给树模型加一点随机性配合早停降低过拟合风险。跑通这段你就有一个能提交的分数了。下一步才是把特征做厚把分数往下压。3. 数据清洗与特征工程把时序问题改造成监督学习3.1 滑窗与滞后特征把一条时间序列变成一张表竞赛拿到的原始数据不是现成的特征表而是一条或多条时间序列。树模型不认序列只认“一行样本、若干列特征”所以要先把序列改造成有监督的表格。最常用的手段是构造滞后特征用过去若干个时间点的功率当作当前时刻的特征。比如预测15分钟后的功率就把15分钟前、30分钟前、1小时前、24小时前的功率都取出来当成几列。df df.sort_values(time).reset_index(dropTrue) step 15 # 数据分辨率单位分钟 for shift in [1, 2, 4, 8, 96]: lag shift * step # 对应15/30/60/120分钟/24小时 df[flag_{lag}min] df.groupby(site_id)[power].shift(shift) df[power_roll_mean_1h] df.groupby(site_id)[power].transform( lambda x: x.rolling(4, min_periods1).mean() ) df[power_roll_std_1h] df.groupby(site_id)[power].transform( lambda x: x.rolling(4, min_periods1).std() )这里shift(shift)表示把功率列向后挪shift个时间点第t行的lag_15min就是t-1时刻的实际功率。shift(96)相当于取24小时前的功率因为一天有96个15分钟。groupby(site_id)必须按场站分开做不能让上一场的功率泄漏到下一场。rolling(4)取过去4个点也就是1小时的滑窗均值和标准差能描述最近一段时间的出力水平和波动程度。太阳辐照度变化剧烈时波动特征比单点特征更有区分度。注意滞后特征构造完必须做一次时间顺序校验尤其是跑过排序、去重、合并操作之后。这里错了后面所有模型分数都没有意义。3.2 气象和时间特征把“几点”“是不是阴天”告诉模型只给模型原始时间戳是不够的它学不出“日出日落”这种周期性。我一般会把时间拆成小时、月份、星期、一年的第几天这几列。小时用sin/cos编码比直接用0到23更好因为23点和0点之间的距离应该是1小时而不是23小时。月份也一样用两个三角函数列把周期性展开树模型能找到更自然的切分点。气象特征里辐照度是第一重要的其次是温度然后是湿度、风速。光伏功率基本跟着辐照度走辐照度弱时温度再高也发不出电。温度会影响光伏板的效率组件温度过高时转换效率会掉所以“高温强辐照”和“常温强辐照”的出力并不相同。风对组件有冷却效应风速高时温度对效率的拖累会小一些。df[hour_sin] np.sin(2 * np.pi * df[time].dt.hour / 24) df[hour_cos] np.cos(2 * np.pi * df[time].dt.hour / 24) df[dayofyear] df[time].dt.dayofyear df[day_sin] np.sin(2 * np.pi * df[dayofyear] / 365) df[day_cos] np.cos(2 * np.pi * df[dayofyear] / 365)把这几个时间周期特征和前一天的滞后功率、气象特征合并进训练表树模型就能学到“夏天上午9点、辐照度600、昨天同一时刻功率300”这种组合被映射成“今天上午9点功率350”。注意dayofyear和month不要同时用它们信息高度冗余会让树的切分变得更碎。如果赛题数据跨多年dayofyear比month更精细能区分同一月份的不同位置。3.3 缺失值处理夜间功率和停测数据怎么填光伏数据缺失有两类常见情况。一类是夜间功率本来就该接近0有些数据集在夜间记录为NaN这种情况不能直接填平均值填了会让白天夜间的边界糊掉。另一类是气象站临时停测一段连续几个小时的温度或辐照度全为空这类要靠前后插值。df[power] df.groupby(site_id)[power].transform(lambda x: x.fillna(0)) df[irradiance] df[irradiance].interpolate(methodlinear, limit_directionboth) df[temperature] df[temperature].ffill().bfill()功率填0有一个前提你要先确认缺失的时间段都在夜间。如果白天也有缺失得先看是不是整场停发再决定要不要用前后平均值。辐照度用线性插值只适合短缺口缺口超过6个小时插值和乱猜没区别。如果是带场站编号的数据插值也最好按场站分组做两个场的天气差异会直接导致插值结果失真。处理完缺失还要做一步校验把每条样本的滞后特征检查一遍看有没有因为排序错误导致未来值混进来。数据竞赛里最常见的翻车不是模型不行而是特征错位。模型可以调数据错了你只会对着一个高分反复怀疑人生。4. 光伏第三方库与模型方案pvlib和它替代不了的部分4.1 pvlib能算什么太阳位置与辐照度分解标题里提到光伏第三方库做这类赛题绕不开的是pvlib。它是光伏系统仿真领域比较常用的Python库核心能力是计算太阳在任意时刻、任意经纬度下的位置也就是高度角、方位角以及把水平面总辐照度GHI分解成直接辐照度DNI和散射辐照度DHI。这些物理量在特征工程里很有用因为赛题给的气象字段往往只有GHI而光伏板倾斜面上的有效辐照度才是真正决定发电量的量。import pandas as pd import pvlib from pvlib import location, solarposition sites [ {site_id: A, latitude: 39.9, longitude: 116.4, altitude: 50}, {site_id: B, latitude: 31.2, longitude: 121.5, altitude: 5}, ] for s in sites: loc location.Location( latitudes[latitude], longitudes[longitude], tzAsia/Shanghai, altitudes[altitude] ) times pd.date_range(2022-06-01, 2022-06-03, freq15min, tzloc.tz) solpos solarposition.get_solarposition(times, loc.latitude, loc.longitude) s[solar_zenith] solpos[zenith] s[solar_azimuth] solpos[azimuth] s[solar_elevation] 90 - solpos[zenith]这段代码的核心是get_solarposition给定经纬度、时间和时区返回太阳天顶角和方位角序列。天顶角越接近90度说明太阳越接近地平线可用辐照度越低。高度角用90减天顶角得到可以直接当作特征它和光伏功率有很强的一致性。用经纬度是因为赛题数据里通常只有场站坐标或场站编号而光伏第三方库能把你从“只知道编号”变成“知道这个场站的太阳轨迹”。4.2 库计算和实测气象的取舍物理量是特征不是答案pvlib能算晴空条件下的理论辐照度也能根据GHI分解出DNI和DHI但要提醒一句它算的是“如果天空没云会怎样”不是“实际上发了多少电”。云遮挡、雾霾、场站周围遮挡物这些因素第三方库的模型算不出来。所以常规用法是把库计算结果当成物理特征叠加在气象变量旁边而不是用它替代气象观测或发电量序列。很多时候你会发现加了太阳高度角、方位角、理论晴空辐照度这几个库计算特征之后树模型在早上的爬坡段和傍晚的收尾段分得更准。原因是实测辐照度在某些赛题里只有日累积或者缺测而太阳位置是确定性的不管气象站坏没坏它都在那里。这种确定性的物理特征是“后悔药”能补上数据缺失带来的盲区。如果赛题给了倾斜面角度还可以用pvlib.irradiance.get_total_irradiance把水平面辐照度换算成倾斜面辐照度。没给角度就不强求直接让模型去学水平面辐照度和功率的关系。另外要注意时区问题赛题时间列可能是UTC也可能是本地时间用pvlib前必须统一成带时区的datetime对象不然算出来的太阳位置会整体偏移几个小时。4.3 模型选型与集成LightGBM为主、时序模型为辅特征工程做完以后我的习惯是先让LightGBM跑满再决定要不要上深度学习。深度学习在新能源功率预测上的优势是能学长序列依赖但劣势也很明显对特征数量的要求高、调参成本大、有时候还不如滚动平均稳。常见的组合是LightGBM做主力LSTM/GRU做一个独立预测最后用加权平均或者再训练一个回归器把两个模型的预测拼起来。梯度提升树在表格特征上的表现很稳但树模型有一个天生短板它不能隐式地利用时间顺序。这也是为什么滞后特征那么关键——没有滞后特征树模型只能看到当前气象看不到“昨天这个时候发了多少电”。把滞后功率、滑动统计和气象特征喂进去之后LightGBM基本能摸到功率曲线的骨架。模型策略使用场景分数增益风险单LightGBM快速出分基准提升有限LightGBM加滞后滚动特征正式提分中注意特征泄漏LightGBM加LSTM加权冲榜小训练慢、过拟合多折交叉验证加残差修正最终提交稳定0.5%到1%代码复杂度高集成的时候注意两点。第一两个模型的验证集划分必须完全一致不然分数叠加起来没有任何意义。第二加权平均的权重不要用试出来的“最优值”硬套最好用小范围网格搜索权重变化0.1对最终RMSE的影响在竞赛排名里可能就是几个名次。如果时间允许把训练好的模型保存成pickle最后统一加载做集成别在预测阶段重新训练。5. 光伏预测竞赛避坑四个让我翻过车的环节注意以下四条都是实际踩过的坑按翻车频率排序每一条都能让线上分数直接报废。5.1 现象验证集RMSE很低线上分数崩了原因是用随机切分代替了时间切分。很多人图省事直接用train_test_split训练集里混着测试集时间段的数据模型学到的不是规律而是记忆。光伏发电受天气影响大相邻两个星期的数据高度相关乱序切分会让验证集被剧透打分虚高。解决方法是坚持用时间顺序切分。先按时间排序前70%做训练后30%做验证。如果要更严谨用TimeSeriesSplit做多折每一折都必须保证训练集时间完全在验证集之前。提交前再检查一下验证集最后一个时间点是否小于测试集第一个时间点。失败时看什么如果本地分数和线上分数差超过一倍第一个怀疑切分方式。5.2 现象用了辐照度实测值预测未来分数高得离谱原因是测试集的天气特征如果给的是数值天气预报的未来值那么训练时也应该只用NWP的相应字段。很多赛题会把实测气象列放在训练集里而测试集只有预报气象如果把实测温度、实测辐照度做成特征训练时模型看到的是精确的未来测试时只能拿到预报的未来分布直接错位。线上分数越高越要警惕。解决方法是先读赛题说明搞清楚气象列到底是观测还是预报。如果测试集用的是预报训练时就不要把实测气象当作强特征或者干脆跑一个不含实测气象的版本做对照。我一般会同时跑两个版本一个含实测气象一个不含用不含的版本作为最终预测基准因为它在数据分布上更接近测试环境。失败时看什么把验证集切到测试集同一个月看分数是否仍然异常高。5.3 现象凌晨时段预测出负功率原因是模型在拟合RMSE时为了让强辐照时段的误差更小会在弱辐照时段输出略微低于0的值。光伏功率物理上不可能为负但回归模型不管这个边界它只管数学上最小化误差。凌晨和傍晚的功率接近0模型很容易在这个区间过冲。解决方法是预测后做截断把所有负值全部置为0。别小看这一步很多赛题的分数差距就出在这里。也可以把功率除以装机容量得到0到1之间的容量因子用sigmoid输出配合二值交叉熵做辅助。最简单的做法还是np.clip(pred, 0, None)跑一遍就能看到RMSE大概率下降。失败时看什么如果截断后分数反而变差说明你预测的不是功率而是容量因子回去检查单位换算。5.4 现象提交文件行数对不上或者列顺序不对原因是预测序列和原始测试表顺序被排序或分组操作打乱了。数据清洗时做过sort_values训练时又按场站分组最后预测结果可能和官方给的提交模板顺序不一致导致把A场的预测填到了B场。这种问题不会在本地报错只在评分系统里反馈行数错误或分数异常低。解决方法是维护一个提交模板DataFrame把所有做过的排序、分组操作都反向恢复最终按模板的索引顺序输出。提交前用代码检查三样东西行数是否等于测试行数、索引是否完全一致、有没有NaN。把这步写成一个固定脚本每次提交前跑一遍能省掉很多次无效提交。失败时看什么如果官方提示行数不匹配先检查reset_index的位置大概率是它。6. 用残差修正和曲线检查把精度再压一档6.1 残差修正第二次学习抓的是模型没学会的尾部误差到冲刺阶段单模型分数往往很难降了。这时候我会用残差修正先把LightGBM的验证集预测值算出来计算真实值和预测值的差再用这些残差去训练第二个模型。第二个模型的输入可以只选气象特征和时间特征也可以把第一个模型的预测值作为特征加进去。最后提交的预测值是第一个模型的输出加上第二个模型的残差预测。from sklearn.metrics import mean_squared_error pred_train model.predict(X_train) residual y_train - pred_train model_res lgb.LGBMRegressor(objectiveregression, n_estimators300, learning_rate0.03, num_leaves31) model_res.fit(X_train_res, residual, eval_set[(X_valid_res, y_valid - pred_valid)], callbacks[lgb.early_stopping(30)])注意第二个模型如果输入里包含第一个模型的预测值要避免残差模型在训练和验证时用不同来源的预测值导致偏差。我一般会用验证集上第一个模型的预测结果来训练残差模型保证分布一致。残差修正的提升幅度通常只有1%以内但在高手如云的赛题里这1%可能就是几十个名次的差距值得做。6.2 预测曲线检查提交前画一张图比看十行日志有用每次提交前我会随机抽几个场站和几天时间把真实功率曲线和预测功率曲线叠在一起打印出来。重点看三个地方早上的爬坡是不是慢了半小时、中午的峰是不是被压平了、傍晚的落坡是不是拖了尾巴。如果预测曲线整体比真实曲线滞后说明滞后特征权重太大模型在依赖上一时刻功率而不是气象变化如果峰被明显压平说明特征里缺少辐照度或者模型对强辐照时段拟合不足。我个人的习惯是把可视化脚本放在提交脚本同一个目录里每次跑完训练不去看分数就去看曲线。分数会骗人曲线不会。把检查做成固定动作之后我翻车的次数明显少了也希望这个习惯能帮到你少踩几个坑。本文还有配套的精品资源点击获取