简介:项目基于机器学习的光伏功率预测,包含Python源码与配套训练、测试数据集,面向需要完成毕业设计、课程设计或期末大作业的计算机、电气等相关专业学生,也适合机器学习初学者做回归预测练手。围绕光伏功率预测这一场景,项目设计了数据处理、模型训练与预测、结果保存等模块,代码注释较完整,便于理解特征处理与机器学习建模流程。压缩包共19个文件,涵盖csv数据、py脚本、ipynb示例、md说明、docx任务书及备份文件,整体约4.64MB,数据与代码分离,结构清晰,下载后按说明即可快速部署体验。当前已有65人学习下载,适合需要快速搭建光伏预测项目并掌握完整实现思路的读者,可直接用于课程答辩、毕业设计支撑或进一步算法优化。
1. 光伏功率预测项目为什么值得自己动手做一遍
聊到“基于机器学习的Python光伏功率预测项目源码及数据集”,很多人的第一反应是去GitHub上找一个star高的仓库,跑通然后写进简历。但真正做过一两个并网电站或者分布式光伏项目的人会告诉你:光伏功率预测的难点从来不在模型,而在数据清洗和特征工程。同样的LSTM或者XGBoost,别人跑出95%的准确率,你跑出70%,差别基本都出在数据上。这套方案的核心价值,是让你完整走一遍“原始气象数据→清洗对齐→特征构造→模型训练→误差分析→滚动预测”的链路,而这条链路恰好是电力现货交易、电站运维和电网调度里最缺人的环节。
这篇实战笔记适合两类人:一类是刚入门机器学习、想找一个能落地的回归项目来练手的开发者——光伏功率预测比房价预测、鸢尾花分类更能让你理解时间序列数据的特点;另一类是已经在做新能源或者电力行业软件、需要快速搭建一套预测模块的工程师——这里给出的数据字段、模型选型和参数设置,可以直接映射到你自己的数据源上。我会把整个方案从数据集结构讲到超短期预测的工程技巧,每一步都给出可复现的命令、参数和避坑记录。数据永远是这类项目的上限,模型只是逼近这个上限的手段,这点先记住。
2. 数据集结构拆解:光伏功率预测的第一步不是建模,是搞懂天气和功率的对齐关系
2.1 数据集里有什么:从SCADA系统到气象站的多源数据合并
光伏功率预测项目的数据集通常包含两类数据源,一类是电站本地的SCADA系统采集的逆变器功率、总辐照度、组件温度等运行数据,另一类是气象站或数值天气预报(NWP)提供的辐照度、云量、温度、风速等气象数据。常见的采集频率有两种:分钟级(1分钟、5分钟、15分钟)用于超短期预测,小时级用于短期预测。这个项目的源码包里一般附带的是一个或多个电站的历史数据,时间跨度从几个月到一两年不等。
以典型的光伏电站SCADA数据为例,CSV文件的字段通常包括时间戳、总辐照度(W/m²)、环境温度(℃)、组件温度(℃)、风速(m/s)、风向(°)、直流功率(kW)、交流功率(kW)、逆变器效率等。其中有一个容易忽略的字段叫“功率限发状态”——当电网调度要求限功率运行时,实际输出功率会低于理论可发功率,这个状态不标注出来,模型会学出一堆错误规律。
注意:拿到数据集后第一件事不是看模型,而是逐字段看数据字典。没有数据字典的源码包,优先检查CSV表头和前100行数据,确认单位、时区、采样间隔,这三个信息错了后面步步错。
常见的数据集文件组织方式是:
dataset/ ├── plant1/ │ ├── weather.csv # 气象站数据,15分钟粒度 │ ├── power.csv # 逆变器SCADA数据,15分钟粒度 │ └── plant_info.json # 电站装机容量、经纬度、组件类型 ├── plant2/ └── readme.txt如果你拿到的数据集只有功率序列没有气象数据,那基本只能做统计外推类模型(ARIMA、Prophet),机器学习模型的价值会大打折扣。反过来,如果只有气象数据没有功率数据,那只能做理论辐照度到功率的物理换算式,谈不上机器学习。理想的数据集必须两者都有,并且时间戳要对得上。
2.2 时间对齐与重采样:两条土办法解决采样频率不一致
光伏数据最折磨人的问题就是时间对齐。气象站的数据可能是每15分钟一条,逆变器数据可能是每5分钟一条,有时候还带几秒到几分钟的延迟,直接合并会引入大量空值。我见过有同学直接把两边DataFrame一merge,结果功率序列和辐照度序列错位了15分钟,模型训练损失一路飘绿但预测永远偏移——这就是典型的时间对齐翻车。
正确的做法是先确定目标采样频率,然后把两边的数据都重采样到这个频率上。对于超短期预测,15分钟粒度最常见;对于短期预测,1小时粒度够用。代码示例:
import pandas as pd # 读取原始数据,时间列解析为标准时间 power_df = pd.read_csv('power.csv', parse_dates=['time'], index_col='time') weather_df = pd.read_csv('weather.csv', parse_dates=['time'], index_col='time') # 统一重采样到15分钟,取每15分钟窗口的均值 power_df = power_df.resample('15T').mean() weather_df = weather_df.resample('15T').mean() # 按时间戳对齐合并,删除没有对应气象记录的功率点 merged_df = pd.merge(power_df, weather_df, left_index=True, right_index=True, how='inner') # 检查对齐后各列的空值占比 null_ratio = merged_df.isnull().mean().sort_values(ascending=False) print(null_ratio.head(10))这段代码的逻辑分三步:先把两边的时间序列都重采样到同一种频率(15T表示15分钟),再用inner join把两边数据对齐,最后输出各列空值占比。关键点是重采样的聚合方式,默认用mean()取均值比较稳妥,但如果数据里有明显的尖峰(比如云层快速移动造成的辐照度骤降),均值会把这些突变抹平,对超短期预测不利——这个问题后面避坑章节还会单独讲。
另一个对齐的坑是时区。中国光伏电站数据通常记录的北京时间,但有些NWP数据源用的UTC,两者的差异在日升日落附近会被模型学进去,导致每天早晨和傍晚的预测偏差特别大。处理办法是统一转成带时区信息的时间戳:
# 如果数据是UTC时间,转成北京时间(UTC+8) merged_df.index = merged_df.index.tz_localize('utc').tz_convert('Asia/Shanghai')2.3 异常值与限功率时段剔除:数据清洗决定模型上限
光伏数据的异常值大体分三类:传感器故障导致的恒定值或跳变值(比如辐照度为负、功率超过装机容量)、通信中断导致的长时间零值、电网限功率导致的实际功率远小于理论功率。前两类是脏数据,直接剔除或按前后插值处理;第三类不是脏数据,但它代表的物理规律和正常时段不一样,混在一起训练会让模型产生系统性偏差。
经典的清洗方案是按物理规则做条件过滤:
import numpy as np # 异常点剔除:辐照度负值、功率负值或超装机容量 cleaned_df = merged_df[ (merged_df['irradiance'] >= 0) & (merged_df['irradiance'] < 1500) & (merged_df['power'] >= 0) & (merged_df['power'] <= installed_capacity * 1.1) ].copy() # 限功率时段剔除:功率偏低但辐照度高、同时风速正常的情况 high_irradiance_mask = cleaned_df['irradiance'] > 400 low_power_ratio = cleaned_df['power'] / (installed_capacity * cleaned_df['irradiance'] / 1000) limit_power_mask = high_irradiance_mask & (low_power_ratio < 0.3) # 剔除限功率时段后,记录剔除比例供参考 removed_ratio = limit_power_mask.mean() cleaned_df = cleaned_df[~limit_power_mask] print(f'剔除限功率时段占比: {removed_ratio:.2%}')这里有一个背景知识需要说明:光伏板的输出功率近似正比于组件面辐照度,所以“功率/装机容量”和“辐照度/1000W/m²”应该接近1:1的关系。如果辐照度很高(大于400W/m²)但功率占比不到理论值的30%,大概率是限功率或故障,这种样本对训练是毒药,直接剔除。当然,限功率时段如果单独标注出来,也可以保留一部分专门做“限功率识别”的二分类任务,那是另一个方向。
数据清洗做到这个程度,应该可以看到功率曲线的日变化轮廓清晰了。如果清洗完发现功率序列在午间仍然有大片锯齿状波动,那大概率是云层遮挡导致的实际辐照度快速变化,这类波动要靠后面特征工程里的“波动率特征”来捕捉,不是清洗能解决的。
3. 特征工程与基线模型:先跑通LightGBM,再谈深度学习
3.1 时间特征与天气特征构造:把Excel表格变成有物理意义的数据
光伏功率预测特征工程的核心思路是“让模型知道太阳在哪”。最简单有效的时间特征包括小时、月份、一年中的第几天,这些特征在本质上帮模型隐式地学习太阳高度角的日变化和季节变化。但更直接的做法是用astral库直接计算每个时间戳对应的太阳高度角和方位角:
from astral import LocationInfo from astral.sun import elevation import pandas as pd # 电站经纬度,来自电站信息文件 city = LocationInfo('Plant', 'China', 'Asia/Shanghai', 39.5, 118.2) # 逐行计算太阳高度角,作为新特征 def add_sun_features(df): df = df.copy() df['sun_elevation'] = [elevation(city.observer, ts.to_pydatetime()) for ts in df.index] # 高度角小于0的时段没有太阳,直接标记夜间 df['is_night'] = (df['sun_elevation'] <= 0).astype(int) return df # 把日期时间分解成周期性特征,避免午夜0点突变问题 cleaned_df['hour_sin'] = np.sin(2 * np.pi * cleaned_df.index.hour / 24) cleaned_df['hour_cos'] = np.cos(2 * np.pi * cleaned_df.index.hour / 24) cleaned_df['day_sin'] = np.sin(2 * np.pi * cleaned_df.index.dayofyear / 365) cleaned_df['day_cos'] = np.cos(2 * np.pi * cleaned_df.index.dayofyear / 365)这里为什么用sin/cos编码而不是直接用小时数值?因为小时是圆形变量——23点和0点只差1小时,但如果直接用数值表示,23和0之间的距离是23,模型会以为这两个时间点差异很大,导致午夜附近预测值出现跳变。用sin/cos编码后,23点和0点在特征空间里是紧挨着的,模型学起来顺滑得多。这是时间序列特征工程里最基础也最常被忽略的一个点。
另一个重要的衍生特征是高阶气象交互项,比如“辐照度是否下降但温度没同步下降”——这是云层遮挡的典型信号。直接构造这类特征工程比较费事,一个折中方案是把辐照度和温度都做一阶差分,让模型自己学习它们之间的关系:
# 差分特征:捕捉辐照度和功率的变化趋势 cleaned_df['irradiance_diff'] = cleaned_df['irradiance'].diff(1) cleaned_df['power_diff'] = cleaned_df['power'].diff(1) cleaned_df['temp_diff'] = cleaned_df['temp'].diff(1) # 滑动窗口特征:过去1小时辐照度均值和波动率 cleaned_df['irradiance_mean_1h'] = cleaned_df['irradiance'].rolling(4).mean() cleaned_df['irradiance_std_1h'] = cleaned_df['irradiance'].rolling(4).std() cleaned_df['power_mean_1h'] = cleaned_df['power'].rolling(4).mean()3.2 LightGBM:为什么表格数据上它比深度学习更可靠
在光伏功率预测这件事上,我的经验是先跑LightGBM或XGBoost建立基线,再考虑是否上LSTM、TCN或者Transformer。原因是:光伏预测的输入特征以表格型特征为主(辐照度、温度、湿度、风速、太阳角度),这类特征在梯度提升树模型上很容易达到不错的效果,训练速度快、可解释性强,而且对特征尺度和缺失值不敏感。数据量在十万级以下时,深度学习没有明显优势,反而容易在特征归一化和训练调参上浪费大量时间。
用LightGBM跑基线模型的代码框架如下:
import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit # 特征列清单,排除标签和不可用的原始列 feature_cols = [ 'irradiance', 'temp', 'humidity', 'wind_speed', 'sun_elevation', 'is_night', 'hour_sin', 'hour_cos', 'day_sin', 'day_cos', 'irradiance_diff', 'irradiance_mean_1h', 'irradiance_std_1h', 'power_mean_1h' ] X = cleaned_df[feature_cols].fillna(-999) y = cleaned_df['power'] # 时序交叉验证:按时间顺序切分训练和验证集,避免随机打乱导致数据泄露 tscv = TimeSeriesSplit(n_splits=5) lgb_params = { 'objective': 'regression', 'metric': 'mae', 'learning_rate': 0.05, 'num_leaves': 63, 'max_depth': -1, 'min_child_samples': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'verbosity': -1, 'n_jobs': -1, 'seed': 42 } for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): train_data = lgb.Dataset(X.iloc[train_idx], label=y.iloc[train_idx]) val_data = lgb.Dataset(X.iloc[val_idx], label=y.iloc[val_idx]) model = lgb.train( lgb_params, train_data, num_boost_round=1000, valid_sets=[val_data], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)] ) print(f'Fold {fold + 1}, best_iteration: {model.best_iteration}')这里的关键设计是TimeSeriesSplit而不是普通的KFold。光伏功率是强时间相关序列,相邻时间点的数据高度相似,如果随机打乱划分训练集和验证集,模型等于开卷考试——它在训练时已经见过验证集的“邻居”样本,验证指标会虚高,等你部署到真实环境做未来预测时就原形毕露。时序切分的核心原则是:训练集时间永远在验证集之前。
LightGBM的超参数里,num_leaves是偏差-方差平衡的控制旋钮。光伏数据非线性强、样本量不算大,63是比较中庸的选择;如果验证集上过拟合严重(训练误差远小于验证误差),把num_leaves降到31,同时把min_child_samples提高到50,会立竿见影。feature_fraction=0.8表示每棵树随机抽取80%的特征来分裂,这是防过拟合的有效手段,也是在特征数不多时仍然值得开的选项。
3.3 评价指标踩坑:MAE、RMSE和CRPS到底看哪个
训练完模型后怎么评价?光伏功率预测领域最常用的指标有三个:MAE(平均绝对误差)、RMSE(均方根误差)和R²决定系数。它们的区别在于惩罚力度:MAE对所有误差一视同仁,RMSE对大的误差惩罚更重——如果你特别不希望出现“明明预测100kW实际发了0kW”这种大错,RMSE更能反映这个问题。但在实际并网项目中,电网调度最关心的不是平均误差,而是特定时段的极端误差,所以还要看分位数误差,比如P90误差。
一个不太被人说起但很有用的指标是CRPS(连续排序概率评分),它的特点是同时评价预测值和不确定性估计的质量,适合在做区间预测时使用。光伏功率预测的落地方案里,光给一个点预测值(比如“明天12点功率300kW”)是不够的,电网更想要的是“明天12点功率大概率在280kW到320kW之间”——这时候就需要用分位数回归或者直接输出预测分布。
在前期跑基线阶段,你只需要盯住两个数字:全天的归一化MAE(除以装机容量)和中午时段(10点到14点)的RMSE。前者看整体水平,后者看最需要精度的时段表现。如果中午RMSE比全天MAE大三倍以上,说明模型在辐照度高的时段波动捕捉不够,优先加滑动窗口波动特征,而不是换模型。
4. 从理论到可运行代码:用PyTorch落地一个LSTM光伏功率预测模型
4.1 数据窗口化:为什么LSTM不能直接吃表格数据
LightGBM跑通了基线之后,如果你手头的数据量足够(超过6个月的15分钟粒度数据),可以考虑上LSTM。LSTM的优势在于能直接学习时间序列样本之间的顺序依赖——比如“前15分钟辐照度在下降,说明云层正在靠近,未来15分钟功率可能会继续下降”这种时序模式,LightGBM要显式构造差分特征才能近似学到,LSTM可以自己捕捉。
但LSTM的输入格式和表格模型完全不同:它要求输入是三维张量,形状为(样本数, 时间步长, 特征数)。所以需要先把清洗后的表格数据转换成滑动窗口序列。这里的“时间步长”就是回看窗口长度,比如用过去6个时刻(90分钟)预测未来1个时刻(15分钟)的功率。窗口化的代码:
import numpy as np import torch from torch.utils.data import Dataset, DataLoader def create_sequences(data, feature_cols, seq_len=6, pred_len=1): """把表格数据转换为LSTM输入的三维序列。""" X, y = [], [] values = data[feature_cols].values target = data['power'].values for i in range(len(data) - seq_len - pred_len + 1): X.append(values[i:i + seq_len]) y.append(target[i + seq_len:i + seq_len + pred_len]) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32) feature_cols = ['irradiance', 'temp', 'humidity', 'wind_speed', 'sun_elevation'] X_seq, y_seq = create_sequences(cleaned_df, feature_cols, seq_len=6, pred_len=1) # 按时间顺序切分,前80%训练后20%验证 split_idx = int(len(X_seq) * 0.8) X_train, X_val = X_seq[:split_idx], X_seq[split_idx:] y_train, y_val = y_seq[:split_idx], y_seq[split_idx:] # 转换为PyTorch张量并创建数据加载器 train_dataset = torch.utils.data.TensorDataset( torch.tensor(X_train), torch.tensor(y_train) ) train_loader = DataLoader(train_dataset, batch_size=256, shuffle=False)注意这里shuffle=False——和LightGBM部分同理,时间序列的batch打乱没有意义,甚至会让模型学到错误的跨时间依赖。后面的验证集样本在时间上晚于训练集,这才是未来预测的真实场景。
4.2 模型定义与训练循环:LSTM层数、隐藏维度和优化器选择
一个在光伏预测上稳定好用的LSTM配置是:两层LSTM,隐藏维度64,加一层Dropout(比率0.3),最后接一个全连接层输出预测功率。两层LSTM比一层能捕捉更高层的时序模式,但超过三层收益递减且训练容易发散。隐藏维度64在中等规模数据上够用,如果数据量大(几十万样本)可以加到128。
import torch.nn as nn class PVLSTM(nn.Module): def __init__(self, n_features, hidden_size=64, num_layers=2, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x形状: (batch, seq_len, features) out, _ = self.lstm(x) # 取最后一个时间步的输出接全连接层 out = self.fc(out[:, -1, :]) return out.squeeze(-1)训练时的关键参数:优化器用Adam,初始学习率1e-3,损失函数用HuberLoss(平滑平均绝对误差)。HuberLoss在误差小的时候表现像MSE,误差大的时候表现像MAE,对光伏功率这种偶尔出现极端波动(云层突变)的序列比纯MSE稳得多。学习率需要配合余弦退火调度器,在末尾阶段把学习率降到1e-5,避免在最优解附近震荡。
import torch.optim as optim model = PVLSTM(n_features=len(feature_cols)) criterion = nn.HuberLoss(delta=1.0) optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): model.train() train_loss = 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_x) loss = criterion(pred, batch_y) loss.backward() # 梯度裁剪,防止LSTM训练中梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() train_loss += loss.item() * batch_x.size(0) scheduler.step() avg_loss = train_loss / len(train_dataset) if (epoch + 1) % 10 == 0: print(f'Epoch {epoch + 1:02d}, Loss: {avg_loss:.4f}')clip_grad_norm_这一行的作用经常被忽略,但它在LSTM训练里非常重要。LSTM的反向传播在时间步上链式求导,梯度很容易爆炸到NaN,加一个max_norm=5.0的裁剪让梯度的整体范数不超过5,训练稳定性会明显提升——尤其是当你的数据里有极端值(比如辐照度瞬间从1000跳到50)时,这一行能救你很多次。
提示:如果训练过程中loss不降反升,先检查是不是学习率太大导致震荡;如果loss一开始就卡在某个值不动,优先怀疑输入特征里有没有大量NaN或异常值,而不是怀疑模型结构。LSTM对输入尺度的敏感度比树模型高很多,特征归一化必须做扎实。
4.3 预测结果还原与误差可视化:归一化的后悔药
训练时为了加速收敛,通常要把特征和标签做归一化。但预测出来的功率值必须还原回真实量纲,否则没法跟并网数据做对比。这里的规矩是:训练前保存归一化参数,预测后立即还原,不要关掉程序再想起来,否则又得重跑一遍。
from sklearn.preprocessing import StandardScaler # 训练前:保存功率列的scaler power_scaler = StandardScaler() power_scaler.fit(y_train.reshape(-1, 1)) # 训练中:用缩放的y训练 y_train_scaled = power_scaler.transform(y_train.reshape(-1, 1)).flatten() # 预测后:还原到真实功率单位(kW) pred_scaled = model(torch.tensor(X_val)).detach().numpy() pred_kw = power_scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true_kw = y_val.flatten() # 打印验证集MAE和中午时段的RMSE from sklearn.metrics import mean_absolute_error, mean_squared_error print(f'Val MAE: {mean_absolute_error(true_kw, pred_kw):.2f} kW') print(f'Val RMSE: {np.sqrt(mean_squared_error(true_kw, pred_kw)):.2f} kW')可视化验证一个典型的阴天转晴天的日子,把真实功率和预测功率画在同一张图上,观察模型在上升段和下降段是否滞后。LSTM在这种场景下常见的毛病是“预测比实际慢半拍”——输入窗口里的上升趋势还没走完,模型倾向于延续当前的上升斜率,于是预测偏高。如果发现这个现象,可以尝试把输入窗口从6个时刻加到12个时刻(相当于回看3小时),让模型看到更长历史趋势。
5. 避坑指南:光伏功率预测项目里值得写进简历的5条实战踩坑记录
5.1 辐照度计被遮挡导致午间功率低估,特征却显示高辐照
现象:模型在某个电站上的预测值普遍偏低,尤其在中午时段,真实功率曲线有明显“削顶”现象,但数据集里的辐照度数值却很高。
原因:现场辐照度计的安装位置不合理,被组件边缘或测风杆遮挡,午后一段时间处于阴影中,导致辐照度数据虚高。模型学到的是“高辐照→高功率”,实际功率达不到,误差就被系统性放大。
解决:检查辐照度与功率的散点图,如果看到“辐照度大于800W/m²但功率对应的效率明显偏低”的一簇点群,优先怀疑辐照度计问题而非模型问题。解决方案是给这组数据打标记,训练时降低这些样本的权重,或者干脆用同一电站晴朗天的历史辐照度数据做插值修复。这个案例在数据清洗环节处理,不要带到特征工程之后。
5.2 早晚时段太阳高度角低,模型把负功率学成了零功率
现象:清洗后的数据里夜间功率全部是0,模型预测的夜间功率也很接近0,看起来没什么问题。但看清晨和傍晚的预测结果,功率预测值经常是0,而实际已经发了5%-10%的额定功率。
原因:太阳高度角在±5°附近时,辐照度传感器的响应不准确(余弦响应误差),记录到的辐照度可能很低甚至为0,但实际散射辐照度还能让组件发一小部分电。模型看到“辐照度=0就输出功率=0”这个规律后,在低辐照度区间就没脾气了。
解决:用astral计算的太阳高度角做一个复合判定:当太阳高度角大于-3°但小于5°时,把实际辐照度和理论晴空辐照度的比值作为独立特征输入,而不是只用原始辐照度。这个“比值特征”在低角度时段比原始辐照度可靠得多。顺带说一句,这也是为什么我习惯在特征工程里保留sun_elevation这个原始数值,而不是只做一个is_night二分类标签。
5.3 数据泄露:补全缺失值用了未来数据,模型验证指标虚高到了0.98
现象:某同学用pandas的fillna(method='bfill')对缺失功率做反向填充,然后做时序切分训练,验证集R²高达0.98,模型却在新数据上翻车严重。
原因:bfill用后面的值补前面的缺失,在构造训练样本时,某个样本的特征里可能混进了未来时刻的真实功率信息。验证集在时间上晚于训练集,但训练集的某些特征已经“偷看”了验证集时间范围内的数据,指标自然虚高。
解决:所有填充操作必须限定在时间窗口内部。正确做法是用前向填充ffill(),或者用历史同时间的均值填充(比如用前一天同一时刻的数值)。更稳妥的方式是把填充和窗口切分的顺序反过来——先切分好训练集和验证集,再分别对每个集合内部做缺失值处理,确保训练集的填充绝不引用验证集的数据。这个顺序问题,建议直接写进团队的代码规范里。
5.4 多电站合并训练时,忘记加电站ID导致模型输出“平均功率”
现象:把两个装机容量不同的电站数据合并训练(一个50MW,一个10MW),模型预测的功率曲线介于两者之间,两个电站单独测试误差都不小。
原因:模型不知道“当前样本属于哪个电站”,它对所有样本学了一个全局映射。装机容量不同导致功率量纲差异巨大,均值回归让模型倾向于输出一个“安全”的中间值。
解决:最小改动方案是把电站ID做标签编码或one-hot编码作为特征输入。更好的方案是在特征里加入“装机容量”这个数值特征,相当于给模型一个归一化尺度信息。实际工程项目里如果电站数量多(几十个),建议每个电站单独建模型,或者训练一个全局模型但输出归一化功率再乘回各自装机容量。这个坑在很多公开数据集上都存在,README里不一定会写清楚。
5.5 预测值出现负功率或超过装机容量,后处理没有做物理约束
现象:LSTM预测结果里有少量负功率值,也有个别值超过了装机容量,直接画图时功率曲线看着非常不合理。
原因:模型输出层是线性激活,没有加任何物理约束。回归模型在极端输入组合下(比如夜间冷空气来临时的异常特征组合)可能输出超出合理范围的数值。
解决:在推理阶段加一道物理后处理钳位,这一步虽然技术上很简单,但在工程交付上却是必须的,否则电网调度那边直接打回你的预测数据:
def post_process_pred(pred_kw, installed_capacity): """对原始预测值做物理约束处理。""" # 下限钳位到0,上限钳位到装机容量的1.05倍 pred_clipped = np.clip(pred_kw, 0, installed_capacity * 1.05) # 夜间时段(太阳高度角小于0)功率必须归零 pred_clipped[cleaned_df['sun_elevation'] <= 0] = 0 return pred_clipped处理后的曲线在夜间是平稳的0线,白天没有超过装机容量的尖峰,曲线形态上才有可能让业务方接受。后处理逻辑不复杂,但缺了它,前面模型训练的所有功夫都会在展示环节减分。
6. 从15分钟预测到超短期滚动预测:一个值得掌握的工程技巧
做完单步预测之后,进阶方向是超短期滚动预测——也就是输入过去一段时间的数据,预测未来4小时甚至更长时间窗口的功率曲线。这个能力在电力现货交易里直接对应“日内预测”需求,也是很多岗位JD里写到的“超短期光伏功率预测”的实际指向。
常见的做法是“递归多步预测”:用训练好的单步模型,先预测t+1时刻的功率,然后把这个预测值作为输入特征的一部分,去预测t+2时刻,如此迭代下去。但递归预测有个很坑的累积误差问题——早期预测的误差会像雪球一样越滚越大,到第8步之后几乎失去参考价值。解决办法通常有两种:一是直接训练一个多步输出模型,让输出层同时输出未来16个时刻的功率值;二是在递归预测时引入“计划值”特征——比如NWP气象数据里本身就包含未来几小时的辐照度预报,这些预报值不依赖模型自身输出,可以打断误差的累积链条。
我用得比较多的是第二种方案,具体做法是:把未来时刻的天气预报辐照度作为已知特征,预测t+1时用t+1的辐照度预报值,预测t+2时用t+2的辐照度预报值,这样功率预测的准确度会显著高于纯递归方式。依赖NWP数据的效应时,需要给NWP预报辐照度本身加一个特征“预报时效”——比如提前1小时的预报和提前4小时的预报,可信度完全不是一个级别,模型需要学会自动降低远期预报的权重。
回到工程习惯层面:做这类预测项目,特征列清单和归一化参数一定要用json或yaml格式持久化保存,模型重新训练或换数据的时候,参数可以一键恢复。没有这个习惯,每次换数据都要重新调半天参数,时间就这么浪费掉了。另一个我个人的习惯是每个训练好的模型都保存一份在验证集上的误差报告(分时段MAE和RMSE),方便后来人对照着评估模型退化情况——如果你部署的预测模型上线后每个月误差浮动超过15%,第一件事不是重新训练,而是检查数据源和天气特征的输入质量,误差漂移很可能来自上游数据的变化。
这条路走到这里,基本能把“基于机器学习的Python光伏功率预测项目源码及数据集”背后的完整链条摸个通透:数据清洗决定上限,特征工程决定模型能不能学到物理规律,基线与深度学习各有适用场景,落地工程化则全靠后处理和滚动预测这些细节。自己做一遍这套方案,比搜一百个源码仓库都管用。希望帮到你。
本文还有配套的精品资源,点击获取