☰
光伏功率预测实战:机器学习模型对比与完整源码解析
2026/10/9 18:14:25 网站建设 项目流程

简介:面向光伏功率预测的机器学习项目源码包,适合毕业设计、课程设计与期末大作业参考。项目提供完整 Python 工程化实现,覆盖数据加载、数据预处理、模型训练与预测输出等环节,并附带 Jupyter Notebook 交互式分析文件和说明文档,便于快速理解代码逻辑,整体结构清晰,适合作为高分毕设或课程作业的基础框架。

压缩包共 16 个文件,以 4 个 Python 脚本和 8 个训练/测试 CSV 数据文件为主体,另有 ipynb、md、docx 等辅助文件,整体大小仅 4.64MB,部署使用非常轻便。代码注释细致,新手也能看懂,可对照学习光伏功率预测中的特征工程与回归建模流程。

目前已有 316 人学习/下载。下载后可直接获得可运行的工程入口、划分好的训练与测试数据集、任务说明文档及关键脚本模块,可在本地快速复现光伏功率预测实验,并在此基础上替换数据、调整特征或模型进行二次开发。

1. 光伏功率预测这个标题,到底在解决什么问题

光伏功率预测说白了就是回答「未来 15 分钟到 24 小时,这块光伏电站能发多少电」。电网调度要提前知道出力曲线来安排火电启停,电站业主需要预测来优化自己的交易策略,运维人员要判断是天气原因还是设备故障导致出力骤降。这个标题里的「机器学习」不是炫技,而是因为光伏出力序列有很强的非线性和随机性,传统物理模型(基于辐照度、温度直接折算)在阴雨天和云团快速移动时误差大得离谱。基于历史功率和气象数据训练出来的模型,反而能抓住那些物理公式表达不出来的规律——比如某片区域在特定风向下,云团会提前多少分钟挡住太阳。

这个项目适合谁?三类人。一是刚入门 ML 但不想做烂大街的房价预测、鸢尾花分类的在校生,光伏功率是典型的时间序列回归问题,做完整一套能同时练到特征工程、模型调参、评估指标这些核心技能。二是光伏电站的运维或数据分析人员,手上攒了一堆 SCADA 历史数据却不知道怎么变现,这套方案可以直接拿去改改用。三是做电力市场交易或微电网调度的开发者,需要一个能做基线预测的模块。标题里带了「源码+训练数据+测试数据」,意味着你不需要自己去爬数据、清洗半天才能动手,拿到就能跑通,这也正好踩中了大部分人「急着看效果」的诉求。

这类项目的坑往往不在模型而在数据。常见的问题是时间戳对不齐、辐照度传感器损坏导致异常值、夜间零功率段污染训练集,这些我在后面的章节里会逐条拆开。先放一个结论:别一上来就上 LSTM,先把梯度提升树调明白,多数场景下效果已经够用,而且调试成本低一个量级。

2. 光伏功率预测的建模思路:先弄清楚预测什么、用什么预测

2.1 预测任务的三种粒度,决定了你的模型结构

光伏功率预测按时间粒度分大致三类:超短期(未来 15 分钟到 4 小时)、短期(未来 1 到 3 天)、中期(周级别)。这个标题的项目通常落在超短期和短期之间,因为训练数据和测试数据的口径一般按「历史功率序列 + 对应气象预报」来组织。超短期预测主要靠历史功率的时序惯性,因为未来几小时内云团运动有连续性;短期预测则更依赖数值天气预报(NWP)中的辐照度、温度、湿度等变量。

这三种粒度对应的模型结构差异很大。如果你只是基于历史功率序列做滚动预测,那用滞后特征(lag feature)加梯度提升树就能拿到不错的基线;如果你要把天气预报数据也灌进去做 24 小时预测,那就需要考虑用序列模型或者把气象变量和时序特征拼在一起做多变量回归。拿到项目源码后第一件事不是急着跑,而是打开数据文件确认三件事:时间分辨率是多少(15 分钟?1 小时?)、有没有对应的气象特征列、训练集和测试集的时间段是否有重叠。

延迟特征的选择是个关键点。光伏功率序列在晴天表现出极强的「日周期性」——上午爬坡、中午平台、下午下坡,所以 lag 特征不像纯随机时间序列那样只取前几个时刻,而是要取「昨天同一时刻」「前天同一时刻」「前一个小时」「前 15 分钟」这类具有物理含义的滞后点。有些项目直接把前 168 小时的全部功率作为特征灌给模型,维度爆炸不说,还引入了大量噪声。

表:项目里常见的时间分辨率与模型选择对应关系

时间分辨率预测范围常用特征推荐模型
15 分钟未来 15min-4h历史功率滞后项、滚动均值、云量梯度提升树 / LSTM
1 小时未来 1-24h数值天气预报辐照度、温度、湿度梯度提升树 / 随机森林
1 小时未来 1-3 天数值天气预报全变量 + 季节特征LightGBM / Prophet 对比

2.2 输入特征怎么构造:气象征变量 + 时间特征 + 滞后特征三板斧

光伏功率预测的特征工程是整个流程里最吃经验、也最影响最终精度的环节。第一板斧是气象征变量,包括地表水平辐照度(GHI)、环境温度、组件温度、风速、风向、湿度。其中辐照度是绝对的主导变量,功率和它的相关性在晴天能到 0.9 以上。第二板斧是时间特征:小时数、日天数、月份、星期几、是否节假日。这些特征看似简单,但能帮模型区分「冬夏日照长度差异」和「工作日与周末的用电负荷差异」。第三板斧是滞后特征,也就是上面提到的历史功率序列,用来捕捉云团运动的短期惯性。

特征构造的代码通常长这样:

import pandas as pd import numpy as np # 读取原始数据,假设索引是datetime类型,功率列名为power_kw df = pd.read_csv('solar_data.csv', parse_dates=['time'], index_col='time') df = df.sort_index() # 1. 时间特征:拆出小时、月份、一年中的第几天 df['hour'] = df.index.hour df['month'] = df.index.month df['dayofyear'] = df.index.dayofyear # 2. 滞后特征:昨天同一时刻、前天同一时刻、前一时刻 df['lag_24h'] = df['power_kw'].shift(96) # 15分钟分辨率下,96个点=24小时 df['lag_48h'] = df['power_kw'].shift(192) # 48小时前的功率 df['lag_1step'] = df['power_kw'].shift(1) # 上一个时刻的功率 # 3. 滚动特征:过去1小时的滑动均值,用于平滑瞬时波动 df['rolling_mean_1h'] = df['power_kw'].rolling(window=4, min_periods=1).mean() # 4. 剔除夜间零功率段,避免模型被大量0值带偏 df = df[(df['power_kw'] > 0) | (df['hour'] >= 5) & (df['hour'] <= 19)] # 5. 删除含NaN的行(因shift会产生NaN) df = df.dropna()

逻辑说明:shift(96)在 15 分钟分辨率下表示取 24 小时前的功率值作为特征,这样模型能学习到「今天的出力曲线大概率跟随昨天的形状」,这是光伏序列最显著的特征之一。rolling(window=4)计算过去 1 小时的滑动均值,等于对短时云层遮挡造成的毛刺做了平滑,能降低模型对瞬时抖动的敏感度。

参数说明:窗口大小的选择需要匹配你的数据分辨率——15 分钟数据用 96 是 24 小时,用 48 是半天的周期,这个要根据你项目的预测目标调整。滚动窗口长度我常用 4 或 8,对应 1 小时和 2 小时的平均。夜间剔除逻辑不是简单删行,而是用一个布尔掩码保留白天边界附近的样本,因为清晨和傍晚仍然有爬坡和下降的趋势信息,直接全删到 6 点到 18 点会丢掉过渡段。

2.3 归一化和数据切分:最容易翻车却最容易被忽略的环节

很多人在光伏功率预测上翻车不是因为模型选错,而是数据切分时把时间顺序打乱了。时间序列切分不能像图像分类那样随机 shuffle,否则模型会「偷看」未来数据,测试集上的指标虚高,一上真实环境就崩溃。正确做法是按时间先后切分:比如按 8:2 的比例把前 80% 的时间段作为训练集,后 20% 作为测试集。如果要做交叉验证,必须使用 TimeSeriesSplit 而不是 KFold。

归一化方面也有讲究。功率值和辐照度值在数值量级上差异很大(功率可能上千 kW,辐照度只有几百 W/m²),对基于距离的模型(如 SVM、KNN)影响巨大,但对树模型无所谓。如果你用梯度提升树或随机森林,可以不归一化;如果用神经网络、LSTM 或线性回归,必须归一化。常见的做法是用 MinMaxScaler 把数据缩放到 [0, 1],但要注意一个细节:必须用训练集的 min 和 max 去转换测试集,不能把测试集的数据混进来一起 fit,否则也是信息泄露。

from sklearn.model_selection import TimeSeriesSplit from sklearn.preprocessing import MinMaxScaler # 时间序列切分:按时间顺序,不做随机打乱 tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(X): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] y_train, y_val = y.iloc[train_idx], y.iloc[val_idx] # 训练/验证逻辑在此处展开 # 归一化:只拟合训练集,再转换训练集和测试集 scaler = MinMaxScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 注意:LSTM等神经网络需要三维输入 (样本数, 时间步长, 特征数) # 这里用滑动窗口构造序列样本,假设时间步长为24 def create_sequences(data, seq_length=24): xs, ys = [], [] for i in range(len(data) - seq_length): xs.append(data[i:i+seq_length]) ys.append(data[i+seq_length]) return np.array(xs), np.array(ys) X_seq, y_seq = create_sequences(X_train_scaled, seq_length=24)

逻辑说明:TimeSeriesSplit每次都在递增的历史数据上训练、在下一个时间块上验证,这模拟了真实场景中「用过去预测未来」的过程。create_sequences是为 LSTM 或 GRU 准备数据的标准方式,滑动窗口长度为 24 在 15 分钟数据下表示用过去 6 小时的数据预测下一个点。

参数说明:n_splits=5表示做 5 次前向验证,数据量大时可以增加到 10。滑动窗口的seq_length是神经网络模型里最重要的超参数之一,太短抓不住日周期规律,太长训练速度慢且可能过拟合。光伏场景下我一般从 24 起步试,逐渐加到 96。

3. 模型选型对比:为什么梯度提升树是首选,LSTM 适合什么场景

3.1 梯度提升树(LightGBM / XGBoost)为什么是默认选项

光伏功率预测在大多数比赛中被证明梯度提升树的效果强于深度学习——这不是因为深度学习不行,而是因为表格型数据 + 手工特征工程的组合在中小规模数据集上很难被序列模型超越。梯度提升树的优势有三点:对特征尺度不敏感,不用归一化;能自动处理特征交互,比如辐照度和时刻的联合效应(同一辐照度在早晨和下午对应不同功率);训练速度快,调参空间相对可控。LightGBM 和 XGBoost 两者选哪个?我一般选 LightGBM,因为直方图算法在大数据集上更快,内存占用更小,而且对光伏这种带明显周期性的数据,max_depth控制得当的情况下不容易过拟合。

LightGBM 训练的核心代码可以这样写:

import lightgbm as lgb from sklearn.metrics import mean_absolute_error, mean_squared_error # 数据集构建 train_data = lgb.Dataset(X_train, label=y_train) val_data = lgb.Dataset(X_val, label=y_val, reference=train_data) # 参数配置 params = { 'objective': 'regression', 'metric': 'mae', 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': 7, 'min_child_samples': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'verbosity': -1 } # 训练模型 model = lgb.train( params, train_data, num_boost_round=500, valid_sets=[val_data], callbacks=[lgb.early_stopping(stopping_rounds=50)] ) # 预测与评估 y_pred = model.predict(X_test, num_iteration=model.best_iteration) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print(f'MAE: {mae:.2f} kW, RMSE: {rmse:.2f} kW')

逻辑说明:lgb.Dataset是 LightGBM 的原生数据格式,构建时传入特征矩阵和标签向量。early_stopping的作用是在验证集指标连续 50 轮不提升时停止训练,防止过拟合,同时能自动确定最优迭代轮数。feature_fraction和bagging_fraction是两套随机采样参数,前者每次建树随机选 80% 的特征,后者每次训练随机选 80% 的数据,作用是增强模型的鲁棒性。

参数说明:num_leaves=31是 LightGBM 的核心复杂度控制参数,值越大模型越复杂,31 对应深度约为 5 的叶子数,在光伏数据上表现稳定。learning_rate=0.05和num_boost_round=500是一对搭配,学习率越低需要的迭代次数越多,如果训练时间紧张可以把学习率调到 0.1 并减少迭代轮数。max_depth=7是额外限制树深度的保险丝,防止叶子数过大时过拟合。min_child_samples=20要求每个叶子节点至少有 20 个样本,数值太小时模型会学到局部噪声。

3.2 LSTM 的正确打开方式:适合短序列预测和在线更新场景

LSTM 的真正优势不是精度,而是滚动预测的灵活性。当你需要每 15 分钟滚动预测未来 4 小时,并且希望模型能利用最新时刻的功率做状态更新时,LSTM 的序列输入天然适合这种场景——因为你可以把最近 6 小时的数据拼成一个序列直接喂进去,不需要手动构造滞后特征。但 LSTM 的训练成本高、对数据量要求大、调参更敏感,数据量少于 2 万条时不建议用。

用 LSTM 做光伏功率预测的最小实现:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping import numpy as np # X_seq shape: (样本数, 时间步长, 特征数) # 这里特征数=5,对应:功率、辐照度、温度、风速、小时正弦编码 model = Sequential([ LSTM(64, return_sequences=True, input_shape=(X_seq.shape[1], X_seq.shape[2])), Dropout(0.2), LSTM(32, return_sequences=False), Dropout(0.2), Dense(16, activation='relu'), Dense(1) ]) model.compile(optimizer='adam', loss='mae', metrics=['mae']) # 早停:监控验证集损失,连续20轮不下降则停止 early_stop = EarlyStopping(monitor='val_loss', patience=20, restore_best_weights=True) history = model.fit( X_seq_train, y_train, validation_data=(X_seq_val, y_val), epochs=100, batch_size=64, callbacks=[early_stop], verbose=0 ) y_pred = model.predict(X_seq_test)

逻辑说明:return_sequences=True让第一个 LSTM 层输出完整的序列给第二层,第二层return_sequences=False只输出最后一个时间步的隐藏状态,再接全连接层输出预测值。Dropout(0.2)在 LSTM 层之间随机丢弃 20% 的神经元输出,是防止循环网络过拟合最有效的正则手段。EarlyStopping的restore_best_weights=True会在训练停止后把权重回滚到验证集表现最好的那一轮,避免了「训练到最后一轮反而过拟合」的问题。

参数说明:LSTM(64)的 64 是隐藏单元数,太大容易过拟合且训练慢,太小表达能力不足。光伏数据我习惯从 64 开始,数据量大时加到 128。batch_size=64是批大小,影响训练速度和梯度稳定性,显存小就减半。patience=20是早停耐心轮数,光伏数据噪声较大,耐心太小容易在损失值正常波动时提前停止。

3.3 模型对比实验怎么做:固定评估指标,控制变量

「高分代码」项目里通常包含多个模型的对比结果,一般会展示随机森林、XGBoost、LightGBM、LSTM 四者的误差指标。复现这个对比不是说把所有模型跑一遍就完事了,而是要固定同样的特征、同样的数据切分、同样的评估指标,否则对比没有意义。我建议至少跑以下三个模型:线性回归(作为基线,判断 ML 模型到底比简单规则强多少)、LightGBM(主模型)、LSTM(序列模型对照组)。

评估指标选择方面,光伏功率预测最常用的是 MAE、RMSE、R2 和归一化均方根误差(nRMSE,即 RMSE 除以装机容量)。MAE 反映平均偏差大小,RMSE 对大幅误差更敏感——这对光伏很重要,因为云团遮挡导致的大误差对电网调度的危害远大于均匀的小误差。如果你要跟其他电站对比效果,必须使用 nRMSE 归一化,否则不同装机容量的电站之间毫无可比性。

4. 训练与评估流程:跑通最小样例,再谈精度提升

4.1 完整训练脚本:从数据加载到结果存档

把前面几节的代码整合成一条完整流水线,这个脚本可以直接作为项目的主入口:

import pandas as pd import numpy as np import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import json # ---------- 1. 数据加载 ---------- df = pd.read_csv('solar_data.csv', parse_dates=['time'], index_col='time') # 按时间升序排列,确保切分顺序正确 df = df.sort_index() # ---------- 2. 特征工程 ---------- def build_features(df): df = df.copy() df['hour'] = df.index.hour df['month'] = df.index.month df['dayofyear'] = df.index.dayofyear # 对小时做正弦编码,保留周期性 df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) # 滞后特征:前一天同时刻 df['lag_24h'] = df['power_kw'].shift(96) # 滚动统计 df['rolling_mean_1h'] = df['power_kw'].rolling(window=4, min_periods=1).mean() df['rolling_std_1h'] = df['power_kw'].rolling(window=4, min_periods=1).std() return df.dropna() df_feat = build_features(df) # ---------- 3. 特征与标签划分 ---------- feature_cols = ['hour_sin', 'hour_cos', 'month', 'dayofyear', 'lag_24h', 'rolling_mean_1h', 'rolling_std_1h', 'ghi', 'temp', 'wind_speed'] X = df_feat[feature_cols] y = df_feat['power_kw'] # ---------- 4. 时间序列交叉验证 ---------- split_idx = int(len(X) * 0.8) X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] # ---------- 5. 训练 LightGBM ---------- params = { 'objective': 'regression', 'metric': 'mae', 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': 7, 'min_child_samples': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'verbosity': -1 } model = lgb.train( params, lgb.Dataset(X_train, label=y_train), num_boost_round=1000, valid_sets=[lgb.Dataset(X_test, label=y_test)], callbacks=[lgb.early_stopping(stopping_rounds=50)] ) # ---------- 6. 评估与存档 ---------- y_pred = model.predict(X_test, num_iteration=model.best_iteration) metrics = { '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(f'MAE: {metrics["mae"]:.2f} kW') print(f'RMSE: {metrics["rmse"]:.2f} kW') print(f'R2: {metrics["r2"]:.4f}') # 保存模型和结果 model.save_model('lgb_model.txt') with open('metrics.json', 'w') as f: json.dump(metrics, f, indent=2)

逻辑说明:hour_sin和hour_cos是成对构造的周期编码,单独用hour=0到hour=23这个整数会让模型误以为 23 点和 0 点距离很远,正弦余弦编码则能让首尾相接。rolling_std_1h是滚动的标准差特征,它捕捉的是过去一小时内出力的波动程度,云团快速移动时这个值会明显升高,模型可以利用它来预判未来短时间的波动性。

参数说明:feature_cols是可配置的,不同项目的数据列名可能不同,实际使用时要按真实的 CSV 列名来替换。split_idx = int(len(X) * 0.8)是按原始时间顺序切分,如果数据本身已经是乱序的,要先 sort_index。num_boost_round=1000配合早停可以保证训练充分,实际迭代次数由best_iteration决定。

4.2 误差分析与结果可视化:光看指标不够,要看曲线

模型评估不能只看三个指标。光伏功率预测的特殊之处在于误差分布极不均匀——晴天误差小、阴天误差大、日出日落时段误差中等。如果只看 MAE 可能会被平均效果掩盖局部时段的糟糕表现。正确的做法是画出三条曲线:真实功率曲线、预测功率曲线、误差绝对值曲线。不管哪个项目,把这三条线画在一张图上,比任何指标都更能说明问题。

import matplotlib.pyplot as plt # 取测试集前3天的数据进行可视化 plot_steps = 96 * 3 # 3天,15分钟分辨率 plt.figure(figsize=(14, 5)) plt.plot(y_test.index[:plot_steps], y_test.values[:plot_steps], label='Actual', color='black', linewidth=2) plt.plot(y_test.index[:plot_steps], y_pred[:plot_steps], label='Predicted', color='tab:red', linewidth=1, alpha=0.8) error = np.abs(y_test.values[:plot_steps] - y_pred[:plot_steps]) plt.bar(y_test.index[:plot_steps], error, label='Abs Error', color='tab:blue', alpha=0.3, width=0.01) plt.title('PV Power Prediction: Actual vs Predicted (3 Days)') plt.xlabel('Time') plt.ylabel('Power (kW)') plt.legend() plt.tight_layout() plt.savefig('prediction_result.png', dpi=150)

逻辑说明:plt.bar是柱状图,用来显示每个时间点的绝对误差,半透明的柱体和两条功率曲线叠在一起,能直观看出误差集中在哪些时段——如果柱子密集出现在午后的某个区间,就说明模型对那个时段(如下午辐照度高但温度也高导致组件效率下降)的系统性偏差没有学到。

参数说明:plot_steps=96*3控制可视化的长度,15 分钟分辨率下 96 点是一整天,3 天足够观察日周期模式。width=0.01在时间索引跨度大时让柱子变细,避免柱体过度重叠盖住曲线。

观察误差时要问自己三个问题。第一,误差是否集中在云团过境时段(功率剧烈波动的区间)?如果是,考虑增加分钟级辐照度变化率特征。第二,是否有明显的「早晚系统性偏低或偏高」趋势?可能是特征里缺少温度对组件效率的影响。第三,连续阴雨天误差是否比晴天大很多?如果是,需要检查数值天气预报的云量特征是否进了模型。

4.3 随机森林、XGBoost 的对照组怎么快速搭

为了证明你最终选的模型是「认真比较过的」而不是「拍脑袋选的」,项目里通常需要几个对照组的指标。用一个循环把所有树模型跑一遍是最省事的做法:

from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor results = {} # 随机森林 rf = RandomForestRegressor( n_estimators=200, max_depth=10, min_samples_leaf=5, random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) y_pred_rf = rf.predict(X_test) results['RandomForest'] = { 'mae': mean_absolute_error(y_test, y_pred_rf), 'rmse': np.sqrt(mean_squared_error(y_test, y_pred_rf)) } # XGBoost xgb_model = XGBRegressor( n_estimators=300, learning_rate=0.05, max_depth=7, subsample=0.8, colsample_bytree=0.8, random_state=42, n_jobs=-1 ) xgb_model.fit(X_train, y_train, verbose=False) y_pred_xgb = xgb_model.predict(X_test) results['XGBoost'] = { 'mae': mean_absolute_error(y_test, y_pred_xgb), 'rmse': np.sqrt(mean_squared_error(y_test, y_pred_xgb)) } # 打印对比表 for name, metric in results.items(): print(f"{name:12s} MAE={metric['mae']:.2f} kW RMSE={metric['rmse']:.2f} kW")

逻辑说明:这一节展示了标准化操作的写法——每个模型用同样的训练集测试集、同样的评估函数,这样出来的对比才有说服力。n_jobs=-1表示使用所有 CPU 核心并行训练,随机森林和 XGBoost 都能吃满多核。

需要补一句经验值:如果 LightGBM 的 MAE 在 30 kW 左右,随机森林一般在 35-40 kW,XGBoost 在 30-35 kW 之间。如果你的结果差距远大于这个范围,先检查特征工程是否一致,而不是急着调参。

5. 光伏功率预测的常见问题与避坑指南

5.1 夜间零功率段污染训练集,模型被「带偏」

现象:模型预测结果在清晨和傍晚时段出现负值,或者白天的预测曲线整体被压低。查看训练数据分布时会发现零值样本占了将近一半。

原因:光伏电站夜间不发电,功率记录是 0,但辐照度很可能也接近 0,这些样本的特征分布是「低辐照度、零功率」,占样本总量 50% 左右。如果直接训练,模型会发现「大多数时候输出 0 的损失很小」,从而倾向于输出一个偏保守的预测值,白天高功率段的权重被稀释。

解决:训练时过滤掉夜间时段的数据,或者至少过滤掉辐照度低于阈值的样本。常见做法是只保留小时在 5 点到 20 点之间的数据,另一个做法是给零功率段样本降权。但注意:预测时仍然要处理夜间边界——把模型输出小于 0 的预测值直接 clip 到 0,因为负功率在物理上没有意义。

# 方案1:按时段硬过滤 df = df[df.index.hour.between(5, 20)] # 方案2:按辐照度阈值过滤(更通用,不受季节日照时长影响) df = df[df['ghi'] > 10] # 辐照度低于10 W/m² 视为无效 # 预测后处理:负值归零 y_pred = model.predict(X_test) y_pred[y_pred < 0] = 0

注意上面这几种操作是互补的而不是互斥的,我用的时候通常先做辐照度阈值过滤,再在预测端做 clip,两道保险。

5.2 数据时间戳对齐问题:气象数据和功率数据差了一个小时

现象:测试集上模型的 R2 在 0.95 以上,但换成新数据后效果崩塌,误差翻倍。查看时间序列时发现辐照度曲线的峰值比功率曲线峰值早了或晚了一段固定时间。

原因:光伏电站的 SCADA 系统和气象站的数据采集频率不同,气象数据可能是整点采集,功率数据是每 15 分钟采集,合并时索引没对齐就产生了系统性偏移。辐照度提前一小时的值被当作当前值输入,等于给模型喂了「未来数据」。

解决:合并数据时用pd.merge_asof做时间戳近似匹配,并做偏移检查。具体操作是把两条序列画在一张图上,看辐照度峰值与功率峰值是否在同一时刻,如果有固定偏移,把它作为 shift 参数校准回来。

# 检查辐照度和功率的峰值时间偏移 ghi_peak_time = df.loc[df['ghi'].idxmax()].time power_peak_time = df.loc[df['power_kw'].idxmax()].time print(f"辐照度峰值时间: {ghi_peak_time}") print(f"功率峰值时间: {power_peak_time}") # 如果发现功率曲线滞后于辐照度曲线,需要将辐照度shift到对齐 offset = int((power_peak_time - ghi_peak_time).total_seconds() / 900) # 按15分钟粒度计算 df['ghi_aligned'] = df['ghi'].shift(-offset) # 正数表示辐照度提前,需要向后平移

这一步做完之后务必重新训练模型验证效果,因为特征对齐方式的修正会改变所有特征的重要性排序,这是最容易被忽视的「隐性 bug」。

5.3 LSTM 训练结果不稳定,每次跑出来的指标都不同

现象:同一个训练脚本连续跑三次,三次的 MAE 波动超过 15%,有时候甚至出现训练不收敛、loss 变成 NaN 的情况。

原因:LSTM 的权重初始化是随机的,加上光伏数据的噪声较大,模型容易收敛到不同的局部最优。另外输入数据没有归一化时,lstm 内部的状态计算会因为数值范围过大导致梯度爆炸,输出 NaN。

解决:固定随机种子、规范化输入数据、降低学习率、增加早停耐心。如果专业一点,可以试试在 LSTM 层之前加 BatchNormalization,或者改用 GRU(参数更少、训练更稳定)。多跑几次取平均值也是行内做实验的常见习惯——本来深度模型就带随机性,单次结果不能说明问题。

import tensorflow as tf import random # 固定所有随机源 random.seed(42) np.random.seed(42) tf.random.set_seed(42)

把上面这段放在import tensorflow之后、模型构建之前,能在大多数情况下复现结果。但如果换了 CPU/GPU 环境,同样的种子也可能得到不同结果——这是平台的浮点运算差异。

5.4 预测曲线比真实曲线「平滑」太多,峰谷抓不住

现象:预测曲线在晴天正午时段明显低于真实值,在早晚时段又略高于真实值,整体看起来像「被压缩过」的版本。误差集中在功率快速爬升和下降的阶段。

原因:树模型本质上在做特征空间内的均值回归,对于极端值(如正午接近满发功率的样本),特征组合可能没有覆盖到足够多的训练样本,模型倾向于输出一个温和的中间值。这本质上是回归模型的「趋中」特性,不是 bug。

解决:第一,检查特征里是否缺少「前一天同时刻功率」这个最强特征;第二,尝试分位数损失函数,比如 LightGBM 的objective='quantile'配合alpha=0.5来预测中位数,或者用alpha=0.9预测上分位数作为「可能达到的最大出力」;第三,增加与云量相关的特征,让模型在高云量时敢于预测更低的输出、在低云量时敢于预测更高的输出。

5.5 雨天、阴天等非晴天的预测误差系统性偏大

现象:把误差按天气类型分组统计后发现,晴天的 MAE 只有 20 kW,阴雨天却高达 80 kW,模型在所有非晴天场景下严重低估或高估。

原因:训练数据中晴天的样本占比过高(大多数地区晴天数远多于阴雨天),模型学到了「晴天模式」,对阴雨天的低辐照度高波动场景没有足够的样本支撑。此外,部分项目的气象特征来自天气预报,其辐照度数据本身就是预测值,已经带了误差。

解决:做法通常是按天气类型分层采样或加重阴雨天的训练权重,但更实际的做法是把「天气类型」作为分桶条件训练多个模型——晴天模型和阴雨天模型分开,推断时先用一个分类器判断当前天气类型,再走对应的功率预测模型。这个「分类-回归」两阶段的思路在行内多个光伏预测项目里被验证有效,精度提升通常在 10% 以上。

6. 从「跑通」到「用起来」:三个能提升预测精度的进阶操作

第一个值得做的操作是引入数值天气预报变量的滞后差分。很多项目只把天气变量的原始值直接作为特征,但光伏功率对辐照度的「变化趋势」比对「绝对值」更敏感——云团接近时辐照度会连续下降,这个趋势信息比单纯「当前辐照度是多少」更有预测价值。构造时对 GHI 做差分:df['ghi_diff'] = df['ghi'].diff(1),再把差分值也放进特征列表。效果通常能带来 3% 到 5% 的误差下降,成本几乎为零(一行代码),我测试过的项目里没有例外。

第二个是分时段建模。既然早晚爬坡、正午满发、傍晚下坡三种状态的功率特性差异悬殊,让一个模型硬扛所有时段是不合理的。我习惯按小时划分成 4 个区间(如 5-9 点、9-14 点、14-18 点、18-21 点)分别训练模型,预测时根据预测目标时刻落在哪个区间走哪个模型。代价是训练 4 个模型,换来的是每个模型只需要学「一段曲线」,MAE 可以额外降 5% 到 8%。

第三个是预测结果的滚动修正。在超短期预测场景里,每次新的功率实测值出来后,用它和昨天的偏差来修正当前对未来几小时的预测。原理很简单:如果今天前 30 分钟的实际出力比模型预测偏高 10%,未来几小时大概率继续偏高。具体做法是计算当前实测功率与预测功率的比值作为修正系数,乘到后面的预测序列上:

# 假设 model 已经训练好,base_pred 是模型给出的未来4小时预测 # current_actual 是当前时刻的实测功率,current_pred 是当前时刻的模型预测 correction_factor = current_actual / current_pred corrected_forecast = base_pred * correction_factor

对晴天来说修正系数稳定在 1.0 附近,对拥有大面积云团的天气来说修正系数可能从 0.6 波动到 1.4,滚动修正能显著降低这种场景下的误差。

最后说个养成习惯:任何光伏功率预测模型,交付之前必须用「最近一周的实测数据」做一个模拟在线测试——只给模型一个时间点之前的数据,让它逐个预测后面的点,每一步把真实值补进去更新特征。这种「模拟在线」和「一次性切分测试」的结果差异往往超过 20%,如果你只跑了离线切分就匆忙上线,大概率要在真实场景里翻车。我早年在某市的光伏电站项目上吃过这个亏,离线指标好看得很,上线第二天连续阴雨天就被调度那边打来电话问「怎么偏了这么多」。从那以后,模拟在线验证成了固定动作。

希望这篇的内容能帮你把光伏功率预测这个方向走通,在数据、特征、模型、验证这几个环节上少踩几个坑。

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

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

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

立即咨询