简介:面向风电光伏功率预测与人工智能竞赛的综合资源包,整合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_splits=5) for train_idx, valid_idx in tscv.split(X): model = lgb.LGBMRegressor( objective="regression", n_estimators=800, learning_rate=0.05, num_leaves=63, subsample=0.8, colsample_bytree=0.8, random_state=42 ) 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_estimators=800只作为上限,实际迭代次数由early_stopping(50)决定,也就是连续50轮验证集RMSE不下降就停。num_leaves=63控制树的复杂度,叶子太多容易拟合噪声,太少则欠拟合。subsample和colsample_bytree都取0.8,是给树模型加一点随机性,配合早停降低过拟合风险。
跑通这段,你就有一个能提交的分数了。下一步才是把特征做厚,把分数往下压。
3. 数据清洗与特征工程:把时序问题改造成监督学习
3.1 滑窗与滞后特征:把一条时间序列变成一张表
竞赛拿到的原始数据不是现成的特征表,而是一条或多条时间序列。树模型不认序列,只认“一行样本、若干列特征”,所以要先把序列改造成有监督的表格。最常用的手段是构造滞后特征:用过去若干个时间点的功率当作当前时刻的特征。比如预测15分钟后的功率,就把15分钟前、30分钟前、1小时前、24小时前的功率都取出来当成几列。
df = df.sort_values("time").reset_index(drop=True) step = 15 # 数据分辨率,单位分钟 for shift in [1, 2, 4, 8, 96]: lag = shift * step # 对应15/30/60/120分钟/24小时 df[f"lag_{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_periods=1).mean() ) df["power_roll_std_1h"] = df.groupby("site_id")["power"].transform( lambda x: x.rolling(4, min_periods=1).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(method="linear", limit_direction="both") 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( latitude=s["latitude"], longitude=s["longitude"], tz="Asia/Shanghai", altitude=s["altitude"] ) times = pd.date_range("2022-06-01", "2022-06-03", freq="15min", tz=loc.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(objective="regression", n_estimators=300, learning_rate=0.03, num_leaves=31) 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 预测曲线检查:提交前画一张图比看十行日志有用
每次提交前,我会随机抽几个场站和几天时间,把真实功率曲线和预测功率曲线叠在一起打印出来。重点看三个地方:早上的爬坡是不是慢了半小时、中午的峰是不是被压平了、傍晚的落坡是不是拖了尾巴。如果预测曲线整体比真实曲线滞后,说明滞后特征权重太大,模型在依赖上一时刻功率而不是气象变化;如果峰被明显压平,说明特征里缺少辐照度,或者模型对强辐照时段拟合不足。
我个人的习惯是把可视化脚本放在提交脚本同一个目录里,每次跑完训练不去看分数就去看曲线。分数会骗人,曲线不会。把检查做成固定动作之后,我翻车的次数明显少了,也希望这个习惯能帮到你少踩几个坑。
本文还有配套的精品资源,点击获取