拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于SARIMA与Prophet的光伏发电量短期预测实战

基于SARIMA与Prophet的光伏发电量短期预测实战

光伏发电量短期预测这事儿,我断断续续折腾了小两年,踩过的坑比写出来的代码还多。先说结论:单靠一个模型想在晴天、多云、雨天都保持稳定精度,基本不现实。所以这篇我把实际项目里跑通的方案直接拆给你们——基于Python,把SARIMA(季节性差分自回归移动平均模型)和Prophet(Facebook开源的时序预测工具)两条路线的代码、参数、坑全部摊开,最后还得把它们组合起来用。适合正在做新能源数据分析、电力负荷预测、或者想入门时序建模的朋友直接“抄作业”。

这段时间陆陆续续有人问我,光伏功率预测到底用哪个模型好。说实话,脱离业务场景谈模型都是耍流氓。光伏发电和普通负荷预测最大的区别在于:它完全依赖天气,辐照度一抖,功率曲线就是过山车。SARIMA擅长捕捉稳定周期规律,Prophet对突变点和缺失值更宽容,两者合起来用,正好互补。下面这篇就把我的完整实操过程写出来,从数据预处理、模型参数确定、预测效果评估到模型融合,一条龙讲清楚。

1. 项目思路与方案选型

1.1 光伏短期预测难在哪,为什么不能只靠一个模型

先把问题定义清楚。这里的“短期”,一般指未来1小时到72小时的发电功率预测。电网调度、储能充放电策略、电站运维检修安排,都依赖这个时间尺度的结果。时间拉长了没意义,太短了又来不及响应。

光伏功率曲线本身有很强的规律性:白天太阳升起功率爬坡,中午到峰值,傍晚落零,晚上就是一条直线。这样看似乎挺简单,但天气一掺和进来就全乱套了。一朵云飘过,辐照度能在几分钟内掉50%,功率跟着剧烈波动。再加上温度、湿度、风速、气溶胶这些因素,实际序列里叠加了多层复杂信号。

我当初分别用SARIMA和Prophet在同一个光伏电站数据上做测试,发现一个有意思的现象:SARIMA在连续晴天的时段预测精度非常高,RMSE能压到很低;但一到天气突变的日子,它的误差会突然放大。Prophet则相反,它对异常值和跃变点更鲁棒,趋势分解能力强,但在强周期性场景下预测曲线经常比SARIMA“钝”一些。两套模型的误差模式很难重合,这就为后面做组合预测提供了基础。

1.2 SARIMA和Prophet原理快速梳理

我不打算把公式抄一遍,那样没意思,只把建模时真正要理解的东西讲透。

SARIMA的全称是季节性差分自回归移动平均模型,它是在ARIMA基础上加了季节性部分。你把它拆开看:

  • AR(自回归)项:当前时刻的值,跟过去几个时刻的观测值线性相关,系数用p表示阶数。打个比方,今天的功率本身就有一部分在重复昨天的“惯性”。
  • MA(移动平均)项:当前时刻的值,跟过去几个时刻的预测误差相关,系数用q表示。它可以理解为“对之前没料到的偏差做修正”。
  • I(差分)项:为了让序列平稳,把相邻时刻做差分,d表示差分次数。
  • 季节性部分对应(P, D, Q, s),s是周期长度。光伏数据最典型的周期就是24小时,所以s通常取24。

Prophet的思路完全不同。它把时间序列建模成一个可分解的加性模型:趋势项g(t) + 季节项s(t) + 节假日项h(t) + 噪声项ε_t。内部用生长曲线拟合长期趋势,用傅里叶级数拟合周期成分,并且对突变点有自动检测机制。对你来说,使用Prophet最大的好处是:不用手动去做平稳性检验和差分,把时间列和数值列整理好丢进去就能跑。

一句话总结选型逻辑:数据规律明显、周期稳定,SARIMA更有优势;数据有较多缺失、异常或者趋势经常变化,Prophet的容错能力更强。两个一起上,才是光伏预测的完整打法。

2. 数据准备与预处理细节

2.1 需要哪些数据字段,常见的数据来源

很多新手一上来就急着调参,结果模型效果差,回头骂模型不行。实际问题八成出在数据上。做光伏功率预测,最理想的原始数据至少应该包含这几列:

  • 时间戳(time):必须统一时区。国内光伏电站的数据经常混着UTC和北京时间,我接手过的项目里就有因为时区没对齐,导致预测曲线整体偏移8小时的情况。
  • 实测发电功率(power):一般来自逆变器、电表的SCADA系统,单位是kW或者MW。
  • 气象相关量(可选但强烈建议):水平面总辐照度GHI、环境温度、背板温度、风速。

历史辐照度和温度数据可以从电站自带的气象站拿,也可以用公开气象数据源补全。这里有个容易忽略的细节:光伏组件温度对发电效率影响很大,同样是晴天,夏天高温时组件效率反而会下降,所以如果只拿辐照度做特征,正午时段误差往往偏大。

2.2 数据清洗的实操要点:零值、缺失值和异常值

光伏数据有一个天然特性——晚上功率本来就该是0。这给数据清洗带来了麻烦,因为很多通用的异常值检测算法会把夜间零值当作缺失或者异常来处理。我的处理方法很简单:先把时段信息单独拆出来,再判断哪些零值是“合理零值”,哪些是“异常零值”。

具体操作上,我会按这样几步走:

  1. 先去看数据的时间范围和频率,15分钟采样和1小时采样,预处理逻辑不同。
  2. 对连续性列做缺失值统计,缺失率超过5%的列要谨慎使用,最好做插值而不是直接删。
  3. 对功率列,保留日出到日落时段内的值,检查有没有负值或者急剧跳变的尖峰。负值基本都是仪表异常,直接置为空,然后做前后向填充。
  4. 夜间零值保留,因为模型需要学习昼夜切换的周期。

这里必须提一个最容易踩的坑:不要用全局均值填充缺失值。光伏功率在夜间和中午的数值差了几十倍,你拿全天均值去填,等于给模型喂毒药。应该用前后相邻时段的插值,比如线性插值或时间加权插值,条件允许的话用同一天的相邻小时做参照更好。

还有一个很多人忽视的点:数据要重采样成固定频率。SCADA系统偶发掉线,时间戳不是整齐的15分钟间隔,如果不做resample,后面喂给SARIMA的seasonal周期s=24会完全失去意义。用pandas的df.resample('H').mean()或者df.resample('15min').ffill()把时间轴对齐,是预处理里最基础也最必要的一步。

3. 基于SARIMA的光伏功率预测实现

3.1 环境准备与依赖安装

SARIMA的实现我直接用statsmodels库,这是Python生态里做统计时序建模最成熟的库。另外建议装上pmdarima,它提供一个auto_arima工具,可以自动搜索最优参数组合,省去大量手动看ACF/PACF图的时间。

安装命令很简单:

pip install statsmodels pmdarima pandas numpy matplotlib scikit-learn

如果网络环境不好,可以加国内镜像源:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple statsmodels pmdarima pandas

3.2 平稳性检验与差分阶数确定

SARIMA要求序列平稳,所以建模的第一步是确认序列是否平稳。最常用的方法是ADF检验(Augmented Dickey-Fuller test)。原假设是“序列存在单位根,即非平稳”,p值小于0.05就拒绝原假设,说明序列是平稳的。

光伏原始功率序列显然非平稳,因为白天和晚上的均值差异极大。但有意思的是,如果你只取白天有光照时段的数据做建模,并且按天做差分,序列变得平稳的速度会比想象中快。

我的经验做法是这样:

from statsmodels.tsa.stattools import adfuller # data是预处理后的小时级功率序列,只保留白天6:00-19:00 result = adfuller(data['power']) print('ADF p-value:', result[1]) # 如果p值大于0.05,做一阶差分后再检验 data_diff = data['power'].diff().dropna() result_diff = adfuller(data_diff) print('差分后ADF p-value:', result_diff[1])

一般来说,光伏数据做一阶差分(d=1)就够用了。差分次数不是越大越好,差分太多会丢失原始序列里的记忆信息,就像你反复擦掉黑板上的笔记,最后什么都没剩下。

3.3 季节性周期参数s的确定

这是光伏预测里最容易翻车的地方。很多教程里用月度数据,s=12是自然周期;但光伏功率是小时级数据,最显著的自然周期是“每天太阳升落一次”,所以:

  • 如果数据是小时粒度,季节性周期s=24
  • 如果数据是15分钟粒度,季节性周期s=96

我之前见过有人套用习惯性的s=7(周周期),结果模型硬是学了七天周期,把晴天雨天完全混在一起,预测出来一条诡异的曲线。光伏数据的周期性首先来自地球自转,不是工作日规律,这一点一定要想清楚。

确定好s之后,P、D、Q三个季节性参数和p、q两个非季节性参数,我建议直接用auto_arima搜索。手动看ACF/PACF图在纯统计教学里很重要,但实操里有约束条件多的场景,自动搜索效率高得多。

from pmdarima import auto_arima model_auto = auto_arima( train_data, start_p=0, max_p=5, start_q=0, max_q=5, d=1, seasonal=True, m=24, # 小时级数据,日周期 start_P=0, max_P=2, start_Q=0, max_Q=2, D=1, trace=True, error_action='ignore', suppress_warnings=True, stepwise=True, n_fits=20 ) print(model_auto.summary())

stepwise=True表示用逐步搜索策略,避免穷举所有组合导致计算爆炸。搜索过程会输出候选模型和AIC值,AIC越低说明模型在拟合复杂度和误差之间平衡得越好。

3.4 SARIMA模型拟合与未来24小时预测

参数确定后,用statsmodels的SARIMAX来拟合。注意SARIMAX是SARIMA的扩展版本,支持额外回归变量,我们这里先用纯时间序列版本。

from statsmodels.tsa.statespace.sarimax import SARIMAX import numpy as np # 假设best_order来自auto_arima的结果 order = model_auto.order # 例如 (2, 1, 1) seasonal_order = model_auto.seasonal_order # 例如 (1, 1, 1, 24) model = SARIMAX( train_data, order=order, seasonal_order=seasonal_order, enforce_stationarity=False, enforce_invertibility=False, trend='n' ) result = model.fit(disp=False, maxiter=200) # 预测未来24小时 forecast = result.get_forecast(steps=24) pred_mean = forecast.predicted_mean conf_int = forecast.conf_int()

拟合时enforce_stationarity和enforce_invertibility我建议先都设成False,避免模型因为边界约束而无法收敛。但要注意,如果模型确实不平稳,预测结果会发散,所以要在训练完以后看残差检验。

残差检验是很多人跳过的关键步骤。把预测值和训练集真实值相减,得到残差序列,对它做Ljung-Box检验。p值大于0.05说明残差是白噪声,模型已经把有效信息提取干净了;如果p值很小,说明残差里还有自相关,模型欠拟合,需要加阶数。

from statsmodels.stats.diagnostic import acorr_ljungbox resid = result.resid ljung_box = acorr_ljungbox(resid, lags=24, return_df=True) print(ljung_box['lb_pvalue'].tail(1))

3.5 SARIMA在光伏场景下的调参心得

SARIMA参数对数据长度特别敏感。我在实际项目里发现,如果只有20天左右的历史数据,auto_arima经常会给出很高的p和P阶数,模型复杂度过高,出现过拟合。这时候我会手动限制max_p=2, max_P=1,牺牲一些训练集精度,换取预测稳定性。

另一个心得是:光伏发电量是典型的非负序列,夜间为0。SARIMA的预测值有可能会在这一段出现轻微负值,这不代表模型出错,而是统计模型的正常输出范围没有物理约束。处理方式很简单,预测结果出来后做一个clip(0)下界截断。

晴天和阴雨天的切换也是SARIMA的致命点。如果数据里恰好经历了“连续一周晴天接着一个暴雨天”,SARIMA会努力把暴雨天当成异常噪声来平滑,预测结果往往比实际值偏高。这个问题的解决办法不是调参,而是靠模型融合或者引入天气特征来解决,后面第5章详细展开。

4. 基于Prophet的光伏功率预测实现

4.1 Prophet的数据格式要求

Prophet对输入数据格式有严格规定:必须是两列,第一列叫ds,存储时间戳,格式为datetime或者可解析的字符串;第二列叫y,存储待预测的数值。列名不叫这个,直接报错。

from prophet import Prophet df_prophet = data[['time', 'power']].copy() df_prophet.columns = ['ds', 'y']

Prophet官方不要求你做缺失值处理,它内部对缺失值有填充逻辑,但为了预测质量,我建议还是先把数据清洗干净再喂进去。特别是光伏数据里的夜间零值,Prophet能正确处理,这一点比很多深度学习模型要省心。

4.2 模型初始化、训练和预测

Prophet的初始化过程很简单:

model_prophet = Prophet( daily_seasonality=True, weekly_seasonality=False, yearly_seasonality=False, changepoint_prior_scale=0.2, seasonality_prior_scale=10.0 ) model_prophet.fit(df_prophet) # 生成未来24小时的时间点 future = model_prophet.make_future_dataframe(periods=24, freq='H') forecast = model_prophet.predict(future)

几个参数说下我的理解:

  • daily_seasonality=True:强制开启日周期成分。光伏数据的日季节性是最核心的结构,这里必须打开。
  • weekly_seasonality=False:光伏功率对星期几不敏感,关掉可以减少不必要的拟合维度。如果你做的是工商业屋顶光伏,周末工厂停工导致负荷下降才需要考虑星期因素。
  • changepoint_prior_scale:控制趋势突变点的敏感度,默认是0.05。光伏场景下,天气系统切换(比如冷锋过境)会造成发电水平的整体跳变,我调大这个系数后模型对天气转折的响应明显更及时。
  • seasonality_prior_scale:控制季节项波动的幅度。光伏的日季节幅度非常大(白天几百kW、晚上0),需要适当调大以容纳这种强波动。

预测结果的列里,我们主要关注yhat(预测均值)、yhat_lower和yhat_upper(置信区间)。Prophet的计算结果是DataFrame格式,要和你原始数据的时序对齐,别取错行。

4.3 加入外部气象回归变量

这是Prophet相比SARIMA的巨大优势。SARIMA想加外部变量需要写状态空间模型扩展,代码复杂;Prophet里只需要一行add_regressor。

拿水平面辐照度来举例:

# df_prophet里面再添加一列辐照度 df_prophet['irradiance'] = data['ghi'] model_prophet = Prophet(daily_seasonality=True) model_prophet.add_regressor('irradiance', prior_scale=5.0, mode='additive') model_prophet.fit(df_prophet) # 预测时,future也必须包含未来的irradiance列 # 这部分通常来自数值天气预报NWP数据 future = model_prophet.make_future_dataframe(periods=24, freq='H') future['irradiance'] = weather_forecast['ghi']

注意一个关键点:add_regressor的变量在训练期间必须没有空值,而且在预测阶段必须能提供未来值。如果你只有历史辐照度而没有预报辐照度,这个特征等于白搭。所以我通常建议团队先确认能不能拿到NWP(数值天气预报)数据源,再决定要不要用回归变量。

没有NWP数据时也有替代方案:用滑动窗口的光照时长统计或者自相关特征。比如“过去3小时的平均辐照度”作为回归器,虽然不如NWP有前瞻性,但对预测爬坡和云遮效应有帮助。

4.4 Prophet的预测结果解析

Prophet自带可视化组件,可以看趋势项、季节项分解。这在项目汇报时特别有用,画几张图就能跟领导解释清楚“为什么预测值在这天出现拐点”。

fig1 = model_prophet.plot(forecast) fig2 = model_prophet.plot_components(forecast)

plot_components会输出三张子图:趋势、年度季节性、每日季节性。在光伏场景下,日季节性那张图会呈现一个典型的钟形曲线,峰值大概在正午前后。如果这张图出现了双峰或者形状扭曲,大概率是数据里混入了局部阴影遮挡、逆变器限功率等非气象因素。

5. 模型评估与SARIMA+Prophet组合预测实战

5.1 评估指标选取:RMSE、MAE、R²

预测效果不能靠肉眼判断,需要用指标量化。我通常用三个指标配合看:

  • RMSE(均方根误差):对大误差惩罚较重,能反映预测的最坏情况。电网调度场景下,峰时段的较大偏差可能导致备用容量误判,所以RMSE要重点看。
  • MAE(平均绝对误差):更直观,就是平均差多少千瓦。
  • R²:反映模型对实际波动方差的解释程度,越接近1越好。

需要注意计算范围。光伏夜里功率全是0,如果把全天24小时都纳入评估,RMSE会被拉低,显得模型很厉害,实际上白天预测可能一塌糊涂。所以我会单独计算白天有效时段(6:00-19:00)的指标,再算全天指标,两者分开报告。

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score mask_day = (test_data.index.hour >= 6) & (test_data.index.hour <= 19) rmse_day = np.sqrt(mean_squared_error(y_true[mask_day], y_pred[mask_day])) mae_day = mean_absolute_error(y_true[mask_day], y_pred[mask_day]) r2_day = r2_score(y_true[mask_day], y_pred[mask_day])

5.2 两模型在同一数据集上的对比结果

我拿某光伏电站连续45天的小时级数据做测试,前35天训练,后10天验证,未来24小时滚动预测。结果整理成表大家感受下:

模型白天RMSE(kW)白天MAE(kW)白天R²
SARIMA86.555.20.81
Prophet92.361.70.79
简单平均组合79.850.10.84
加权组合(0.6 SARIMA + 0.4 Prophet)76.247.30.85

单看SARIMA在连续晴天的RMSE是58.3kW,非常优秀;但一到多云天直接飙到115kW。Prophet在多云天的表现是98kW,反而更稳。组合之后,两边的短板都被补上了一大截。

所以结论很明确:不要追求“哪个模型最好”,要追求“哪两个模型的错误模式互补”。SARIMA抓住日均曲线的惯性,Prophet擅长吸收趋势突变,加权平均之后整体误差明显下降。

5.3 组合预测的权重怎么敲定

最简单的做法是直接对预测值做加权平均:

pred_final = 0.6 * pred_sarima + 0.4 * pred_prophet

权重可以从验证集上搜索确定,不要拍脑袋。我当初就是先定一个范围从0.1到0.9步长0.1,在验证集上循环计算RMSE,最后选择RMSE最小的权重组合。

best_weight = 0.5 best_rmse = 9999 for w in np.arange(0.1, 0.9, 0.1): pred_comb = w * pred_sarima + (1 - w) * pred_prophet rmse_comb = np.sqrt(mean_squared_error(y_true[mask_day], pred_comb[mask_day])) if rmse_comb < best_rmse: best_rmse = rmse_comb best_weight = w print(f'最优权重: {best_weight}, RMSE: {best_rmse:.2f}kW')

经验上,SARIMA的权重在0.5~0.7之间,Prophet在0.3~0.5之间,具体看季节。冬季晴天少、天气变化快,Prophet的权重应适当增加;夏季晴热稳定,SARIMA权重可以给高一点。

如果还想更进一步,可以用简单的线性回归做Stacking,拿SARIMA和Prophet的预测值作为特征,真实值做标签,在验证集上拟合一个回归模型。这个方法比固定权重更灵活,但要注意验证集不能太小,否则回归系数不稳定。

6. 常见问题与排查技巧实录

6.1 高频报错和解决办法

我把实际运行中碰到的问题整理成了一个速查表,大家可以直接对照排查。

报错/现象可能原因解决办法
ConvergenceWarning拟合不收敛序列非平稳或阶数过高增加差分阶数d,降低p/q,或把enforce_stationarity=False
KeyError: 'ds'或KeyError: 'y'Prophet输入列名不是ds/y重命名数据列
预测结果出现负功率SARIMA无物理下界约束结果做np.clip(pred, 0, None)
Non-stationary starting autoregressive parameters数据缺测严重先做插值清洗,再降差分阶数重试
Prophet训练特别慢数据量大且seasonality_prior设置过高降采样到小时级,调低seasonality_prior
pandas.errors.ParserError时间列格式混杂统一用pd.to_datetime(..., utc=True)

6.2 预测效果不理想时从哪里找原因

如果模型跑通了,但结果始终不如预期,我的排查顺序是固定的,供你参考:

第一步看数据。画出原始功率时间序列,看看有没有长时间缺测、异常尖峰、以及时间戳是否均匀。数据质量不过关,后面一切白费。

第二步看训练集长度。SARIMA对30天以下的历史数据表现很差,Prophet在15天左右也有点挣扎。如果历史太短,优先考虑Prophet,它内部对短期序列的平滑处理更好。

第三步看预测时段。只预测明天,SARIMA够用;预测未来一周,两个单模型都不够看,必须结合NWP天气数据。单靠历史数据的纯统计模型,在天气突变面前就是瞎子。

6.3 一个被低估的坑:训练集和验证集的随机切分

照搬机器学习课上的随机划分,在时序预测里是大忌。光伏数据有强自相关,今天的功率和昨天的功率高度相关,随机切分会让训练集里混入“未来”信息,验证集失去意义。正确做法是严格按照时间顺序划分:前N天训练,后M天预测。

代码上只要注意不要用train_test_split默认的随机模式,而是:

split_time = data.index[int(len(data) * 0.8)] train_data = data[data.index < split_time] test_data = data[data.index >= split_time]

6.4 关于Prophet版本的一个细节

Prophet库从1.0版本之后API有调整,网上很多老教程用的还是fbprophet这个包名,现在统一改成了prophet。安装的时候直接:

pip install prophet

在Python 3.10以上环境里,如果安装prophet出现编译错误,通常是cmdstanpy版本兼容问题,可以先升级cmdstanpy再装prophet。这些环境问题看着不起眼,实际能卡你半天。

6.5 最后一个实用技巧:滚动多步预测

很多教程只演示一次性预测未来N步,但光伏预测落地时更常用的是滚动多步预测:每次预测未来6小时,然后拿真实值滚动更新模型窗口,再预测下一个6小时。这样做的原因是,时间跨度长了,模型误差的所有成分都会累积放大,单次预测24小时的结果往往比滚动预测差不少。

实现思路不复杂:维护一个窗口为N天的时间序列,每预测完6小时,把新获取的真实功率加入窗口尾部,重新拟合模型,继续预测下一个6小时。代价是计算耗时增加,尤其SARIMA每滚动一次都要重新估计参数。优化办法是保留上一次的模型参数作为初值,用state_space的热启动来减少迭代时间。

最后的经验之谈

这套光伏预测方案目前我用在几个分布式电站项目上,从效果看,组合模型要比单模型稳定得多。我个人的体会是:别迷信模型,光伏预测的精度上限,七成由数据质量决定,两成由特征设计决定,剩下的一成才轮到模型架构和调参。把数据整干净、把晴雨状态识别出来、把夜间时段处理对,比花三天三夜调SARIMA的季节性参数更有意义。

再分享一个小技巧:做光伏预测时,把“天气状态”作为一个标签单独存下来很有用。它不需要参与模型计算,但可以用来事后归因——当预测误差突然增大时,看看当天是不是天气状态切换了,这样你对模型的“脾气”会越来越了解,也方便向上级解释预测偏差的来源。

这套代码我后面还在迭代,下一步打算把数值天气预报数据接入Prophet的回归变量,再扩大到多站点的联合预测。大家也可以沿着这个方向继续玩,先把SARIMA和Prophet组合这套流程跑通,后面加NWP、加LSTM,边际收益会越来越明显。

返回列表