
简介基于Python与机器学习的光伏发电功率预测系统面向高校毕业设计、课程设计、结业作业及新能源方向研究者解决光伏发电量短期预测的完整建模问题。资源包含完整可运行程序源码、多组训练与测试数据集、独立测试样本以及项目说明文档代码在关键步骤附有详细注释便于初学者理解与按需修改。压缩包共20个文件涵盖CSV数据文件、Python脚本、Jupyter Notebook交互式分析文件、Word说明文档、备份文件等类型整体大小约6.32MB目录划分清晰通过简单配置即可本地部署。目前已有80人学习下载。项目在学术评审中获得高度评价以数据读取、数据清洗与特征构造、模型训练、预测评估为主线形成一套可直接复用和扩展的光伏功率预测基线方案。对于毕业设计或专题研究这套资源提供了完整的数据与代码框架能够显著降低预测建模的上手成本。1. 光伏发电功率预测比普通回归难在哪做毕业设计选这个题的人不少但很多人把光伏功率预测当成一个普通的回归问题来跑结果换了好几个模型误差始终下不来。实际上这套系统的难点不在模型而在数据的时间对齐、天气突变处理和评估方式上——直接用默认的train_test_split切数据等于把未来信息泄漏进了训练集指标虚高但一到现场就翻车。这篇文章从数据集清洗、特征工程、LightGBM和LSTM的选型对比到一个能拉出来用的预测系统代码完整过一遍我在这个项目里能复现的部分。适合正在做毕业设计、想用Python和机器学习完成光伏功率预测系统实现的人也适合想把手头气象数据转成可训练数据集的一线工程师。2. 数据集构建与预处理先让数据干净模型才有意义2.1 光伏预测需要什么样的数据光伏发电功率预测的输入不是单一的功率历史序列而是气象特征和功率特征的组合。常见做法是收集三个层面的数据光伏电站的实测发电功率频率通常是15分钟或5分钟一条、电站所在地的气象站实测数据辐照度、温度、湿度、风速、风向、以及数值天气预报NWP数据主要是未来几小时的辐照度预报。如果做的是短期预测未来1到4小时NWP的气象预报字段几乎是必需品如果做超短期预测未来15分钟到1小时可以只依赖历史功率和实测气象的时序外推。我在这个项目里用的是公开的光伏电站运行数据集包含连续一年的数据时间分辨率为15分钟每天96个点。核心字段有直流侧功率、交流侧功率、总辐照度GHI、组件温度、环境温度、湿度、风速。需要说明的是不同数据集的字段名差异很大到手后第一件事是统一单位——辐照度有的是W/m²有的是kW/m²温度有的是摄氏有的是华氏这些不统一后面全部白算。数据集到手后不要急着跑模型先做一次时间完整性检查。光伏数据最常见的坑是时间戳错位有的数据集用本地时间有的用UTC有的在跨月时漏了几个点。我一般会先按时间排序检查是否有重复时间戳和缺失时间点import pandas as pd import numpy as np df pd.read_csv(pv_data.csv, parse_dates[time]) df df.sort_values(time).reset_index(dropTrue) # 检查重复时间戳 dup df[df[time].duplicated()] print(f重复时间戳数量: {len(dup)}) # 检查缺失时间点按15分钟频率生成完整时间序列 full_time pd.date_range(startdf[time].min(), enddf[time].max(), freq15min) missing full_time.difference(df[time]) print(f缺失时间点数量: {len(missing)})这段代码的逻辑是先排序去重再用pd.date_range生成理论上的完整时间序列通过difference找出缺失点。参数freq15min要和数据集的采样频率严格一致如果数据集是5分钟一条这里就必须改成5min否则会把所有点都判成缺失。缺失点数量不多时用前后值线性插值补上即可如果一段连续缺失超过两小时比如逆变器停机千万不要插值硬补——这一段数据直接删掉因为这种长段缺失对应的功率状态和正常发电完全不同插值补出来的数据会把模型带偏。2.2 清洗策略夜间零值、突变点与负功率光伏数据清洗有三个绕不开的问题夜间功率为零但零值占比极高、传感器偶发负值、云层遮挡造成的功率骤降被误判为异常值。夜间零值处理上很多人直接把辐照度低于某阈值的行删掉这其实可以更精细一点。我一般用两个条件同时判断太阳高度角低于-5度且功率绝对值小于装机容量的1%。只按辐照度筛会误删傍晚和清晨的弱光数据而这些时段的功率曲线是训练模型学习低辐照度应对能力的关键样本。负功率的出现通常是传感器零漂或夜间逆变器自耗电直接归零就好不用删行。真正需要谨慎处理的是突变点光伏功率正常变化是连续的但云遮挡会让功率在几分钟内掉到原来的20%再恢复这是真实物理现象不要当异常值剔除否则模型学不到多云工况。和这个对应的是传感器故障造成的尖峰——单点功率突然跳到额定功率的120%以上这种才需要处理# 传感器故障尖峰识别超过额定功率120%的点且邻域无明显对应气象变化 rated_power 1000 # kW按实际数据集修改 df[power] df[power].clip(upperrated_power * 1.2) # 转速性突变检测用滚动中位数识别孤立尖峰 df[power_med] df[power].rolling(window5, centerTrue).median() df[is_spike] (df[power] - df[power_med]).abs() rated_power * 0.15 df.loc[df[is_spike], power] df.loc[df[is_spike], power_med]这里rolling(window5, centerTrue)取前后两个点和当前点共5个值的中位数对孤立尖峰有很强的鲁棒性。阈值rated_power * 0.15的意思是单点偏离邻域中位数超过额定功率的15%就判定为尖峰。这个阈值不是固定的组件清洗、故障停机导致的功率骤变也可能超过15%所以处理完一定要人工抽查几个时间段的曲线确认没有把真实的功率波动洗掉。2.3 训练集切分时间序列不能随机打乱这是整个项目里影响最大、也最容易翻车的一步。光伏功率预测是时间序列任务如果用train_test_split默认的随机切分训练集和测试集会混入相邻时段的样本短期气象的强相关性会让测试集评估结果虚高到不可信。正确的切法是按时间顺序切分并且测试集必须是时间上靠后的那一段模拟真实场景下用过去预测未来的状态# 按时间顺序切分前80%训练后20%测试 split_idx int(len(df) * 0.8) df_train df.iloc[:split_idx] df_test df.iloc[split_idx:] # 额外做一组连续验证最后连续15天作为验证集 val_days 15 val_start len(df) - val_days * 96 # 一天96个15分钟点 df_val df.iloc[val_start:]参数0.8不是固定的数据量大可以到0.85数据量小可以0.7但要保证测试集覆盖至少一个完整的多云-晴天周期。另外注意连续验证集的选取如果数据集有明确的四季差异训练集最好包含至少一个完整年度周期不然用夏天训练、冬天测试误差会大得离谱。3. 特征工程与模型选型从气象原始值到可训练特征3.1 时间特征与滞后特征的设计逻辑光伏发电功率预测的特征分三类时间特征捕捉日周期和季节周期、气象特征当前与未来的辐照度、温度、滞后特征过去几个时刻的功率值捕捉云层变化的连续过程。时间特征不能直接把小时当数值丢进去13点和14点的差值是1但23点和0点的差值也是1物理含义完全不同。我一般把小时和月份都做周期编码用sin和cos两个维度表示df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) df[month_sin] np.sin(2 * np.pi * df[month] / 12) df[month_cos] np.cos(2 * np.pi * df[month] / 12)周期编码的原理是把线性时间映射到单位圆上这样凌晨0点和深夜23点在角度上相邻模型才能学到这两个时间的发电状态接近这一事实。只用整数小时特征树模型也能凑合学但对线性模型或LSTM来说会是灾难。滞后特征是最有效的预测信号。光伏功率的自相关性极强——晴天时相邻15分钟功率差值很小多云天则能捕捉到云层移动的趋势。我习惯构造三组滞后特征滞后1个点15分钟前、滞后2个点30分钟前、以及过去1小时的平均功率。滞后步长不宜太多太多会引入噪声树模型的特征重要性会分散在无用的旧时刻上。3.2 LightGBM与LSTM两个方向的取舍很多做毕业设计的同学一上来就选LSTM觉得时序问题就该用循环神经网络。但实际跑下来在数据量不大一万到几万条且以气象特征为主的任务里梯度提升树往往更稳。LightGBM对特征尺度不敏感不需要归一化训练速度极快而且能直接输出特征重要性方便反推哪些气象变量真正起了作用。LSTM的优势在于能自动学习长程依赖对纯序列外推更擅长但训练时间长、超参数多、容易过拟合而且对输入数据的归一化要求极高。我两个模型都跑了最终的参数配置如下import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit feature_cols [hour_sin, hour_cos, month_sin, month_cos, ghi, temp, humidity, wind_speed, power_lag1, power_lag2, power_avg_1h] X df[feature_cols].values y df[power].values # 时间序列交叉验证 tscv TimeSeriesSplit(n_splits5, test_sizeint(len(df) * 0.08)) model lgb.LGBMRegressor( n_estimators500, learning_rate0.05, num_leaves31, max_depth6, subsample0.8, colsample_bytree0.8, random_state42 ) # 用时间序列交叉验证评估而不是单次切分 for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] model.fit(X_train, y_train, eval_set[(X_val, y_val)], eval_metricrmse)参数里面splits5和test_sizeint(len(df) * 0.08)是配合使用的TimeSeriesSplit要求每次验证的测试集大小固定这里取总样本的8%大约对应23天。和随机K折不同TimeSeriesSplit每一折的训练集永远是验证集之前的数据不存在数据泄漏。subsample0.8让每棵树只用80%的样本colsample_bytree0.8让每棵树只用80%的特征都能略微缓解过拟合。LSTM部分我只提示关键结构输入要构造成三维张量(样本数, 回看步长, 特征数)回看步长我用的是8过去2小时归一化必须用训练集的统计量测试集和预测时复用同一套均值和方差这在下文避坑章节会详细展开。对于毕业设计来说用LightGBM拿到一个可靠的基准模型再对比LSTM在特定时段比如多云突变时段是否更优是个既出工作量又不至于失控的策略。3.3 预测未来为什么训练用特征平移测试要靠NWP训练时的气象特征是实测值但预测未来的功率时实测值还不存在只能用数值天气预报NWP的辐照度预报值替代。这是整个系统里最容易被忽略的环节。很多代码在测试集上评估时仍然用实测气象数据得到的结果是理想气象条件下的预测误差而真实部署时NWP辐照度本身就有误差功率预测误差会成倍放大。我的做法是分两套评估实验室评估用实测气象记录模型上限系统级评估把测试集里的辐照度加一个随机扰动幅度按NWP的历史误差分布取反映真实部署表现。扰动幅度一般用±15%到±20%的均匀噪声模拟多云天NWP对辐照度的低估或高估。如果要做严谨的验证应该收集同一时刻的NWP预报快照和实测值构建一个误差分布再做扰动。4. 预测系统实现从模型训练到可调用接口4.1 系统整体架构与文件组织这套光伏功率预测系统按典型的三段式组织数据层原始数据与预处理脚本、模型层训练和评估脚本、模型权重输出、服务层加载模型、接收气象输入、输出预测结果。做毕业设计时这分层够用直接上Docker或消息队列反而显得臃肿且答辩时不好讲清楚。目录结构我习惯这样排pv_forecast/ ├── data/ │ ├── raw/ # 原始csv不动 │ ├── processed/ # 清洗后特征数据 │ └── nwp/ # NWP预报数据 ├── src/ │ ├── preprocess.py # 数据清洗与特征工程 │ ├── train_lgb.py # LightGBM训练脚本 │ ├── train_lstm.py # LSTM训练脚本可选 │ └── predict.py # 预测服务脚本 ├── models/ │ ├── lgb_model.txt # LightGBM模型权重 │ └── scaler.joblib # 归一化参数LSTM用 └── outputs/ ├── predictions.csv └── metrics.jsonpreprocess.py和train_lgb.py可以连起来跑先做清洗特征再直接训练出模型文件。预测服务单独拆一个脚本这样训练和推理解耦模型更新时不用改服务代码。4.2 训练脚本带完整的评估逻辑训练脚本要输出两部分内容模型文件和评估指标文件。评估指标不能只给RMSE光伏功率预测的公开评估一般看四个RMSE、MAE、R²、以及归一化均方根误差NRMSERMSE除以装机容量。NRMSE在不同装机容量的电站之间可比较毕业设计里把这个指标写进论文比单报RMSE更有说服力 train_lgb.py 训练LightGBM模型并输出评估指标保存模型文件到models/ import json import joblib import numpy as np import pandas as pd import lightgbm as lgb from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score # 加载处理好的特征数据由preprocess.py生成 data pd.read_csv(data/processed/pv_features.csv) feature_cols [hour_sin, hour_cos, month_sin, month_cos, ghi, temp, humidity, wind_speed, power_lag1, power_lag2, power_avg_1h] target_col power # 时间顺序切分 split_idx int(len(data) * 0.8) train_df data.iloc[:split_idx] test_df data.iloc[split_idx:] X_train train_df[feature_cols].values y_train train_df[target_col].values X_test test_df[feature_cols].values y_test test_df[target_col].values # 训练模型提前停止防止过拟合 model lgb.LGBMRegressor( n_estimators1000, learning_rate0.03, num_leaves31, max_depth8, subsample0.8, colsample_bytree0.8, random_state42, n_jobs-1 ) model.fit(X_train, y_train, eval_set[(X_test, y_test)], eval_metricrmse, callbacks[lgb.early_stopping(stopping_rounds100)]) # 评估 y_pred model.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, y_pred)) mae mean_absolute_error(y_test, y_pred) r2 r2_score(y_test, y_pred) rated_power 1000 # kW记得匹配数据集 nrmse rmse / rated_power * 100 metrics { rmse_kw: rmse, mae_kw: mae, r2: r2, nrmse_pct: nrmse, test_size: len(y_test) } print(json.dumps(metrics, indent2)) # 保存模型和特征列表 model.booster_.save_model(models/lgb_model.txt) joblib.dump(feature_cols, models/feature_cols.joblib)这段代码里early_stopping(stopping_rounds100)的意思是连续100轮验证集合loss不再下降就停止训练。n_estimators1000是个上限实际训练中早停会在几百轮内触发最后保存的模型是验证集最优的那一棵而不是最后一轮这点LightGBM会自动处理好。还有个细节model.booster_.save_model保存的是原生版本格式对应的加载方式是lgb.Booster(model_filemodels/lgb_model.txt)和joblib.dump(model)存整个Python对象不一样。用原生格式的好处是跨环境可移植从训练机复制到部署机不需要版本完全一致兼容性好。4.3 预测服务接收实时气象输出未来功率预测服务在系统里承担的是接一个实时请求返回未来4小时功率序列的职责。输入是气象预报数组未来每15分钟的GHI、温度、湿度、风速输出是功率数组。落地时有个容易错的地方滞后特征power_lag1和power_lag2需要历史功率值如果服务启动时没有历史缓冲区第一次预测就做不了。我一般会先在服务里加载最近24小时的实测功率缓存用滑动窗口的方式维护 predict.py 加载训练好的模型输入气象预报和最近历史功率输出未来预测功率 import json import numpy as np import joblib import lightgbm as lgb class PVForecastService: def __init__(self, model_path, feature_cols_path, rated_power1000): self.booster lgb.Booster(model_filemodel_path) self.feature_cols joblib.load(feature_cols_path) self.rated_power rated_power self.history_power [] # 维护最近2小时功率用于滞后特征 def update_history(self, power_series): 每次获得新实测值后更新缓存最多保留最近8个点 self.history_power list(power_series[-8:]) def predict_next_4h(self, nwp_features, start_hour, start_month): nwp_features: 未来16个时刻的 [ghi, temp, humidity, wind_speed] 返回16个预测功率值对应未来4小时15分钟步长 # 预填充滞后特征最初用最后已知功率 lag1, lag2 self.history_power[-1] if self.history_power else 0, \ self.history_power[-2] if len(self.history_power) 2 else 0 predictions [] hour_sin np.sin(2 * np.pi * start_hour / 24) hour_cos np.cos(2 * np.pi * start_hour / 24) month_sin np.sin(2 * np.pi * start_month / 12) month_cos np.cos(2 * np.pi * start_month / 12) for ghi, temp, hum, wind in nwp_features: features np.array([hour_sin, hour_cos, month_sin, month_cos, ghi, temp, hum, wind, lag1, lag2]) # 用滚动预测更新滞后特征 pred self.booster.predict(features.reshape(1, -1))[0] predictions.append(max(pred, 0)) lag2, lag1 lag1, pred return predictions # 使用示例 service PVForecastService(models/lgb_model.txt, models/feature_cols.joblib) # nwp_features 是未来16个时刻的预报气象数组 forecast service.predict_next_4h(nwp_features, start_hour10, start_month5) print(json.dumps(forecast, indent2))这段代码里最核心的是滚动预测逻辑预测下一个点后把这个点的预测值当作未来时刻的滞后特征输入也就是用自己预测出来的功率作为下一步的lag。这会引入误差累积——预测得越远滞后特征越不准。所以这个方式通常只在1到2小时内可信4小时之后的预测值参考意义减弱。如果要做更长时间尺度的预测应该改用NWP功率预报或引入光伏物理模型。5. 避坑指南光伏功率预测五个高频翻车点5.1 随机切分导致数据泄漏指标虚高现象同一个模型用train_test_split随机切分时RMSE只有随机切分方式的一半测试集曲线和实际曲线几乎重合看着效果特别好。原因光伏功率是强时间相关的相邻15分钟的两个样本特征非常相似。随机切分把相同时段前后的样本分别丢进训练集和测试集模型相当于见过测试集时段的数据特征。解决必须用TimeSeriesSplit或手动按时间顺序切分测试集在时间上严格靠后。养成习惯任何时序项目都先检查数据是否按时间排序再谈切分。5.2 归一化泄漏测试集用了全局统计量现象LSTM模型训练时验证损失正常测试集表现也不错但部署上线后预测值整体偏小尤其在辐照度较高的晴天。原因LSTM的输入特征做了MinMax归一化训练脚本里用全局X.min()和X.max()计算缩放参数测试集的缩放范围也用了同一份统计量。测试集本身参与了统计量计算相当于模型间接接触了测试集分布。解决先切分再归一化只在训练集上计算scaler的均值和标准差测试集和预测时复用训练集的scaler。用sklearn.preprocessing.MinMaxScaler或StandardScaler时都要先fit训练集再transform测试集。5.3 云遮挡导致的功率骤降被误删现象清洗后的数据里有一整天功率最大值只有正常晴天的30%怀疑是数据异常准备删掉。原因当天实际是多云天气大块云层遮挡导致功率持续走低这不是传感器故障是真实物理过程。删掉这一整天模型在测试时遇到多云天就会预测偏高。解决清洗时区分点异常和段异常传感器故障通常是单个点剧烈突变云遮挡是连续一段时间的波动。处理突变点时用邻域中位数而不是全局阈值保留多云天的数据样本。5.4 NWP辐照度辐照度误差传导现象测试集评估RMSE在30kW左右部署后实际RMSE到60kW以上。原因测试时用的是实测辐照度部署时用的是NWP预报辐照度两者本身存在误差。辐照度误差会直接映射到功率预测误差上尤其在多云时段。解决系统级评估时给辐照度加上按历史误差分布生成的扰动把NWP误差纳入预测体系考量。条件允许时记录NWP快照持续统计预报误差分布用于评估模型的真实可靠性。5.5 时间戳时区与夏令时错位现象预测曲线整体偏移一个小时清晨功率提前一小时出现。原因数据集里的时间戳是UTC但气象数据是本地时间或者历史数据跨夏令时切换4月和10月各有一小时数据异常。解决统一全部时间戳到同一时区推荐全部转成UTC存储显示时再转本地时间。跨夏令时的时间段要额外检查必要时丢弃那一小时的数据。6. 误差分段分析与残差检验验证模型到底学到了什么模型评估不能只看整体RMSE光伏功率的误差在不同辐照度区间差异巨大。我习惯把测试集按辐照度分成三档低辐照度小于100W/m²、中辐照度100到500W/m²、高辐照度大于500W/m²分别计算误差。低辐照度时段对应清晨傍晚功率绝对值小RMSE自然小高辐照度时段功率大RMSE大但相对误差可能反而更小。这类分段统计对论文里的分析和答辩都很有价值# 按辐照度分桶统计误差 test_df test_df.copy() test_df[pred] y_pred test_df[error] test_df[pred] - test_df[power] def bucket_stats(df, col, thresholds): for i in range(len(thresholds)): lo, hi thresholds[i] bins [0, 100, 500, float(inf)] # 辐照度分档 test_df[ghi_bucket] pd.cut(test_df[ghi], binsbins, labels[low, mid, high]) for name, group in test_df.groupby(ghi_bucket): rmse np.sqrt((group[error] ** 2).mean()) mae group[error].abs().mean() print(f{name}: 样本数{len(group)}, RMSE{rmse:.2f}kW, MAE{mae:.2f}kW)残差自相关检验也是被忽视的一环。光伏功率预测的残差如果存在明显的自相关说明模型没有完全捕获时序信息还有提升空间。用pandas.Series.autocorr计算残差在连续滞后时间上的自相关系数如果滞后1阶的相关系数超过0.3通常说明模型没利用足够的历史信息可以增强滞后特征或改用序列模型。验证做完之后我通常还会做一个极端场景测试挑一个晴天、一个阴雨天、一个多云骤变日分别画出实测和预测曲线并排对比。这种做法比整体指标更直观尤其能暴露出多云突变时段模型是否出现系统性滞后。从那以后我每次完成这套光伏功率预测系统都会强制走一遍分桶评估加极端场景曲线检查的流程少跑这一步光看一个RMSE很容易高估模型的实用性。希望帮到你这套资源整个流程跑下来你的毕业设计或工程原型的核心部分就已经立住了。本文还有配套的精品资源点击获取