光伏功率预测机器学习实战:从特征工程到随机森林模型
2026/9/11 14:56:17 网站建设 项目流程

简介:这是一份基于机器学习的光伏功率预测项目完整工程包,面向需要完成毕业设计、期末大作业或课程设计的高校学生,也适合想入门回归预测任务的开发者;项目覆盖数据处理、特征构建、模型训练与预测输出等完整流程,代码附有注释,关键模块简单修改即可适配其他新能源功率预测场景。压缩包共16个文件,小巧易传,约4.64MB;其中8个csv文件为训练集与测试集数据,4个py文件包含数据加载、预处理、训练预测等核心逻辑,另有ipynb交互式笔记本、说明文档和任务书docx,便于快速理解项目结构并运行调试,目前已有218人学习下载。从内容预览看,代码以光伏直流功率预测为场景,包含完整的训练集与测试集划分,用户在安装基础Python库后即可直接跑通结果;作为导师认可的高分项目,其整体功能完整、界面清爽简洁,既能直接作为毕设演示,也能在此基础上扩展做对比实验或学术分析,实际应用价值较高。

1. 为什么光伏功率预测要落到机器学习上

光伏电站的出力曲线看起来是“日出而作、日落而息”的确定性过程,真正做过的才知道,一块云飘过来功率能瞬间掉 40%,再飘走又弹回去。电网调度要的是未来 15 分钟到 4 小时的功率曲线,靠物理模型做辐照度反演,误差大且部署成本高;靠持续预测(persistence)在晴天还行,多云天几乎失效。于是机器学习成了这类功率预测项目的现实解:用历史发电功率加气象特征,训练回归模型直接拟合“辐照度→功率”的映射关系。

这个项目是一套完整可跑的 Python 光伏功率预测工程,结构里有 DC_Data 下的 train_1 到 train_4、test_1 到 test_4 共八份 CSV,配合 Data_Process.py 做数据清洗、Train_Predict.py 做模型训练与评估、Load_Save_Data.py 做序列化持久化,主入口 main.py 和 DC_PV_Power_Predict_2018.ipynb 方便你命令行跑或 notebook 逐步调。适合两类人:一是做毕业设计、期末大作业,需要一个能讲清数据流和模型对比的完整项目;二是刚接触时序预测的工程师,想找一个能替换特征和模型的最小骨架。下面按数据、特征、训练、评估、部署的顺序拆开讲。

2. 数据预处理与特征构造:从 CSV 到模型输入

2.1 训练数据长什么样

拿到 train_1.csv 这类文件,先别急着训练。光伏功率预测的数据集一般包含时间戳、环境辐照度、组件温度、环境温度、风速、湿度,以及实际发电功率。先读出来看一眼结构:

import pandas as pd df = pd.read_csv("DC_Data/train_1.csv", encoding="utf-8") print(df.info()) print(df.head(10)) print(df.isnull().sum())

这段代码做了三件事:打印字段类型与缺失数量、预览前 10 行、统计每列空值。光伏采集设备偶尔掉线,时间戳不连续和功率列为 0 是常见现象,缺失值比例超过 5% 的列要做标记,不要直接 fillna。

2.2 清洗策略:异常值和夜间数据

光伏功率预测有个容易忽略的问题:夜间数据全是 0,占了样本总量近一半。直接用全量数据训练,模型会被大量零值带偏。我处理这类序列的常见做法是,先按辐照度阈值过滤掉夜间样本,再对功率列做 3σ 去异常:

import numpy as np # 过滤夜间:辐照度低于 10 W/m² 的点视为无意义样本 df = df[df["irradiance"] > 10].copy() # 功率列 3σ 去异常 mean_p, std_p = df["power"].mean(), df["power"].std() df = df[(np.abs(df["power"] - mean_p) < 3 * std_p)].copy() # 时间戳解析并排序 df["time"] = pd.to_datetime(df["time"]) df = df.sort_values("time").reset_index(drop=True)

过滤夜间数据能显著减少模型对零样本的过拟合;3σ 去异常去掉的是传感器抖动或逆变器限功率导致的跳变点。注意逆变器限功率(curtailment)产生的“平台期”是真实物理现象,不全是噪声,异常检测后要人工抽查一下被删样本是否连续成段。

2.3 特征工程:滞后特征比什么都管用

光伏功率预测中的最大信息来源,不是天气变量,而是功率自身的历史值。因为辐照度传感器和组件实际接收到的能量之间隔着积灰、遮挡、温度系数等一堆干扰,而历史功率是这些因素综合作用后的最终结果。我一般至少构造三个维度的特征:

df["hour"] = df["time"].dt.hour df["month"] = df["time"].dt.month df["power_lag_1"] = df["power"].shift(1) # 上一时刻功率 df["power_lag_2"] = df["power"].shift(2) df["power_rolling_30min"] = df["power"].rolling(30, min_periods=1).mean() df["irradiance_lag_1"] = df["irradiance"].shift(1) df = df.dropna().reset_index(drop=True)

滞后 1 时刻和 2 时刻的功率,对齐的是未来值本身的短时惯性;30 分钟滑动平均可以平滑云的瞬时遮挡;辐照度滞后一时刻是因为热惯性——组件温度变化滞后于辐照度变化,功率响应也滞后。这个特征组合在后面的模型对比中会比单纯用天气变量高不少。

2.4 归一化与数据集切分

机器学习模型输入量纲不一致会拖慢收敛,树模型虽然不敏感,但如果后面要换 LSTM 或 MLP,归一化是必选项。切分时要注意:时间序列不能随机打乱再切,否则训练集“偷看”了未来。

from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split feature_cols = ["hour", "month", "power_lag_1", "power_lag_2", "power_rolling_30min", "irradiance_lag_1"] X = df[feature_cols].values y = df["power"].values scaler_x = MinMaxScaler() scaler_y = MinMaxScaler() X_scaled = scaler_x.fit_transform(X) # 按时间顺序切分:前 80% 训练,后 20% 验证 split_idx = int(len(X_scaled) * 0.8) X_train, X_valid = X_scaled[:split_idx], X_scaled[split_idx:] y_train, y_valid = y[:split_idx], y[split_idx:]

这里没有用 train_test_split 的随机切分参数,而是按索引位置硬切。时序预测中 RandomState 固定但 shuffle=True 同样会造成数据泄漏,写代码时最容易踩。如果你要对比不同模型的表现,这套归一化和切分逻辑要保持完全一致。

3. 模型选型与训练:为什么随机森林是可靠的基线

3.1 基线模型选择逻辑

光伏功率预测项目里见过有人直接上 LSTM,结果训练时间长、调参困难,效果还不如决策树模型。原因是这类数据集通常只有几万到十几万行,深度网络的优势发挥不出来。我给这个项目定的基线是随机森林回归器:非线性拟合能力强,能捕捉辐照度与功率之间的分段关系,对异常值鲁棒,不需要精细调参就能拿到可用的分数。

from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error model = RandomForestRegressor( n_estimators=300, max_depth=15, min_samples_leaf=3, random_state=42, n_jobs=-1 ) model.fit(X_train, y_train) y_pred = model.predict(X_valid) print("R2:", r2_score(y_valid, y_pred)) print("MAE:", mean_absolute_error(y_valid, y_pred)) print("RMSE:", np.sqrt(mean_squared_error(y_valid, y_pred)))

n_estimators 设 300 是精度与耗时的折中,超过 500 收益很小;max_depth=15 防止单棵树过深导致过拟合;min_samples_leaf=3 保证叶节点有足够样本支撑预测稳定性;n_jobs=-1 用满所有 CPU 核心。对几万行数据量,这个配置训练时间在几十秒量级。

3.2 特征重要性分析:验证你的特征工程

训练完成后第一件事是看特征重要性,这能验证滞后特征的设计是否有效。如果 hour 和 month 排在前面,说明数据里可能没有包含天气预报值,模型在用周期性硬扛;如果 power_lag_1 重要性显著领先,说明短时惯性是主要预测来源,符合光伏出力的物理特性。

importances = pd.Series(model.feature_importances_, index=feature_cols) print(importances.sort_values(ascending=False))

随机森林的特征重要性基于基尼不纯度减少量,它告诉你的是“这个特征在减少预测误差中的参与度”,不是因果贡献。比如 irradiance_lag_1 重要性低,不代表辐照度没用,可能只是被 time 相关特征吸收了共线性。做特征筛选时不要只砍掉低重要性特征,要结合删除后验证集分数变化来定。

3.3 扩展到 XGBoost:随机森林之外的另一个选择

随机森林是 bagging 思路,XGBoost 是 boosting 思路,后者在同样数据上往往能再提升一点精度,代价是需要调学习率和树深度。XGBoost 对缺失值有内建处理,如果原始数据清洗想偷懒可以依赖它,但我还是建议把清洗做在前面,模型才能专注拟合有效模式。

import xgboost as xgb xgb_model = xgb.XGBRegressor( n_estimators=500, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, random_state=42 ) xgb_model.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], verbose=False) print("XGB R2:", r2_score(y_valid, xgb_model.predict(X_valid)))

learning_rate=0.05 配合 n_estimators=500,是 XGBoost 常见的“小步慢走”配置;subsample 和 colsample_bytree 都设为 0.8,让每棵树只看到 80% 的样本和 80% 的特征,降低过拟合。XGBoost 的优势在于它对特征之间的交互项拟合更细,但如果你发现验证集分数和训练集分数差得远,优先降 max_depth 或增大 min_child_weight。

4. 项目代码结构与再训练流程:从 ipynb 到生产脚本

4.1 文件职责划分

这个项目的文件组织方式是典型的“数据处理与模型训练分离”,main.py 是总入口,Data_Process.py 里是清洗与特征构造逻辑,Train_Predict.py 里是模型训练与评估,Load_Save_Data.py 负责把处理好的数据和训练好的模型落盘。这种分层的意义在于:换数据集时只改 Data_Process.py,换模型时只动 Train_Predict.py,评估指标变了也只动最后一段。

# main.py 中典型的调度逻辑 from Data_Process import build_dataset from Train_Predict import train_model, evaluate_model from Load_Save_Data import save_model, load_model if __name__ == "__main__": df = build_dataset("DC_Data/train_1.csv") X_train, X_valid, y_train, y_valid, scaler_y = train_model("DC_Data/train_1.csv") pred, metrics = evaluate_model(X_valid, y_valid) save_model(model, path="output/model.pkl")

入口脚本里我只写了四个函数的调用,实际使用时每个函数内部会打印日志和数据形状,方便追查哪一步出了问题。我自己跑这类项目时习惯在 build_dataset 里返回一个 dict 而不是多个变量,否则函数参数会越改越长。

4.2 DC_PV_Power_Predict_2018.ipynb 适合什么场景

notebook 文件和纯脚本的区别在于迭代速度。如果你还在摸索特征组合、对比模型阶段,建议用 notebook 的方式,每个 cell 只做一件事,变量都在内存里,改完特征立即可见。文件里已经是按流程排好的 cell,从读数据到出指标一路跑下来,最后可以导出 HTML 放到毕设附录里当作实验记录。真正要批量预测多个文件时,再退回 main.py 用命令行跑。

4.3 多文件训练数据的处理差异

项目里 train_1 到 train_4 是四份独立数据,每份对应的电站、季节、天气条件可能不一样。不要简单地把四个文件纵向堆在一起训练——如果各文件采样频率或功率单位不一致,堆起来的模型反而会被数据量大的那份主导。我建议的做法是每个文件单独训练一个模型,预测时按电站编号路由到对应模型。项目里测试集 test_1 到 test_4 与训练集编号对应,设计上就是这个思路。

以 train_1.csv 作为基准模型,test_1.csv 作为泛化验证:

# 加载已保存模型并对新数据预测 model = load_model("output/model_1.pkl") test_df = pd.read_csv("DC_Data/test_1.csv") test_df["power_lag_1"] = test_df["power"].shift(1) test_df = test_df.dropna().reset_index(drop=True) X_test = test_df[feature_cols].values X_test_norm = scaler_x.transform(X_test) test_pred = model.predict(X_test_norm)

加载模型时也要一起加载 scaler_x 和 scaler_y,很多人保存模型时漏了标准化器,预测阶段对测试数据重新 fit 了一遍,分数看起来还行,实际一上线就崩。这个坑在时序预测项目里出现频率极高。

5. 误差分析与模型调优:预测曲线对不上真实功率时怎么办

5.1 晴天 vs 多云天的分段评估

光伏功率预测里,全局 R² 好看不代表模型可用。多云天突变点的预测误差会被晴天样本稀释。我把验证集按天气分段评估,做法是按辐照度变化率把样本分成稳定段和波动段:

df_valid = df.iloc[split_idx:].copy() df_valid["irr_diff"] = df_valid["irradiance"].diff().abs() df_valid["segment"] = np.where(df_valid["irr_diff"] > 50, "cloudy", "stable") for seg in ["stable", "cloudy"]: mask = df_valid["segment"] == seg y_seg = y_valid[mask] pred_seg = model.predict(X_scaled[split_idx:][mask]) print(seg, "R2:", r2_score(y_seg, pred_seg), "MAE:", mean_absolute_error(y_seg, pred_seg))

这段代码里 irr_diff 大于 50 W/m² 视为云的快速移动。实际操作中这个阈值要根据你的数据采样频率调整,分钟级数据 50 合适,秒级数据可能要放大。分段评估的目的是定位误差来源——如果波动段误差远大于稳定段,说明模型缺少“变化趋势”类特征。

5.2 误差时间分布与偏置修正

画出误差随小时变化的曲线,常常能看到固定规律:上午 8 点系统性低估,下午 16 点系统性高估。这是模型对辐照度爬坡和衰减阶段曲率拟合不足的表现。一个简单的修正是按小时学习误差偏置,然后叠加到预测值上:

errors = y_valid - model.predict(X_scaled[split_idx:]) hours_valid = df["hour"].iloc[split_idx:].values bias_df = pd.DataFrame({"hour": hours_valid, "error": errors}) hourly_bias = bias_df.groupby("hour")["error"].mean() def unbiased_predict(model, X, hours): raw_pred = model.predict(X) return raw_pred + hours.map(hourly_bias).values

偏置修正的本质是让模型在某个固定时段的高频误差被统计平均抵消。它能提升 MAE,对 R² 的影响通常有限,因为 R² 看的是方差解释度,偏置修正不改变化形状。我一般只在最后的集成模型上做这一步,在调参阶段就做会掩盖真实问题。

5.3 超参数调优的边界:网格搜索跑多久才算合理

用 RandomForestRegressor 时,n_estimators 从 100 调到 1000 耗时线性增长,但精度提升很小;max_depth 从 5 到 15 效果明显,超过 20 开始过拟合。XGBoost 的调参顺序有讲究:先定 learning_rate 和 n_estimators,再调 max_depth 和 min_child_weight,最后调 subsample 和 colsample_bytree。不要一上来就跑 GridSearchCV 的全参数组合,几百个组合跑完可能花了几个小时,收获却是“和默认参数差不多”。

对于十万行以内的数据,先手动把树模型跑通,再考虑要不要上 LSTM。深度学习在时序预测上不是银弹——光伏功率预测的主要挑战是云的随机移动,这一部分即使 LSTM 也学不会,除非引入数值天气预报(NWP)数据作为额外特征。项目里没有包含气象预报文件,所以现有模型做到 R² 0.95 以上、MAE 在额定功率的 5% 以内,就已经是数据质量支撑的合理上限了。

6. 超短期预测的进阶验证:跑通测试集只是开始

项目交付的测试集 train_1 到 test_4 是同一个时间段内切出来的还是另一个季节的,这是验证模型泛化能力的关键。把 test_1.csv 跑出 R² 后,拿 train_2.csv 训练出的模型去预测 test_2.csv,同时用 train_1.csv 的模型去预测 test_2.csv,两个结果做对比,能看出模型是否过拟合到训练集的特定天气形态。这一步在很多课程设计里没有做,但它恰恰是答辩时最能体现工程判断力的部分。

最后分享一个我在这类项目上最常用的验证技巧:预测步长拉伸测试。训练阶段构造 power_lag_1、power_lag_2 做特征,模型学会的其实是“一步超前”的映射;但实际调度要的是未来 1 小时甚至 4 小时的功率曲线。这时候用模型递归预测——把模型输出的预测值当作下一次预测的 power_lag_1 输入,连续迭代 4 次到 12 次,观察误差累积速度。

def recursive_forecast(model, last_power, irradiance_seq, n_steps): preds = [] current_power = last_power for i in range(n_steps): feat = np.array([[hour_seq[i], month, current_power, current_power, current_power, irradiance_seq[i]]]) feat = scaler_x.transform(feat) current_power = scaler_y.inverse_transform( model.predict(feat).reshape(-1, 1))[0, 0] preds.append(current_power) return preds

如果递归预测 6 步之后误差从 3% 膨胀到 15%,说明模型依赖的是短时惯性,没有学到辐照度曲线形状的先验。这个结果本身不是“项目不合格”,而是告诉你这类机器学习方案适用于 15 分钟以内的超短期预测,更长的时间尺度需要引入外部辐照度预报或用 sequence-to-sequence 结构重构输入输出窗口。能说清楚这个边界,比模型分数多 0.01 更值钱。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询