简介这份资源面向高校学生与机器学习初学者提供一套基于Python的光伏发电功率预测系统完整实现可用于毕业设计、课程结业作业及专题研究等场景。包内共20个文件以csv训练与测试数据集、py程序脚本为主另含ipynb交互式笔记本、md说明文档、docx任务说明及若干备份文件压缩包约6.32MB结构清晰便于按模块查阅。程序段均附详细注释覆盖数据加载与保存、数据预处理、模型训练与预测等环节读者可据此理解光伏功率预测的完整流程并借助独立测试样本验证模型效果。资源已获80人学习适合希望快速搭建预测模型、对照代码与数据动手实践的学习者也可作为相关课题的参考方案。1. 光伏功率预测这件事为什么值得用 Python 从头搭一遍光伏发电功率预测系统说白了就是拿气象数据去猜一块光伏板接下来能发多少电。听起来像回归问题但真正做过的人都知道它的难点不在模型选多深而在数据怎么对齐、特征怎么构造、评估怎么不骗自己。我见过太多人拿一份现成数据集套个随机森林R² 跑到 0.95 就收工结果一上真实场景早上和傍晚的功率曲线直接崩掉。原因很简单太阳辐照度、温度、湿度这些变量和功率之间不是线性关系而且早晚低辐照区间的样本极少模型根本没学到。这套系统适合两类人一类是想把机器学习从鸢尾花、房价数据集里拉出来做点有物理意义的工程问题另一类是已经在做新能源相关系统需要一套能跑通、能改、能解释的基线。Python 在这里的角色不是炫技而是把数据清洗、特征工程、模型训练、误差分析串成一条可复现的流水线。下面我会按真实落地顺序从数据长什么样、怎么切、怎么建模一直讲到评估指标怎么选、坑在哪。2. 先把数据这件事说透光伏功率预测的数据集长什么样、怎么切2.1 一份典型光伏数据集包含哪些列哪些列是坑常见的光伏发电功率数据集通常来自某个光伏电站的实测记录采样粒度从 5 分钟到 1 小时不等。核心列一般包括时间戳、发电功率kW 或 MW、太阳总辐照度W/m²、环境温度、组件温度、湿度、风速、风向、气压。有些数据集还会带理论发电量、逆变器效率、直流侧电压电流。这里第一个坑是时间戳的时区与对齐。很多公开数据集的时间戳是本地时间但气象数据可能来自另一个源两者差半小时功率曲线和辐照度曲线就会错位。我一般会先把所有时间列统一转成 UTC再按同一时间粒度重采样。第二个坑是夜间数据。光伏夜间功率为零辐照度也为零如果直接丢进去训练模型会学到大量“零对零”的样本白天的高功率区间反而被稀释。常见做法是只保留辐照度大于某个阈值比如 10 W/m²的时段或者把夜间样本降采样。第三个坑是缺失值不是随机缺失。逆变器故障、传感器结冰、通信中断都会造成整段缺失这种缺失本身带有信息。直接均值填充会把故障时段伪装成正常发电模型评估会虚高。我的习惯是先标记缺失段超过连续 3 个采样点的缺失不填充直接剔除该段短缺失用线性插值并在特征里加一列“是否插值”的标志位。2.2 用 pandas 做时间对齐与重采样的最小代码下面这段代码假设你手头有一份 CSV列名是英文时间列叫timestamp功率列叫power辐照度列叫irradiance。实际列名不同改rename里的映射即可。import pandas as pd import numpy as np # 读取原始数据parse_dates 直接把时间列转成 datetime df pd.read_csv(pv_data.csv, parse_dates[timestamp]) # 统一时区到 UTC避免本地时间带来的错位 df[timestamp] df[timestamp].dt.tz_localize(Asia/Shanghai).dt.tz_convert(UTC) # 按 15 分钟重采样功率取均值辐照度取均值温度取均值 df df.set_index(timestamp).resample(15min).mean().reset_index() # 标记夜间辐照度低于 10 W/m² 视为无效发电时段 df[is_daytime] df[irradiance] 10 # 只保留白天数据用于训练夜间数据单独存一份用于验证零功率 day_df df[df[is_daytime]].copy() night_df df[~df[is_daytime]].copy() # 对白天数据的短缺失做线性插值最多插 2 个点 day_df day_df.interpolate(methodlinear, limit2, limit_directionboth) # 删除仍然缺失的行 day_df day_df.dropna() print(day_df[[timestamp, power, irradiance, temperature]].head())这段代码的逻辑说明tz_localize和tz_convert解决时区错位resample(15min).mean()把不规则采样统一到固定频率is_daytime用辐照度阈值切分昼夜避免夜间零值污染interpolate的limit2控制插值范围超过两个连续缺失就放弃防止把故障段填成正常。参数上辐照度阈值 10 W/m² 不是固定的低纬度地区可以调到 20高纬度冬天可以降到 5取决于你的数据分布。2.3 训练集、验证集、测试集为什么不能随机切时间序列预测最忌讳随机切分。随机切会让同一天的数据同时出现在训练集和测试集里模型相当于“见过答案”评估指标会好得离谱。正确做法是按时间顺序切前 70% 训练中间 15% 验证最后 15% 测试。如果数据跨年最好按年份切比如用前两年训练第三年测试这样能检验模型对季节变化的泛化能力。# 按时间顺序切分不 shuffle n len(day_df) train_end int(n * 0.7) val_end int(n * 0.85) train_df day_df.iloc[:train_end] val_df day_df.iloc[train_end:val_end] test_df day_df.iloc[val_end:] print(f训练集 {len(train_df)} 条验证集 {len(val_df)} 条测试集 {len(test_df)} 条)这里有个细节验证集和测试集之间的边界最好落在同一天的不同时段而不是硬切在中午。如果切在中午验证集全是上午测试集全是下午模型评估会受日内模式影响。我一般会按天切保证每个集合都包含完整的日内曲线。3. 特征工程把辐照度和温度变成模型能吃的信号3.1 光伏功率预测的 5 个必做特征原始列直接丢给模型也能跑但效果通常差一截。下面这 5 个特征是我在多个项目里验证过、性价比最高的第一辐照度的滞后项。光伏功率对辐照度的响应有惯性当前时刻的功率和过去 1 到 3 个采样点的辐照度都相关。加irradiance_lag1、irradiance_lag2能明显提升早上的预测精度。第二温度差。组件温度和环境温度的差值反映散热条件差值越大效率损失越明显。用module_temp - ambient_temp作为特征比单独给两个温度更有效。第三太阳高度角。这个需要根据经纬度和时间计算它决定了理论辐照度的上限。有了太阳高度角模型能区分“冬天中午”和“夏天傍晚”这种辐照度相同但功率不同的场景。第四滚动均值。对辐照度做 1 小时滚动均值能平滑云层遮挡带来的高频波动让模型学到趋势而不是噪声。第五时间编码。小时、分钟用 sin/cos 编码避免模型把 23 点和 0 点当成两个极端。import numpy as np # 滞后特征 day_df[irradiance_lag1] day_df[irradiance].shift(1) day_df[irradiance_lag2] day_df[irradiance].shift(2) # 温度差 day_df[temp_diff] day_df[module_temp] - day_df[temperature] # 滚动均值窗口 4 个点15 分钟粒度下是 1 小时 day_df[irradiance_roll_mean] day_df[irradiance].rolling(window4, min_periods1).mean() # 时间编码 day_df[hour] day_df[timestamp].dt.hour day_df[hour_sin] np.sin(2 * np.pi * day_df[hour] / 24) day_df[hour_cos] np.cos(2 * np.pi * day_df[hour] / 24) # 删除因为 shift 产生的首行缺失 day_df day_df.dropna()参数说明shift(1)和shift(2)的阶数取决于采样粒度15 分钟粒度下 2 阶对应半小时1 小时粒度下 2 阶对应 2 小时。滚动窗口window4也是按粒度调的粒度越细窗口越大。min_periods1保证序列开头不会因为窗口不满而丢数据。3.2 太阳高度角的计算方法与代码太阳高度角可以用 pvlib 这个库算但如果你不想引入额外依赖也可以用简化的天文公式。下面是一个不依赖第三方库的实现精度足够做特征。def solar_elevation(lat, lon, timestamp_utc): 简化太阳高度角计算输入纬度、经度、UTC 时间返回高度角度 # 一年中的第几天 day_of_year timestamp_utc.timetuple().tm_yday # 太阳赤纬近似公式 declination 23.45 * np.sin(np.radians(360 * (284 day_of_year) / 365)) # 时角 hour timestamp_utc.hour timestamp_utc.minute / 60 hour_angle 15 * (hour - 12) # 高度角 lat_rad np.radians(lat) dec_rad np.radians(declination) ha_rad np.radians(hour_angle) elevation np.degrees(np.arcsin( np.sin(lat_rad) * np.sin(dec_rad) np.cos(lat_rad) * np.cos(dec_rad) * np.cos(ha_rad) )) return max(elevation, 0) # 对每一行应用 day_df[solar_elevation] day_df[timestamp].apply( lambda t: solar_elevation(31.2, 121.5, t) )这个公式的误差在 1 到 2 度以内对特征工程够用。注意纬度 31.2、经度 121.5 是示例实际用你电站的坐标。高度角为负时截断到 0因为夜间数据已经被过滤掉了。3.3 特征筛选别把 20 个特征一股脑塞进去特征不是越多越好。辐照度、温度、滞后项之间相关性很高全塞进去会让线性模型系数不稳定树模型也会偏向高频特征。我一般先用皮尔逊相关系数筛掉相关性大于 0.95 的冗余特征再看特征重要性。下面这段代码用随机森林做一次快速筛选。from sklearn.ensemble import RandomForestRegressor import pandas as pd feature_cols [ irradiance, temperature, module_temp, humidity, wind_speed, irradiance_lag1, irradiance_lag2, temp_diff, irradiance_roll_mean, hour_sin, hour_cos, solar_elevation ] X day_df[feature_cols] y day_df[power] rf RandomForestRegressor(n_estimators100, random_state42) rf.fit(X, y) importance pd.Series(rf.feature_importances_, indexfeature_cols).sort_values(ascendingFalse) print(importance)跑完你会看到辐照度、太阳高度角、辐照度滚动均值通常排前三。如果某个特征重要性低于 0.01可以考虑删掉。但注意重要性低不代表没用可能是被相关特征替代了。我一般会保留 8 到 12 个特征兼顾效果和可解释性。4. 模型选型与训练从线性回归到梯度提升的取舍4.1 为什么光伏功率预测首选梯度提升树光伏功率和气象变量之间的关系是非线性的而且存在明显的交互效应高辐照度加高温时功率反而因为组件温度过高而下降。线性回归拟合不了这种交互神经网络需要大量数据调参梯度提升树比如 XGBoost、LightGBM在中小规模表格数据上通常是最优解。它自带特征交互、对缺失值鲁棒、训练速度快而且能输出特征重要性方便解释。我一般会先跑一个线性回归作为基线再跑随机森林最后上 LightGBM。如果 LightGBM 比线性回归提升不到 5%说明特征工程没做到位或者数据本身噪声太大换更复杂的模型也没用。4.2 用 LightGBM 训练光伏功率预测模型的完整代码import lightgbm as lgb from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score # 准备训练、验证、测试数据 X_train train_df[feature_cols] y_train train_df[power] X_val val_df[feature_cols] y_val val_df[power] X_test test_df[feature_cols] y_test test_df[power] # 构造 LightGBM 数据集 train_data lgb.Dataset(X_train, labely_train) val_data lgb.Dataset(X_val, labely_val, referencetrain_data) # 参数设置 params { objective: regression, metric: mae, boosting_type: gbdt, num_leaves: 31, learning_rate: 0.05, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1, random_state: 42 } # 训练带早停 model lgb.train( params, train_data, num_boost_round1000, valid_sets[val_data], callbacks[lgb.early_stopping(stopping_rounds50), lgb.log_evaluation(period100)] ) # 预测 y_pred model.predict(X_test, num_iterationmodel.best_iteration) # 评估 mae mean_absolute_error(y_test, y_pred) rmse np.sqrt(mean_squared_error(y_test, y_pred)) r2 r2_score(y_test, y_pred) print(fMAE: {mae:.2f} kW, RMSE: {rmse:.2f} kW, R2: {r2:.4f})参数说明num_leaves31控制树复杂度光伏数据通常 31 到 63 够用再大容易过拟合。learning_rate0.05配合num_boost_round1000和早停是比较稳的组合。feature_fraction0.8和bagging_fraction0.8引入随机性防止模型死记训练集。early_stopping(stopping_rounds50)表示验证集 MAE 连续 50 轮不下降就停避免无效训练。4.3 评估指标怎么选MAE、RMSE、R² 各自说明什么MAE 是平均绝对误差单位是 kW直接告诉你预测平均偏了多少。RMSE 对大误差更敏感如果 RMSE 远大于 MAE说明存在少数预测特别离谱的样本通常是云层突变或传感器故障。R² 是拟合优度但光伏数据里 R² 容易虚高因为夜间零值被剔除了白天功率本身变化范围大R² 到 0.9 以上不稀奇。我更关注归一化 MAE也就是 MAE 除以装机容量。比如 10 MW 电站MAE 是 0.5 MW归一化后 5%这个数字对调度才有意义。另外按小时分组的误差也很重要早上和傍晚的误差通常比中午大如果只看整体 MAE会掩盖这些局部问题。# 按小时分组计算 MAE test_df test_df.copy() test_df[pred] y_pred test_df[hour] test_df[timestamp].dt.hour hourly_mae test_df.groupby(hour).apply( lambda g: mean_absolute_error(g[power], g[pred]) ) print(hourly_mae)如果某个小时的 MAE 明显高于其他小时就要回去看那个时段的特征是不是有问题比如太阳高度角计算偏差、或者滞后特征在那个时段失效。5. 避坑与排查光伏功率预测里最容易翻车的 4 个地方5.1 现象验证集 MAE 很低测试集 MAE 翻倍原因验证集和测试集的时间分布不一致。比如验证集落在春天测试集落在夏天模型没学过高温高辐照下的功率衰减。解决按时间顺序切分后检查两个集合的辐照度、温度分布如果差异大用滚动预测的方式做多折验证而不是只切一次。5.2 现象早上第一小时预测功率为负原因模型是回归模型输出没有物理约束低辐照度时可能预测出负值。解决在预测后加clip(0, capacity)把输出限制在 0 到装机容量之间。更彻底的做法是用 Tweedie 或 Gamma 分布做目标函数但 LightGBM 的objectiveregression加后处理更简单。5.3 现象特征重要性里辐照度排第一但模型在阴天误差巨大原因阴天辐照度波动剧烈滚动均值和滞后项跟不上变化。解决加一个“辐照度变化率”特征即当前辐照度减去上一时刻辐照度再除以时间间隔。这个特征能捕捉云层遮挡的突变。day_df[irradiance_diff] day_df[irradiance].diff() / 0.25 # 15分钟0.25小时5.4 现象训练集损失一直在降验证集损失很早就开始升原因过拟合。光伏数据里如果特征里包含了很多滞后项模型容易记住训练集的日内模式。解决降低num_leaves提高min_data_in_leaf或者加 L2 正则lambda_l2。我一般会把min_data_in_leaf设到 20 以上防止叶子节点样本太少。6. 进阶技巧用滚动预测和误差归因把系统做扎实6.1 滚动预测别用未来数据预测过去实际部署时你不可能拿到未来的气象数据。所以评估阶段就要模拟真实场景用过去 24 小时的数据预测未来 1 小时然后滚动前进。下面是一个简单的滚动预测框架。def rolling_forecast(df, model, feature_cols, window96, horizon4): window: 输入窗口长度15分钟粒度下 96 个点 24 小时 horizon: 预测步长4 个点 1 小时 preds [] for i in range(window, len(df) - horizon): X_input df[feature_cols].iloc[i-window:i].values.reshape(1, -1) # 这里简化处理实际应该用最近一个时间点的特征 X_pred df[feature_cols].iloc[i:i1] pred model.predict(X_pred)[0] preds.append(pred) return np.array(preds)实际做的时候特征里的滞后项和滚动均值都要用历史数据实时计算不能直接用整张表的预计算值。这个细节决定了离线评估和线上效果能不能对上。6.2 误差归因把 MAE 拆到天气类型上整体 MAE 是 5%但晴天可能只有 2%阴天 15%。把测试集按天气类型分组分别算误差能帮你判断模型到底行不行。天气类型可以用辐照度方差来粗分方差小的是晴天方差大的是多云或阴天。test_df[irradiance_std] test_df[irradiance].rolling(window4).std() test_df[weather_type] pd.cut( test_df[irradiance_std], bins[-1, 20, 80, 1000], labels[晴天, 多云, 阴天] ) weather_mae test_df.groupby(weather_type).apply( lambda g: mean_absolute_error(g[power], g[pred]) ) print(weather_mae)如果阴天误差是晴天的 3 倍以上说明模型对突变场景的捕捉不够可以考虑加一个分类模型先判断天气类型再分别用不同的回归模型预测。6.3 一个我踩过的坑别用未来数据做特征标准化标准化参数均值和方差必须只用训练集计算然后应用到验证集和测试集。我见过有人图省事对整张表做StandardScaler().fit_transform()结果测试集的均值和方差泄露到了训练过程评估指标虚高。正确做法是fit在训练集transform在验证集和测试集。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val) X_test_scaled scaler.transform(X_test)这个习惯我坚持了很多年每次搭新流水线都会先检查这一条。光伏功率预测系统不难难的是每个环节都不骗自己。希望帮到你。本文还有配套的精品资源点击获取