光伏发量预测实战:从数据清洗到TensorFlow LSTM模型部署
2026/9/11 8:34:21 网站建设 项目流程

简介:这是一套基于TensorFlow实现光伏电站发电量预测的完整项目,面向机器学习初学者、能源数据分析人员及对时间序列预测感兴趣的开发者,解决历史电站生产数据与天气数据到未来出力预测的建模问题。资源包共14个文件,以10个CSV电站与天气数据集为主体,另外包含1个Python训练脚本、1个Jupyter Notebook建模流程文件及2个keep占位文件,整体压缩包仅513KB,小巧完整。已有233人学习浏览,适合快速上手实践。项目覆盖数据清洗、缺失值处理、归一化、特征编码等预处理方法,并示范了基于LSTM/RNN的时序模型构建、训练、评估与调优环节。借助附带的电站分站数据集与逐日天气数据,读者可直接运行代码复现预测流程,理解从原始数据到模型输出的完整链路,是深度学习应用于能源预测场景的良好范例。

1. 光伏电站发量预测项目,真实工程里难在哪里

光伏电站发量预测项目,真正难的不是搭模型,而是把电站生产数据、天气数据和TensorFlow代码接起来以后,系统还能稳定运行。功率序列有强自相关,天气突变时常发生,单站样本量又不像CV那样动辄几十万张;这些问题决定了落地路径:先处理两类原始数据,再用LSTM建序列模型,最后做滚动外推验证。这个方案适合需要给调度、检修提供“未来24小时发电预期”的工程师,也适合已入门深度学习的Python开发者拿真实光伏数据走一遍完整流程。下面按这套预测源代码的常见结构,从数据清洗、特征工程、TensorFlow建模讲到最终投运阶段的坑位,重在实际能复制到本地的细节。

2. 数据基础:电站生产数据与天气数据的清洗、对齐与特征工程

2.1 光伏预测的两类数据源,先统一时间频率再拼接

光伏电站发量预测项目里,数据一般由两部分拼接而成。第一部分来自电站SCADA或逆变器采集系统,常见字段有时间戳、实时功率(kW)、日累计发电量(kWh)、组件背板温度(℃),采样频率大多是1分钟或15分钟。第二部分来自气象站或气象服务接口,常用字段包括水平辐照度GHI、散射辐照度DHI、环境温度、湿度、风速、云量,气象数据通常是1小时一条。

两类数据频率不一样,是动手后遇到的第一个坑:不能直接把两个DataFrame按时间戳横向拼接,因为高频时间点上天气列会大量落空。我一般先把功率数据重采样到15分钟,天气数据再对齐到同样的15分钟网格。功率用均值重采样,把1分钟功率平均成15分钟值,符合调度口径;天气则用前向填充。之所以不用线性插值,是因为气象短时间变化往往是阶跃式的,插值会人为制造“云量逐渐变化”的假象,模型会把这个假象学进去,到了真实突变天气就预测偏。

import pandas as pd # 功率重采样到15分钟,天气数据对齐并用前向填充补齐全空位 power_15 = power_df.resample("15min").mean() weather_15 = weather_df.resample("15min").ffill() merged = power_15.join(weather_15, how="inner") merged = merged.dropna(subset=["power_kw", "ghi_w_m2"])

这里resample("15min").mean()会让1分钟功率聚合成15分钟平均功率,如果采集链路本身已经做了均值处理,那么读取后用asfreq更合适,避免二次平滑。ffill()保留气象数据“上一小时的值延续到下一小时”的确定性,代码后可以顺手看一眼合并后的索引是否连续,常见的datetime索引重复会直接导致join行数膨胀。

2.2 缺失值与异常功率样本的处理原则

电站实测数据里最常见的情况是:夜间功率为0、低辐照时功率抖动、停电或传感器故障导致连续缺失。处理原则是:夜间零值是正常的,不能当异常删;白天辐照接近零但功率突然跳变的点,大概率是计量故障。连续缺失不超过3个点,线性插值可以接受;超过3个点,我倾向于直接丢弃该段,因为长序列缺失会逼模型用上一段时间的边界值硬凑。

另一个必须做的操作是把功率除以额定装机容量,转换为标幺值pu。不同容量电站如果共用同一套代码,必须用pu而不是kW,否则模型会把“电站容量大小”当成特征学进去。

# 去除异常负功率,再归一化到额定容量 power_df["power_kw"] = power_df["power_kw"].clip(lower=0) power_df["power_pu"] = power_df["power_kw"] / nominal_capacity_kw power_df = power_df[power_df["power_pu"].between(0.0, 1.2)]

clip(lower=0)只处理负数,而不截断上限,是因为超过1.2pu的读数多为传感器故障,直接截断会让模型误以为“超发到1.2封顶”是正常行为;用筛选去掉这些样本,模型就只见过常态区间。

2.3 特征表与时间编码,避免未来信息泄漏

光伏功率与GHI之间接近线性,这是预测的核心抓手;但云层遮挡、组件温升会让关系曲线弯曲,所以模型需要同时看到历史功率、历史天气、未来时段的气象预报。特征构造的核心原则是:历史时点的特征用真实观测值,未来时点的气象特征用天气预报值,绝不能在训练时把未来真实气象值偷偷塞进输入。

# 加入一天内时刻和一年内日期的正余弦编码 merged["hour_sin"] = np.sin(2 * np.pi * merged.index.hour / 24) merged["hour_cos"] = np.cos(2 * np.pi * merged.index.hour / 24) merged["day_sin"] = np.sin(2 * np.pi * merged.index.dayofyear / 365) merged["day_cos"] = np.cos(2 * np.pi * merged.index.dayofyear / 365)

下表是项目里最常用的一套特征组合,按“训练用真实值/推理用预测值”做了区分:

特征组示例字段数据来源训练时取值推理时取值
历史出力power_pu电站SCADA真实值真实值
实测气象ghi_w_m2, temp_c, humidity, wind_speed气象站真实值观测值
预报气象ghi_fcst, cloud_fcst数值天气预报实测值代替预报值
时间编码hour_sin/hour_cos, day_sin/day_cos时间戳构造确定值确定值

表格最后一行的时间编码是纯确定值,可以放心加入。需要检查的是“预报气象”的字段,如果训练和推理两者分布差异大——比如预报GHI系统性强偏低——模型的白天峰值会被明显压低,这种情况用历史实测GHI训练得到的权重上线后还要做一次偏差校核。

3. TensorFlow模型搭建:光伏发量预测源代码里的LSTM骨架与滑窗数据

3.1 为什么光伏序列适合LSTM而不是普通回归或Transformer

线性回归很难刻画功率的时间连续性。光伏出力存在强自相关:上午10点5分的输出与10点几乎一致,却和昨天同一时刻也高度相关。LSTM的门控机制能保留多尺度时序信息,对云层快速过境后的功率骤降有一定迟滞感知能力。不选Transformer的理由更实际:单站光伏样本量通常在数万条量级,Transformer在这种数据规模下容易过拟合,在线推理时的显存占用也更大;LSTM的训练速度和稳定性更适合放到传统服务器上定时跑批。

3.2 用timeseries_dataset_from_array把生产数据切成Keras窗口

构造训练样本的核心是(X, y)对:X形状为(样本数, 回看窗口长度, 特征数),y形状为(样本数, 预测步数)。常规配置是回看过去24小时、预测未来24小时,15分钟一个点,两个长度都是96。窗口逐点滑动,样本量最大,模型能覆盖每天的起动、攀升、午间平顶、回落、归零各阶段;如果内存吃紧,也可以把滑动步长换成4,让样本稀疏,但会损失细节拟合。

import tensorflow as tf # 生成滑窗样本,一次给LSTM提供96个时间步 dataset = tf.keras.utils.timeseries_dataset_from_array( data=X_all, targets=y_all, sequence_length=96, sequence_stride=1, batch_size=256, shuffle=True, seed=42 )

sequence_length=96决定模型一次看到过去96个时点,一个时点是15分钟;sequence_stride=1让窗口逐点滑动,让连续样本之间有96个时间步的重叠;shuffle=True打乱的是样本而不是时间序列内部顺序,打乱后模型无法记住连续段落的顺序,反而能学到横跨不同季节的普遍规律。

3.3 从输入张量到预测输出的网络结构参数表

双LSTM接全连接输出是这类预测最常见且最稳的结构。第一层return_sequences=True保留完整时间维,第二层只输出最后一个时刻的隐状态;中间夹Dropout防过拟合,最后一层Dense直接映射到96个预测点。输出层不加激活函数,因为功率回归不是分类,不应限制在0到1之间。

inputs = tf.keras.Input(shape=(96, X_all.shape[-1])) x = tf.keras.layers.LSTM(128, return_sequences=True)(inputs) x = tf.keras.layers.Dropout(0.2)(x) x = tf.keras.layers.LSTM(64, return_sequences=False)(x) x = tf.keras.layers.Dropout(0.2)(x) outputs = tf.keras.layers.Dense(96)(x) model = tf.keras.Model(inputs, outputs) model.compile(optimizer="adam", loss="mse", metrics=["mae"])
层名输出形状说明
Input(None, 96, feat_count)96个历史时点,feat_count由特征数量决定
LSTM(128, return_sequences=True)(None, 96, 128)第一层保留完整时间步给第二层
Dropout(0.2)(None, 96, 128)训练时随机丢弃20%,降低过拟合
LSTM(64, return_sequences=False)(None, 64)只保留最后时刻,压缩时间信息
Dense(96)(None, 96)一次性输出未来96个点的功率预测

这里采用“直接多步预测”而不是“递归预测”的原因需要说明:递归是把上一步输出当作下一步输入,误差会沿着96步逐渐累积,日出阶段容易偏得离谱;直接多步输出一次性生成未来96点,误差分布更平坦,虽然需要更多训练数据捕捉输出的多样性,但光伏样本通常足够。

4. 训练管线与参数调优:归一化切分、回调设置和误差指标分析

4.1 先切分再归一化,顺序颠倒就是数据泄漏

训练前必须先切分,再归一化,这个顺序比很多参数都重要。如果先对全体数据计算均值和方差,测试集的分布信息就进入了训练过程,验证误差会偏低,真实首日预测掉精度。切分必须按时间顺序:前70%训练,中间15%验证,最后15%测试。光伏数据有季节结构,随机抽样会让模型看到“未来才出现的夏天”,等于考试时偷看答案。

from sklearn.preprocessing import StandardScaler, MinMaxScaler n_total = len(features) n_train = int(n_total * 0.7) n_val = int(n_total * 0.85) scaler_X = StandardScaler().fit(features[:n_train]) scaler_y = MinMaxScaler().fit(labels[:n_train]) X_scaled = scaler_X.transform(features) y_scaled = scaler_y.transform(labels)

特征X使用StandardScaler而不是MinMax,因为GHI和风速的长尾分布明显,零均值单位方差对偶发极端天气更稳;输出y使用MinMax映射到0~1附近,适配MSE损失。推理时必须把预测结果用inverse_transform还原成功率值再上报,漏掉这一步会得到一组永远小于1的“神秘数字”。

4.2 训练参数设置与EarlyStopping、ReduceLROnPlateau的使用

Adam初始学习率给1e-3,先跑10轮看Loss是否震荡,震荡就降到5e-4。同时挂EarlyStopping和ReduceLROnPlateau两个回调,一般能在20到40轮内收住。

early_stop = tf.keras.callbacks.EarlyStopping( monitor="val_mae", patience=12, restore_best_weights=True ) reduce_lr = tf.keras.callbacks.ReduceLROnPlateau( monitor="val_mae", factor=0.5, patience=3 ) history = model.fit( train_ds, validation_data=val_ds, epochs=100, callbacks=[early_stop, reduce_lr], verbose=2 )

patience=12表示验证集MAE连续12轮没有改善就停止训练;restore_best_weights=True会在停止时把权重回滚到最优那一轮,避免最后几轮过拟合权重被保存。ReduceLROnPlateaupatience=3是独立判断条件,验证指标连续3轮不降时学习率减半,让收敛末期可以用更小步长精细调整。

下面是这类项目里推荐的一组训练参数配置,直接照抄也能跑通:

超参数配置说明
optimizerAdam无需额外配置,自带自适应学习率
learning_rate1e-3起步,震荡降到3e-4光伏Loss曲面相对平滑,不建议用大于1e-2的值
batch_size256样本多时稳定,显存占用小
epochs100 + EarlyStopping防止训练时间浪费在过拟合区间
lossmse对峰值误差惩罚更大,比mae更贴合功率预测
metricsmae便于直观对比验证集误差

4.3 白天时段单独评估,MAE与NRMSE的计算方式

夜间出力全为0,如果算全时段RMSE,零值会把误差稀释掉,模型白天不敢出力的问题反而被淹没了。行业里通常看白天误差:以GHI大于50W/m²为分界,统计这一时段的平均绝对误差MAE,以及均方根误差除以该时段平均功率得到的NRMSE。MAE反映每一刻预测与实际的贴近程度,NRMSE反映整体曲线形状的拟合度。

day_mask = test_df["ghi_w_m2"].values > 50 y_true_day = y_true[day_mask] y_pred_day = y_pred[day_mask] mae_day = float(np.mean(np.abs(y_true_day - y_pred_day))) nrmse = float(np.sqrt(np.mean((y_true_day - y_pred_day) ** 2)) / (np.mean(y_true_day) + 1e-6)) print(f"白天MAE={mae_day:.4f} pu, NRMSE={nrmse:.3f}")

代码里分母加上1e-6是为了防止白天功率恰好为0时除零。我常看到的一种误用是把MAE直接除以额定容量,得出“预测准确率98%”的数字,这个数字忽略了夜间大量零样本,报到调度系统会严重高估能力。

5. 预测投运:滚动外推验证与日出时刻修正技巧

5.1 滚动窗口重预测,还原真实调度过程中的误差

模型在测试集上一次性预测所有96个点,并不能代表投运效果。真实系统每天每隔15分钟刷新一次未来24小时预测,所以我习惯用滚动窗口重预测来做仿真:起始位置取历史真实数据作为输入,预测未来96个点;随后把输入窗口向后滑动96步,用新的真实数据再算下一轮。这种评估方式与部署后的首次预测流程完全一致。

pred_collection = [] for start_idx in range(0, len(X_scaled_test) - 96, 96): x_input = X_scaled_test[start_idx:start_idx + 96] x_batch = np.expand_dims(x_input, axis=0) y_pred = scaler_y.inverse_transform( model.predict(x_batch, verbose=0) ).flatten() pred_collection.append(y_pred)

inverse_transform把模型的0~1输出还原回kW,再除以额定容量转成pu后画图对比,能直观看到连续十几天的误差累积情况。滚动评估得到的误差会明显高于单次预测,这是正常现象,不要因此怀疑模型写错;对比时建议把全时段MAE、白天MAE、NRMSE列成一张表来看,滚动过程的第7天、第30天通常因天气系统变化比头几天更差,这属于正常涟漪反应,不要为了数据平滑盲目加大正则项。

5.2 日出前后的低出力修正,十几行代码的启发式后处理

日出前后1~2小时是错误最集中的时段:模型往往延续前夜低功率输出,忘记此时辐照已经抬升。项目里的简单做法是,用ephempvlib算出预测日当地的理论日出时刻,当预测日排在日出后但模型输出接近0,同时GHI预报大于200W/m²时,按前一日同时段的功率曲线做线性抬升。

if pred_time < sunrise_minutes and predicted_pu < 0.02: if ghi_fcst > 200: correction = yesterday_curve[start_idx] * 0.6 predicted_pu = max(predicted_pu, correction)

这里的0.6是保守系数,目的是让模型在晴天早晨至少能动起来,而不是给出一个全天为0的预测。日出后一个小时,真实出力快速爬升,模型学到的历史趋势会接管输出,启发式修正的影响自然消失。这个技巧在阴天转晴的早晨效果非常明显,代码量不过十来行,值得直接收进预测服务的后处理管线。

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

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

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

立即咨询