分布式光伏出力预测:气象因子筛选与模型构建实战
简介面向光伏功率预测研究与应用的一份完整代码与数据资源针对分布式光伏发电中气象因子复杂、功率序列非平稳等问题提供了基于经验模态分解、主成分分析与长短期记忆网络的组合预测模型实现。资源共8个文件包括4个MATLAB脚本分别承担预测主程序、风向处理、结果预测及指标评价等功能、3个mat格式的5分钟采样间隔实测数据集涵盖风速、温度、功率等关键变量以及1个附加zip压缩包整体仅116KB结构清晰轻量便于快速部署。目前已有209人学习下载。解压后可直接运行预测流程利用山西某电站8个月实测数据完成环境因子序列分解、主成分关键影响因子提取与LSTM动态时间建模并通过指标脚本定量评估预测精度该资源既适合作为算法改进的基线方案也可用于光伏功率预测精度提升方面的实验对比、论文复现或课程设计能够帮助使用者系统理解数据预处理与深度时序预测的完整链路具有较强的工程参考价值。1. 拿到“分布式光伏发电计及气象因子及出力预测方法研究.zip”要先做对哪几件事把“分布式光伏发电计及气象因子及出力预测方法研究.zip”解压后你面对的其实不是一套能直接跑的软件而是一套典型的研究工程包里面通常有历史出力、气象站数据和若干模型文件。这份研究真正想解决的问题是提前4小时甚至更短时间尺度上把屋顶光伏电站的出力预测误差压到调度能接受的范围。分布式光伏和集中式电站不一样装机分散、数据采集点又多又杂气象因子几乎是唯一能跨站点迁移的特征来源所以气象因子处理得对不对直接决定后面所有模型的上限。适合读这篇的人是做光伏并网调度、微网能量管理、电站运维或者打算入行新能源预测的从业者看完至少能理清从原始数据到可部署预测模型的完整链路。2. 气象因子怎么挑先做相关性分析再决定要不要上云量2.1 把辐照度、温度、湿度、风速按时间对齐到同一张表这类研究包里的数据格式差异很大有的出力表是15分钟一个点气象站数据却是小时级的还有的是5分钟级。第一步不是建模而是把两张表对齐到统一时间分辨率。我一般先统一到15分钟因为分布式光伏的出力波动主要发生在分钟级15分钟既够用又能兼顾数值天气预报NWP的更新频率。import pandas as pd import numpy as np # 出力数据csv里通常有 timestamp, power(kW) power pd.read_csv(pv_power.csv, parse_dates[timestamp]) # 气象数据常见字段timestamp, ghi(W/m2), temp, humi, wind weather pd.read_csv(weather.csv, parse_dates[timestamp]) # 统一为15分钟分辨率取每个15分钟窗口的均值 power (power.set_index(timestamp) .resample(15min).mean() .reset_index()) weather (weather.set_index(timestamp) .resample(15min).mean() .reset_index()) # 用merge_asof做时间对齐允许前后2分钟偏差 # 注意两张表都必须先按timestamp升序排好 power power.sort_values(timestamp) weather weather.sort_values(timestamp) df pd.merge_asof(power, weather, ontimestamp, tolerancepd.Timedelta(2min)) # 看缺失情况气象站宕机或采集器掉线都会留下空洞 print(df.isna().mean().sort_values(ascendingFalse))这段代码的核心是merge_asof它不是精确匹配而是找“最接近的过去时刻”很适合两张表频率不一致的场景。tolerance2min的意思是如果气象站记录比出力记录晚超过2分钟就不匹配避免把毫无关联的读数硬凑在一起。resample(15min).mean()则把高频数据压到低频取均值能压制瞬时噪声。指标对齐后一定要看缺失率气象站停测、辐照仪被鸟粪遮住、采集器离线都会产生空洞。缺失率超过20%的因子建议直接丢掉不要靠插值硬补因为气象因子的空间相关性极强插出来的数据看着合理实际会把模型学歪。2.2 用 Spearman 相关挑因子别只盯皮尔逊很多人拿到数据先算了 Pearson 相关系数发现辐照度和出力相关性只有0.7就以为辐照度不是好特征。这个结论通常是错的因为光伏出力对辐照度是分段响应低辐照度时光电转换效率低高辐照度时又可能因为组件温度升高效率下降整个关系接近饱和曲线线性相关的假设从根上就不成立。所以我一般用 Spearman 秩相关做初筛它只看排序一致性对非线性关系更敏感。# 先看因子与出力的Spearman相关 cols [ghi, temp, humi, wind, power] corr df[cols].corr(methodspearman) print(corr[power].sort_values(ascendingFalse)) # 再看辐照度对出力的滞后相关性确认时滞范围 print(\n滞后相关性分钟: 相关系数) for lag_min in range(0, 121, 15): lagged_ghi df[ghi].shift(lag_min // 15) valid pd.concat([df[power], lagged_ghi], axis1).dropna() r valid[power].corr(valid[ghi], methodspearman) print(f{lag_min}: {r:.4f})shift(lag_min // 15)做的事情是把辐照度序列向后挪若干步然后计算“过去某时刻的辐照度”与当前出力的相关性。因为热容量效应组件温度对辐照度的响应不是瞬时的通常会有0到30分钟的滞后滞后相关曲线最高点对应的滞后期可以作为后续构造滞后特征的参考。从结果看ghi 的 Spearman 相关一般能到0.85以上temp 在0.3到0.5之间humi 是负相关wind 在屋面上往往只有0.1左右。湿度负相关是有物理背景的高湿度通常伴随云层增厚削弱了到达地表的直接辐射。风速的正向作用主要体现在组件散热上但屋顶电站周围遮挡物多风场紊乱风速计读数和组件实际散热状态经常对不上所以风速在分布式场景下常常是要被砍掉的那一个。2.3 云量这个变量有云图数据就用没有就别硬补云量是分布式光伏预测里讨论最多、落地最坑的因子。卫星云图的分辨率通常十几公里起步而一个屋顶电站的受云影响范围可能只有几百米云图显示“多云”和屋顶上空正好飘过一块云完全是两码事。数值天气预报里的总云量对超短期预测15分钟到4小时有一定价值但对当天出力峰值的预测时效经常跟不上。常见的替代做法是构造“辐照度变化率”特征用过去几个时间步的辐照度差分来表征云层过境的动态过程。# 辐照度变化率当前值与15分钟前差值的比例 df[ghi_diff] df[ghi].diff() df[ghi_rate] df[ghi_diff] / (df[ghi].shift() 1e-6) # 云层过境往往表现为高频正负交替 df[ghi_volatility] df[ghi].rolling(window6).std()ghi_rate的物理含义是辐照度在15分钟内的相对变化正值代表云层正在散开负值代表云正在遮住太阳绝对值大说明天空状态正在剧烈切换。ghi_volatility用滚动标准差描述过去90分钟的辐照度震荡幅度晴天接近0层积云天气会很高。这两个特征在实际模型里的重要性经常排进前三比直接用数值天气预报云量稳定得多。卫星云图数据如果站点恰好有可以作为附加特征但不要把它当主力因为云图更新时间通常15分钟到1小时等它反映到数据里云的影子已经飘过三个电站了。3. 把时间序列切成样本滑窗、滞后与数据集构造3.1 不是喂一整条序列而是喂成“过去90分钟预测未来1小时”的样本预测任务要先定义清楚用过去多长的历史数据预测未来多长的时间。我一般默认做“过去90分钟6个点预测未来60分钟4个点”这个配置在调度场景里最常用既给了模型足够的历史上下文又不会因为预测步长过长导致误差失控。import numpy as np def make_samples(df, feature_cols, past_steps6, future_steps4): X, y [], [] values df[feature_cols].values for i in range(past_steps, len(values) - future_steps): X.append(values[i - past_steps:i]) y.append(df[power].values[i future_steps - 1]) return np.array(X), np.array(y) feature_cols [ghi, ghi_rate, temp, humi, hour_sin, hour_cos, power] X, y make_samples(df, feature_cols) print(X.shape, y.shape)这里X的形状是(样本数, 6, 7)代表每个样本有6个历史时间步每步7个特征y是未来第4个点也就是60分钟后的出力值。注意标签取了i future_steps - 1这是预测窗口的末端点不是未来第一个点。如果你想预测的是整个未来1小时的平均功率应该对4个未来点的出力取均值再作为标签。实践中还有一个选择窗口内的时间特征。hour_sin和hour_cos是把小时数值映射到单位圆上避免0点和23点的跳跃这在光伏预测里是必做项。3.2 时间序列的train/val/test必须按时间切不能随机切表格数据建模习惯用train_test_split随机切分时间序列这么切会出大问题。光伏出力有极强的季节性和天气过程连续性随机切会把夏天和冬天的样本混进训练集模型提前“见过”了未来验证集上的评估结果虚高等到真正部署时误差直接翻倍。n len(X) train_end int(n * 0.7) val_end int(n * 0.85) X_train, y_train X[:train_end], y[:train_end] X_val, y_val X[train_end:val_end], y[train_end:val_end] X_test, y_test X[val_end:], y[val_end:] print(ftrain: {len(X_train)}, val: {len(X_val)}, test: {len(X_test)})这种切分方式模拟的是真实部署场景模型用过去的数据训练然后去预测它没见过的未来时段。注意这里没有打乱顺序因为打乱会让每个样本携带的时序上下文失效。我自己吃过这个亏有一次随机切分后验证集MAPE只有12%改成时间切分后变成19%那7个点的差距就是数据泄露藏在里面的水分。3.3 归一化只fit训练集验证集和测试集不能参与计算标准化是另一个容易埋雷的环节。很多人先对整个数据集做StandardScaler再切分训练集和验证集这在时间序列里等于默认了“模型知道未来所有时刻的均值和方差”。光伏出力的均值在冬夏差别极大用全量均值做标准化等于提前告诉模型夏天的出力水平比冬天高验证误差自然好看。from sklearn.preprocessing import StandardScaler # 先把3D样本展平成2D方便做逐特征标准化 X_train_2d X_train.reshape(len(X_train), -1) X_val_2d X_val.reshape(len(X_val), -1) X_test_2d X_test.reshape(len(X_test), -1) scaler_X StandardScaler().fit(X_train_2d) X_train_s scaler_X.transform(X_train_2d).reshape(X_train.shape) X_val_s scaler_X.transform(X_val_2d).reshape(X_val.shape) X_test_s scaler_X.transform(X_test_2d).reshape(X_test.shape) scaler_y StandardScaler().fit(y_train.reshape(-1, 1)) y_train_s scaler_y.transform(y_train.reshape(-1, 1)).ravel() y_val_s scaler_y.transform(y_val.reshape(-1, 1)).ravel()逻辑很简单scaler_X和scaler_y只在训练数据上fit验证集和测试集只用同一组参数做transform。时间序列的均值和尺度是漂移的模型在部署时拿到的每一个新样本用的都应该是训练时的统计量而不是它自己时刻的统计量。还有一个容易踩的细节如果是用MinMaxScaler做归一化训练集里没出现过的极端辐照度到测试时会被裁到0或1的边界上。StandardScaler对异常值不够稳健我通常先做分位数截断比如把辐照度卡在1%到99%分位再做标准化能让极端天气下的误差更平稳。4. 出力预测模型怎么选从物理基线到轻量集成再到LSTM4.1 先立一个物理基线辐照度到出力的线性映射拿到一份新的出力预测研究包我做的第一件事永远不是训练模型而是先搭一个物理基线也就是把辐照度按组件效率折算出理论出力。这个基线的误差就是整个预测任务的天花板参考如果连基线都能做到20%误差那么任何机器学习模型只要低于这个数就算有进步。# 物理基线P GHI / 1000 * 装机容量 * 综合效率 capacity_kw 500 # 示例站点装机容量 df[baseline_power] (df[ghi] / 1000) * capacity_kw * 0.85 # 只在白天时段评估基线误差 daytime df[df[ghi] 50].dropna() rmse np.sqrt(((daytime[power] - daytime[baseline_power]) ** 2).mean()) print(f物理基线RMSE: {rmse:.2f} kW)综合效率0.85是一个经验值包含组件老化、逆变器损耗、灰尘遮挡等系统性损失具体站点的效率要从历史数据里回归出来用晴天样本做power ~ ghi的线性回归得到的斜率除以装机容量就是站点实际综合效率。这个基线在晴天表现相当能打误差通常在10%以内但在阴天和过渡天气下会崩盘因为云层把辐照度和实际出力的非线性放大到了线性模型无法描述的程度。物理基线的另一个价值是作为特征工程的参考如果基线误差大说明模型需要的是云动态信息而不是更多的气象站平均数据。4.2 树模型是气象因子的黄金搭档LightGBM是首选在样本量几万到几十万、特征以表格型气象数据为主的预测任务里LightGBM 基本是最优解。它天然处理特征交互、对缺失值有内置策略、训练快还不要求特征服从正态分布气象因子那种偏态分布辐照度大量集中在0附近不用做特殊变换就能直接用。import lightgbm as lgb # 展平窗口把6个时间步的特征拼成一条特征向量 X_train_flat X_train_s.reshape(len(X_train_s), -1) X_val_flat X_val_s.reshape(len(X_val_s), -1) train_data lgb.Dataset(X_train_flat, labely_train_s) val_data lgb.Dataset(X_val_flat, labely_val_s, referencetrain_data) params { objective: regression, metric: rmse, learning_rate: 0.05, num_leaves: 31, min_child_samples: 20, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1, } model lgb.train( params, train_data, num_boost_round500, valid_sets[val_data], callbacks[lgb.early_stopping(50), lgb.log_evaluation(50)], )num_leaves31控制树的复杂度叶子数太大会在气象数据上过拟合min_child_samples20限制每个叶子最少样本量避免学到个别异常天的噪声learning_rate0.05配合早停比默认0.1更稳。特征展平后每个时间步的特征都变成独立维度模型能看到辐照度随时间变化的模式这比只喂均值有力得多。树模型还有一个实用优势可以输出feature_importance。我每次跑完先看重要性排序如果某个气象因子的重要性一直垫底就果断从特征列表里删掉减少上线后的数据采集依赖。做过几次之后你会发现模型稳定依赖的特征就三四个辐照度、辐照度变化率、温度和小时编码其余都是锦上添花。4.3 LSTM与TCN什么时候才值得用用LSTM做光伏出力预测是研究论文里的标配但工程落地时要冷静。样本量少于3万条、预测步长不超过4小时、且以表格特征为主时LSTM经常打不过调好参的LightGBM训练时间却长一个数量级。序列模型真正有优势的场景是需要输出一条连续的预测曲线未来4小时的逐15分钟功率且希望模型自己学出时序依赖结构的时候。import torch import torch.nn as nn class PVLSTM(nn.Module): def __init__(self, input_size, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.reg nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) # 取最后一步的隐状态做回归 return self.reg(out[:, -1, :]) # 输入形状(batch, seq_len6, features7) input_size X_train_s.shape[2] model PVLSTM(input_sizeinput_size, hidden_size64, num_layers2, dropout0.2)hidden_size64在小型预测任务里够用加大到128不一定带来精度收益但训练时长明显增加。num_layers2是这类序列回归任务的常见选择更多层在数据量不大时只会放大过拟合。输入X的形状要变换成(batch, seq_len, features)PyTorch 的batch_firstTrue让代码更直观省去在forward里反复permute的麻烦。LSTM在气象因子预测里最大的坑是过拟合。光伏出力的季节性和天气过程太明显LSTM很容易把“记性”用在复现训练集的历史模式上而不是真正外推到没见过的天气。我的做法是先跑LightGBM作为基准再对LSTM做差分如果LSTM的验证误差没有比LightGBM低10%以上就直接弃用序列模型这是很务实的决策标准。5. 避坑分布式光伏预测里最容易翻车的5个地方5.1 夜间零出力被当成缺失值或异常值清掉现象清洗代码把power 0的行全部删掉训练集只剩白天数据模型在夜间和凌晨时段的预测完全跑飞。原因数据语义混淆。光伏出力的0包含两种语义夜间本来就该是0白天辐照度大于50却出力为0才是异常。解决按辐照度分时段处理夜间保留0值样本白天零值单独标记或者剔除。df[is_night] df[ghi] 10 df[is_abnormal] (~df[is_night]) (df[power] 0)保留夜间样本很重要因为模型需要学会“太阳下山后出力恒为0”这个基本规律。如果用白天样本外推夜间模型可能给出个位数的功率值看着不大但在低出力时段的相对误差会被无限放大。5.2 辐照度单位搞混W/m2和MJ/m2差出2.78倍现象模型整体预测偏高或偏低晴天的RMSE比物理基线还大且偏差比例恒定。原因气象站原始数据里有的给的是瞬时辐照度W/m2有的给的是小时累积辐照量MJ/m2两者数值上可以差近3倍不做单位换算直接混进特征模型学到的权重完全失真。解决统一转成W/m2。1MJ/m2等于277.78Wh/m2如果是1小时累积量直接除以3.6就能得到该小时的平均辐照度W/m2。这个错误隐蔽在单位换算系数上因为两个数值都已经在“看起来合理”的量级里画散点图时只能看到线性关系被拉宽很难直接察觉。我现在的习惯是在数据接入层写一个单位断言assert df[ghi].max() 1500因为地面上水平总辐照度极少数情况会短暂超过1400 W/m2超过这个阈值基本就是单位错了。5.3 晴转阴和雷阵雨时刻误差突然拉满现象整体RMSE看着不错但把误差按小时画曲线午后14点到16点有一个持续的峰值晴天转阴天时预测固执地沿着晴天的曲线往上走。原因气压、湿度等气象站数据对局地对流云的响应滞后卫星云图更新频率不足以捕捉过境云模型在“上一个时刻是晴天”的上下文里无法预判突然的遮蔽。解决加入辐照度变化率特征并单独构造“晴转阴”标记让模型有机会学出状态切换的过渡模式。这是一个无法彻底消除的误差源因为云团移动带来的辐照度变化本身就是混沌过程。但通过强化特征设计能把异常时刻的峰值压下去不少。我在实践中发现把ghi_rate绝对值大于0.4的样本单独统计误差如果这类样本的误差占比超过总量的一半说明模型还处在“只会学晴天规律”的状态需要检查特征窗口是否够长。5.4 站点时间戳时区没对齐预测曲线整体平移现象预测曲线形状和实测几乎一样但整体偏移1小时或8小时RMSE被平移误差拉高到完全不可用。原因数值天气预报数据的标准时区是UTC电站电表数据库用北京时间两张表合并时没有统一时区。常见的有两种偏移只差了整点小时数UTC8或者因为半小时日出方程计算错误导致偏差1小时。解决所有时间戳统一转成无时区的北京时间字符串再入库。df[timestamp] pd.to_datetime(df[timestamp], utcTrue) df[timestamp] df[timestamp].dt.tz_convert(Asia/Shanghai).dt.tz_localize(None)这个坑在多人协作的项目里特别常见数据生产端和数据消费端各用一套时区最后预测结果对不上。建议在数据加载阶段暴露时区参数用配置项强制标注每张表的时区来源不要让它在ETL脚本里隐式传播。5.5 归一化回代时忘了反变换指标全部失真现象训练过程中loss正常下降验证集RMSE却出现天文数字画预测曲线发现预测值被压缩在0附近几乎不随实测波动。原因标签做了标准化计算指标时直接拿标准化后的预测值和原始真实值比较量纲差了若干个标准差。还有一种是MinMaxScaler误用了全量数据的min/max导致测试集样本跨越了训练时定义的边界。解决先inverse_transform再算指标且所有无量纲化参数只fit训练集这个原则要在评估函数入口强制把关。# 回代把标准化后的预测恢复到功率单位 y_pred_orig scaler_y.inverse_transform(y_pred.reshape(-1, 1)).ravel() rmse np.sqrt(((y_test - y_pred_orig) ** 2).mean())归一化的坑往往不是单独出现它和训练集切分搅在一起。比如先在全量数据上做标准化再做切分验证集指标虚高模型上线后表现断崖。排查这类问题最快的方式是画一张真实值和预测值的散点图如果散点虽然相关但都被压缩在某个区间基本就是反归一化没做对。6. 用一份测试集检验整个流水线评估指标怎么设才算有效6.1 评估指标要按装机容量归一化MAPE在夜间不可用光伏出力预测的评估指标和普通回归任务不一样。MAPE平均绝对百分比误差在出力接近0时会爆炸夜间一段微小波动就能把误差拉高到几千倍所以不能拿全天的MAPE当唯一指标。我一般同时看四组数值RMSE看大误差惩罚MAE看平均偏差NMAE归一化MAE看相对装机容量的误差水平白天MAPE看业务感知。def evaluate(y_true, y_pred, capacity_kw): y_true np.asarray(y_true) y_pred np.asarray(y_pred) valid y_true 0.05 * capacity_kw # 只统计白天有效出力 rmse np.sqrt(((y_true[valid] - y_pred[valid]) ** 2).mean()) mae np.abs(y_true[valid] - y_pred[valid]).mean() nmae mae / capacity_kw * 100 mape (np.abs(y_true[valid] - y_pred[valid]) / y_true[valid]).mean() * 100 return {RMSE_kW: rmse, MAE_kW: mae, NMAE_%: nmae, MAPE_%: mape}注意valid y_true 0.05 * capacity_kw这一行它过滤掉了出力低于5%装机容量的时段在这些时段里相对误差没有业务意义。按容量归一化的NMAE是并网考核里最常用的指标5%以下算优秀8%到12%算可用超过15%基本会被调度拒收。6.2 一份可复用的完整评估与分组对比代码评估不能只看总体指标还必须按天气类型切分。同样的模型晴天误差4%雨天误差18%平均下来看着还行但雨天才是真正伤害电网运行的场景。我把分组评估做成一个通用逻辑每次实验都跑一份def evaluate_by_group(df, y_true, y_pred, capacity_kw): groups [] # 按辐照度波动把人分成晴、多云、阴雨三档 ghi_rate df[ghi_rate].abs() df[weather_label] pd.cut( ghi_rate, bins[0, 0.1, 0.5, np.inf], labels[晴, 多云, 阴雨] ) for label in [晴, 多云, 阴雨]: mask (df[weather_label] label) (y_true 0.05 * capacity_kw) mae np.abs(y_true[mask] - y_pred[mask]).mean() nmae mae / capacity_kw * 100 groups.append((label, len(mask), round(nmae, 2))) return groupspd.cut按辐照度变化率分箱变化率低于0.1代表天空状态稳定归为晴0.1到0.5之间代表有云层扰动归为多云超过0.5说明辐照度剧烈震荡归为阴雨。分组后你会发现多云的NMAE最高因为晴雨两态反而好预测就是冷暖空气交汇的过渡态最难搞这个发现会直接影响你对模型复杂度的取舍。6.3 永远先跑一个持久性基线再下结论持久性模型persistence是光伏预测里最容易被忽略的黄金基线它的定义很简单预测未来时刻的出力等于当前时刻的出力。对15分钟级的超短期预测辐照度变化不大时这个笨办法的误差居然很小经常能打赢没有调参的机器学习模型。我现在每个项目的第一步都是跑物理基线和持久性基线两个基线的误差区间直接决定了后面模型的增量价值。如果新模型只比持久性基线好1%而且增加了一堆运维依赖那从工程角度不如不上如果能在多云天气分组上比基线好10%以上才是真正值得部署的改进。这个判断习惯帮我砍掉了不少“论文好看但现场没用”的模型方案。回看自己做过的一堆预测项目最深刻的教训就是建模前先问三个问题数据对齐了吗、基线跑了吗、评估指标会被夜间零值污染吗。这三个问题问完项目基本就稳了一半。希望这篇能帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →