ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

光伏功率预测实战:Python机器学习回归建模与特征工程要点

光伏功率预测实战:Python机器学习回归建模与特征工程要点 简介一套面向毕业设计、期末大作业和课程设计场景的机器学习光伏功率预测项目源码基于历史光伏发电数据构建预测模型涵盖数据加载、清洗、特征提取、模型训练与结果评估等完整流程。代码结构清晰且附有详细注释新手能轻松理解快速部署即可使用项目经过严格调试可稳定复现预测结果。资源包共十六个文件包含八份CSV训练与测试数据、四份Python脚本、一份Jupyter Notebook交互式分析文档、一份说明手册及配置忽略文件其中数据处理脚本负责加载和清洗数据训练预测脚本实现模型训练与输出Notebook可逐步演示项目过程压缩后约4.64MB整体轻量、目录清晰。目前已有二百一十八人学习下载属于导师认可的高分项目具备较高参考价值既可直接作为毕业设计蓝本也可帮助学习者系统掌握光伏功率预测的工程实现。1. 光伏功率预测项目值不值得投入:一场「靠天吃饭」的回归实战「光伏功率预测」这个标题,拆开看是新能源领域最典型的机器学习落地场景:用 Python 把辐照度、温度、湿度这些气象数据和历史发电功率组织成特征矩阵,交给回归模型,预测未来一段时间的功率曲线。课程设计、毕业设计、小规模电站的调度辅助,都绕不开这个核心问题。手头这份「Python 基于机器学习的光伏功率预测项目源码训练数据测试数据」,本质上就是把数据、特征、模型、评估串成一个能复现的闭环。它适合两类人:想拿高分又怕答辩追问原理的学生,和想认真搞懂时序建模、不想只跑 demo 的工程师。我建议把它当项目做透,而不是交差——光伏数据的零值、噪声和天气突变,能把机器学习的坑原原本本暴露出来,填平它们的过程就是能力长进的过程。2. 先建模还是先问任务:超短期光伏功率预测难在特征,不在模型很多人拿到项目包,第一步就是 pip install,然后直接跑 train.py,看到 R² 不错就认为交付了。这是最危险的做法。光伏预测和图像分类完全不同:它是带强物理背景的时间序列,任务口径、特征对齐、数据划分任何一个环节出错,指标都会骗你。我拿到这类项目,第一件事不是碰模型,而是把预测任务定义清楚,再决定特征怎么构造、数据怎么切。2.1 预测任务先定死:超短期、短期、中期选哪个光伏功率预测按提前时间分成三档。超短期预测面向未来 0~4 小时,时间分辨率一般是 15 分钟或 30 分钟,服务于实时调度和储能控制;短期预测面向未来 1~3 天,分辨率 1 小时,服务于日前发电计划;中期预测面向周和月,主要给检修和交易做参考。三档任务的输入特征完全不一样:超短期主要靠历史功率和实时辐照度,短期必须引入数值天气预报,中期则看长期气象趋势。你做课程设计或毕设,拿到的基本都是超短期数据集,15 分钟一条,预测未来几小时的功率曲线。预测类型提前时间时间分辨率典型用途特征难度超短期0~4 小时15 分钟 / 30 分钟实时调度、储能控制主要是辐照度与历史功率短期1~3 天1 小时日前发电计划依赖数值天气预报中期1 周~1 月日检修计划、交易依赖长期气象趋势所以打开项目包的第一步,是先看数据文件的时间戳和分辨率。15 分钟粒度基本就是超短期,别再往短期预测上凑。预测步长也要定死:是只预测下一个时间点,还是预测未来 4 小时这 16 个点?不同答案对应不同建模策略,后面第 6 章我会讲多步预测的做法。任务口径没定,后面的特征和指标都是空中楼阁。2.2 特征工程:气象数据与历史功率的时间对齐是命门超短期光伏功率预测里,特征的重要性远大于模型选择。我把常用特征分成三类:气象特征、历史功率特征、时间特征。气象特征里,水平面总辐照度 GHI 是绝对的榜首,它直接决定功率上限;温度影响组件效率,但数据集给的环境温度不是组件温度,只能近似;湿度、风速、气压是辅助特征,有就加,没有也不必强求。历史功率特征是超短期预测的另一个主力,尤其是未来 15 分钟到 1 小时的功率,和前几个时刻的实测功率高度相关。时间特征用来表达昼夜节律和季节变化,小时角用 sin/cos 编码,比直接用 0~23 的整数更合理。下面这段代码,是把原始 CSV 处理成特征矩阵的核心逻辑:import pandas as pd import numpy as np df pd.read_csv(data/train.csv, parse_dates[time]) df df.set_index(time).sort_index() # 统一到 15 分钟粒度,缺失值先用线性插值补上,limit 限制连续缺失长度 df df.resample(15min).mean().interpolate(limit4) feat pd.DataFrame(indexdf.index) feat[ghi] df[ghi].clip(0, 1400) # 辐照度按物理范围截断 feat[temp] df[temp] feat[humidity] df[humidity] # 时间特征:sin/cos 编码,避免 0 点和 23 点被数值上当成“差很远” feat[sin_hour] np.sin(2 * np.pi * feat.index.hour / 24) feat[cos_hour] np.cos(2 * np.pi * feat.index.hour / 24) # 历史功率滞后项:预测 t1,只能用 t, t-1, t-2 ... 的功率 for lag in [1, 2, 3, 6, 12]: feat[fpower_lag_{lag}] df[power].shift(lag) # 滚动统计:过去 1 小时的功率均值,shift(1) 防止把当前时刻信息带进去 feat[power_roll_mean_4] df[power].rolling(4).mean().shift(1) df_feat feat.join(df[power], howinner).dropna()这段代码有四处容易翻车的地方,对应四条参数纪律。第一,resample 会把乱掉的时间戳对齐到统一的 15 分钟网格,interpolate(limit4) 只允许连续补 4 个缺失点,超过就整段留空,避免把一段坏数据填成一条假直线。第二,ghi.clip(0, 1400) 把负数和超过物理上限的异常值直接截断,深夜传感器偶尔吐出的 -100 W/m² 会被纠正成 0。第三,滞后特征从 lag1 开始,意味着预测 t1 时,能用的最新信息是 t 时刻的真实功率。如果手滑写成 shift(0),等于把标签本身喂给模型,训练集 RMSE 会低到让你误以为自己调参很厉害。第四,滚动均值必须紧跟 shift(1),否则 rolling(4) 会把当前时刻的功率也纳入窗口。dropna 会吃掉开头滞后 12 个点的样本,对几万条数据来说损失可以忽略。我再强调一个容易被忽略的点:特征矩阵的时间索引必须和标签严格一一对应。join 的时候用 howinner,两边索引不一致的行直接丢弃,这是防止时间错位的最简单手段。项目包里的训练数据如果是多个电站拼接的,还要先按电站分组再做滞后特征,跨站点的滞后项没有物理意义。2.3 训练集与测试集切分:时序数据不能随机打乱模型可以换,评估方式不能乱来。分类任务里 train_test_split 随机打乱是常规操作,时间序列如果照搬,就是典型的翻车现场:相邻时间点的样本高度相似,随机切分会让验证集里混进训练集的“近亲”,R² 虚高,一旦真正按时间预测就崩。项目包如果已经给了 train.csv 和 test.csv,通常是按时间顺序切好的,你要做的是确认两个文件的时间区间不重叠,并且 test 的起点紧跟 train 的终点。from sklearn.model_selection import TimeSeriesSplit X df_feat.drop(columns[power]) y df_feat[power] # 时序交叉验证:训练集永远在验证集之前 tscv TimeSeriesSplit(n_splits5) for fold, (tr_idx, va_idx) in enumerate(tscv.split(X)): print(ffold {fold 1}: train {len(tr_idx)} 条, valid {len(va_idx)} 条)TimeSeriesSplit 的核心保证是:每一折的训练索引都小于验证索引,不会出现随机打乱造成的未来信息泄漏。它适合观察模型稳定性,但最终提交效果时,我一般直接用最简单的手动切分:前 80% 按时间顺序做训练,最后 20% 做测试,模型的预测能力要用这段“没见过的未来”来检验。千万别在划分上省事,这一步错了,后面所有调参都是自欺欺人。3. 用 LightGBM 跑通第一版预测:训练代码、参数与评估指标任务和特征定下来之后,模型选择反而简单。坦白说,第一版用什么模型不决定项目天花板,决定天花板的是特征质量和评估方式。对这个数据量级几十万条以下的光伏数据集,我的第一选择是 LightGBM,而不是很多教程里爱讲的 LSTM。3.1 为什么第一版选 LightGBM,而不是 LSTMLightGBM 是梯度提升决策树的工程实现,天生适合表格数据。光伏功率预测的特征矩阵是典型表格:几十个维度,含大量数值特征,样本量在几万条量级。这种场景下 GBDT 类模型训练快、调参路径清晰、自带特征重要性,而且对特征量纲不敏感,省掉归一化这个环节就少一个出错点。LSTM 确实能建模时序依赖,但数据量不够时很容易欠拟合,调参却要烧掉大量时间,对课程设计和工程一期来说性价比并不高。模型适合数据量训练速度调参难度解释性对这个任务的适配线性回归小快低高只能抓线性趋势,夜间容易出负功率随机森林中中中高能拟合非线性,但外推能力一般LightGBM中快中中首选,梯度提升对突变适应好XGBoost中中中中效果接近,训练略慢LSTM大慢高低数据量小时不如 GBDT,可作进阶对比我的习惯是:LightGBM 跑通并拿到稳定基线,再补一个 LSTM 或线性回归做对比,证明你理解不同机器学习算法的适用边界。项目报告里出现“为什么选 A 不选 B”的对比分析,比单纯堆一个高精度模型更值钱。3.2 最小训练代码:加载、切分、训练、评估一条龙先把依赖装好:pip install lightgbm scikit-learn pandas numpy。下面这份代码直接复用第 2 章的特征矩阵 df_feat,按时间切分后训练并输出三个指标:import lightgbm as lgb from sklearn.metrics import mean_absolute_error as mae_fn from sklearn.metrics import mean_squared_error as mse_fn from sklearn.metrics import r2_score import numpy as np X df_feat.drop(columns[power]) y df_feat[power] # 按时间顺序 8:2 切分,前 80% 训练,后 20% 测试 cut int(len(X) * 0.8) X_train, X_test X.iloc[:cut], X.iloc[cut:] y_train, y_test y.iloc[:cut], y.iloc[cut:] model lgb.LGBMRegressor( objectiveregression, learning_rate0.05, num_leaves31, max_depth7, min_child_samples20, n_estimators1200, subsample0.8, subsample_freq1, colsample_bytree0.8, random_state42, verbose-1 ) model.fit( X_train, y_train, eval_set[(X_train, y_train), (X_test, y_test)], eval_metricrmse, callbacks[lgb.early_stopping(100), lgb.log_evaluation(100)] ) y_pred np.clip(model.predict(X_test), 0, None) print(fMAE: {mae_fn(y_test, y_pred):.2f} kW) print(fRMSE: {np.sqrt(mse_fn(y_test, y_pred)):.2f} kW) print(fR2: {r2_score(y_test, y_pred):.4f})参数要按项目数据量调,不能照抄。learning_rate0.05 配合 n_estimators1200 是“小步快走”的组合,靠 early_stopping 在验证集 RMSE 连续 100 轮不降时自动截断,返回的是最优模型而不是最后一轮。num_leaves31 和 max_depth7 控制单棵树复杂度,光伏数据噪声大,叶子节点太多会专门去拟合零功率区的噪声。min_child_samples20 强制叶子节点至少有 20 个样本,这个参数对带大量零值的光伏数据尤其重要。subsample0.8 和 colsample_bytree0.8 是行列双重采样,是 GBDT 抗过拟合的主要手段。三种指标各有用处。MAE 反映平均误差水平,适合向非技术评审解释;RMSE 对大幅误差更敏感,阴天突变时模型崩一次,RMSE 立刻飙升,它是模型“上限稳定性”的照妖镜;R² 必须结合装机容量看,100 kW 的电站 MAE 做到 5 kW 已经很不错,换到 10 kW 的数据集同一个值就是灾难。项目报告里三个指标都贴,再补一句“RMSE 明显大于 MAE 说明误差分布存在长尾,主要来自天气突变段”,评审会觉得你理解了指标背后的物理。注意:y_pred 必须做 np.clip(..., 0, None)。树模型不懂物理,极端情况下会输出负功率,截断到 0 是工程上最常见的处理,不是作弊。3.3 训练数据里的昼夜失衡:白天建模、夜间置零光伏数据有个隐蔽的统计陷阱:夜间功率恒为 0,这部分样本能占训练集一半。模型在均方损失下,为了整体误差最小,会把白天峰值预测向均值方向压缩,结果就是“夜里猜得准,白天整体偏低”。我一般这么做:daytime df_feat[ghi] 10 X_day, y_day X[daytime], y[daytime]只用白天样本训练,预测阶段再根据测试数据的 ghi 判断是否直接置 0。这样 R² 可能反而“下降”,因为模型不再靠大量简单零样本刷分,但白天段的 MAE 才是光伏预测的真实水平。如果不想完全丢掉夜间段,也可以把功率标签做 log1p 变换后再训练,预测完再 expm1 还原,零值附近的样本不再主导梯度。两种做法都行,第一种物理含义更清晰,报告更好写。4. 训练数据与测试数据的组织方式:目录、列约定与归一化边界“项目源码训练数据测试数据”这三个要素,源码反而是最固定的,数据组织才是决定项目能否复现的关键。很多高分的课设项目,代码写得未必多花哨,但数据、脚本、产物分得清清楚楚,别人照着 README 能完整跑一遍,这就是工程加分项。4.1 一个能一键复现的项目目录长什么样我见过的光伏功率预测项目,目录结构大同小异,比较规范的是这样:pv_forecast/ ├── data/ │ ├── raw/ # 原始数据,只读不修改 │ ├── from_train.csv # 训练数据:带功率标签 │ └── test.csv # 测试数据:只有特征,无标签 ├── src/ │ ├── feature_engineering.py │ ├── train.py │ ├── predict.py │ └── evaluate.py ├── models/ │ └── packed_model.txt # 训练产物 ├── results/ │ ├── predictions.csv │ └── feature_importance.png └── README.mdraw 目录放原始 CSV,任何清洗都通过脚本写进新文件,绝不在原文件上改。src 目录每个脚本只干一件事:特征工程、训练、预测、评估,拆开后单独调试不互相牵连。models 和 results 是产物目录,归 git 管理时一般要忽略。这个结构的价值在于,三个月后你自己回来看,或者评审现场打开你的项目包,不需要你讲解就能按 README 重跑。README 里写清楚 Python 版本、依赖安装命令、运行顺序,这三个信息能省掉对方大量试错,也是给自己留的后悔药。特征工程脚本每次运行都会生成一份中间特征矩阵,我习惯把它落到 data/processed/ 下,这样训练脚本不用每次重跑特征计算,对比不同模型时也能保证输入完全一致。raw 只读的意义就在这里:数据源永远可追溯,出问题先怀疑中间步骤,而不是原始文件被动过。4.2 训练集与测试集的 CSV 列约定:先做校验再做特征这两个文件的列名必须完全一致,唯一区别是训练集多一列 power。实际项目里常见的列约定如下,时间列建议统一为一个时区,别用带 08:00 偏移的混合格式:列名类型说明建模角色timedatetime时间戳,升序排列,15 分钟粒度切分与对齐ghifloat水平面总辐照度,单位 W/m²最重要特征tempfloat环境温度,单位 °C特征humidityfloat相对湿度,单位 %可选wind_speedfloat风速,单位 m/s可选powerfloat实际发电功率,单位 kW训练标签读取之后先做三行校验,再谈建模:import pandas as pd train pd.read_csv(data/train.csv, parse_dates[time]) test pd.read_csv(data/test.csv, parse_dates[time]) assert train[time].is_monotonic_increasing, 训练数据时间必须升序 assert train[power].notna().all(), 训练数据标签不能有缺失 assert {time, ghi} set(train.columns), 列名不符合约定 print(ftrain: {len(train)} 条, {train[time].min()} ~ {train[time].max()}) print(ftest : {len(test)} 条, {test[time].min()} ~ {test[time].max()})时间单调递增是第一条命门,如果原始 CSV 里有乱序片段,resample 也救不回来,必须回到 raw 检查源头。测试数据没有 power 列,predict.py 输出时要带上原始 time 和 pred_power 两列,方便 evaluate.py 和榜单或人工核对。另外注意,训练数据的时间范围应该覆盖至少一个完整季节,光伏是强季节性对象,只给一个月数据,模型学不到天气跨季变化,测试段的误差必然爆表。两个 CSV 拼接成完整数据时,还要确认 time 没有重复,否则滞后特征会出现同一时刻多值打架的情况。4.3 归一化的边界:训练集 fit,测试集只能 transform树模型不需要归一化,但如果你做线性回归或 LSTM 对比,这一步必须严谨。先看错误做法:from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X) # 错误:全量数据一起 fit把训练和测试拼在一起 fit,均值和方差里就混入了测试集信息。这是最隐蔽的数据泄露,短期看不出问题,换一段真正未见过的数据评估立刻露馅。正确做法是只对训练集 fit,测试集单独 transform:scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)如果对标签也做归一化,还原预测值时要用同一个 scaler 反变换:y_pred_inv scaler_y.inverse_transform(y_pred_scaled.reshape(-1, 1)).ravel()这里容易出错的是 shape:sklearn 的 scaler 接受二维输入,单列标签要 reshape 成 (-1, 1),否则报错或静默广播。另外,如果标签里有负值,要先 clip 到 0 再归一化,否则 -0.1 会被 scaler 当作正常分布的一部分。记住一个原则:任何从数据里统计出来的量,包括均值、方差、插值用的整体统计,都只能在训练段上计算;测试段必须像一个没见过的陌生人来处理。这也是为什么我推荐第一版就用 LightGBM——树模型对单调变换不敏感,天然绕开归一化这个坑,等到做模型对比时再引入 scaler,出错面更小。5. 避坑 / 常见问题 / 排查:光伏功率预测最容易翻车的五个地方下面这五条,是我在类似项目里踩过的坑,或者是帮别人排查时见过的高频故障。每条按「现象、原因、解决」写,你可以把这一章当成 debug 手册对照使用。5.1 夜间零样本太多,白天预测被“平均”拖低现象:训练完看 MAE、RMSE 都还不错,但画出预测曲线,白天峰值明显偏低,整体被往下压。原因:夜间功率恒为 0,零样本占接近一半,模型为了最小化均方误差,把白天预测向 0 拉近,拿夜间零值的得分补贴白天峰值的失分。损失函数不关心物理过程,它只关心平均误差最小。解决:按 ghi 10 筛出白天样本训练,预测阶段对夜间样本直接置 0;或者对功率标签做 log1p 变换,降低零值区在损失函数里的权重,训练完再 expm1 还原。前者物理意义清晰,报告更好写。5.2 随机切分让 R² 虚高,换段时间就露馅现象:用默认 train_test_split,验证集 R² 达到 0.95 以上,信心满满;换成按时间顺序的测试集,指标掉到 0.6 左右。原因:随机打乱把相邻时间点的相似样本同时分进训练和测试,模型在验证时“见到过”目标时刻的邻居,等于开卷考试。时间序列的样本不是独立的,相邻样本共享天气过程和历史功率模式。解决:所有评估都按时间顺序切分,固定前 80% 训练、后 20% 测试;要报告稳定性就用 TimeSeriesSplit。项目包提供的 train.csv/test.csv 如果已经是按时间切的,直接沿用,但先检查两个文件的时间区间是否衔接且不重叠。5.3 滞后特征写反方向:shift 的时机错了现象:训练集指标好得不真实,比如 RMSE 接近 0,R² 高达 0.99;特征重要性里 power_lag_1 一骑绝尘,其他特征全部趋近于 0。原因:构造滞后项时用了 shift(0),等于把当前时刻的真实功率当成特征,模型直接抄答案;滚动均值漏掉 shift(1) 也是同类错误。这两个错误在代码审查时很难用肉眼发现,因为语法合法、运行不报错。解决:构造完特征后打印 df_feat.head(),滞后列第一行必须是 NaN;更狠的办法是把某几行样本的 power_lag_1 改成随机值,看预测是否剧烈波动——如果变化巨大,说明模型在依赖标签的近亲,赶紧回头查 shift。power_lag_1 重要性高本身不一定是坏事,但它高到脱离其他特征时,优先怀疑泄漏。5.4 归一化和插值把测试集信息提前用掉现象:同一份数据反复验证都高分,换一个电站或者换一段时间的测试数据,误差翻倍。原因:最常见的是 StandardScaler 对全量数据 fit,或者缺失值用整列均值填充,统计量里混入了测试片段的信息,模型相当于提前“背过”未来数据的分布。这类问题比 5.3 更隐蔽,因为它不产生夸张的高分,只是让模型泛化能力悄悄变差。解决:所有统计变换只允许在训练段上 fit;缺失值用时间插值或前向填充,不要用整列均值;实在拿不准,直接用 LightGBM,它原生支持 NaN,树分割时自动决定缺失值走向,这个问题从源头消失。5.5 多云突变日误差爆炸:别硬调参,解释比修正更重要现象:大部分日期预测曲线贴合得很好,一到多云天或阵雨过后,误差是平时的 5 倍,怎么调都在这一点上撞墙。原因:云层遮挡导致的辐照度突变是分钟级物理过程,15 分钟分辨率下,输入信息里根本没有刻画云团移动的变量,任何统计模型都做不到精准预测,这是任务的物理信息极限,不是模型坏了。解决:按天气类型分组评估,晴天、多云分开报指标;有多云特征(云量、云类型预报)就加进模型,没有就承认这部分不可预测。把误差分析写进报告,比硬调参更像专业人士——评审要的是你能解释边界,而不是假装模型万能。6. 再进一步:滚动预测策略与「拿高分」的验证方法前面所有内容解决的是“跑通”和“不翻车”,到这一章,说说怎么让它从“能跑”变成“能打”。6.1 多步预测用 direct 策略:一次建一套模型,误差不累积超短期光伏功率预测的真实需求,是给出未来 1 小时甚至 4 小时的功率曲线,也就是要预测多步。常见做法有两种:迭代预测把上一步输出当下一步输入,误差会像滚雪球一样累积;我更推荐 direct 策略,为每个预测步长单独训练一个模型。用第 2 章的特征矩阵,构造未来第 k 步的标签:base df_feat.drop(columns[power]) # 先把原始标签拿走 model_params dict( learning_rate0.05, num_leaves31, max_depth7, min_child_samples20, n_estimators800, subsample0.8, colsample_bytree0.8, random_state42, verbose-1 ) for k in [1, 4, 16]: df_k base.copy() df_k[target] df[power].shift(-k) # 未来第 k 步的真实功率 df_k df_k.dropna() X_k df_k.drop(columns[target]) y_k df_k[target] model_k lgb.LGBMRegressor(**model_params) model_k.fit(X_k, y_k) print(fk{k}: 用当前特征直接预测未来第 {k} 个时间点)shift(-k) 让标签指向未来,训练时每个模型只学“从当前特征到未来某时刻”的映射,预测时输入当前特征直接出结果,不存在误差回填。k1 是 15 分钟后,k4 是 1 小时后,k16 是 4 小时后,三套模型独立训练、独立评估,还能观察到误差随步长增长的规律,这个分析放在报告里很有说服力。6.2 三张图和一份误差归因,决定项目上限项目答辩或验收时,决定印象分的往往不是 RMSE 那个数字,而是你展示的验证方法。我每次交付必带三张图:真实功率与预测功率的时间序列曲线,把晴天段和多云段圈出来对比;预测值与误差的散点图,看大功率区间是否系统性偏低;特征重要性条形图,说明辐照度和历史功率主导预测的物理原因。三张图配合分组指标表,基本就能把“我理解这个任务”这件事讲透。我的习惯是,每次跑完先看夜间和白天分组指标,再看特征重要性,最后才碰超参数。因为光伏预测的黑匣子不在模型里,而在数据的时间语义里——顺序搞反了,调参只是在给错误系统打补丁。希望帮到你。本文还有配套的精品资源点击获取
返回列表