ARTICLE DETAIL

资讯详情

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

电力负荷与价格预测:数据清洗、特征工程与建模实践

电力负荷与价格预测:数据清洗、特征工程与建模实践 简介一套基于MATLAB的短期电力负荷与价格预测系统面向电力系统分析、机器学习建模及能源数据研究者用于解决日前负荷与电价预测中的非线性回归问题。系统采用神经网络与袋装回归树两种模型结合温度预报、节假日信息及历史负荷数据进行训练并通过NEPOOL区域2004-2007年逐时数据校准在2008年样本外数据上平均误差约1-2%。资源包共73个文件压缩后约31.77MB其中14个m源文件覆盖数据导入、特征构造、模型训练与评估全流程16个xls文件提供系统负荷及区域小时级数据40个txt文件为数据说明与运行指引另有2个pdf文档辅助理解案例背景与算法原理。此外还提供可发布的报告生成及Excel前端用户可调用经MATLAB部署的DLL实现预测模型交互便于复现实验、验证算法效果并扩展至自有数据场景目前已有369人学习下载。1. 电力负荷和价格预测在解压之后要解决的两个问题把“电力负荷和价格预测”压进同一个压缩包很容易让人默认这是同一套模型的两路输出。但实际打开数据会发现这是两个脾气完全不同的时间序列负荷曲线平滑、周期强、受气温驱动明显电价则叠加了市场报价、机组启停和网络阻塞尖峰与负值并存噪声远大于负荷。这个项目的落地路径通常很直接拿到小时级或15分钟级的历史数据先清洗出连续可用的序列再做时间特征构造最后分别为负荷和电价建立预测模型。它适合刚接手能源数据、或想复现一套时序预测基线的工程师。下面按数据清洗、特征构造、建模、评估四步展开每一步给出可复现的命令、参数和筛选理由不绕弯。2. 电力负荷和价格预测的原始数据清洗与时间切分2.1 解包后先看什么时间粒度、缺失分布和脏值解包“电力负荷和价格预测.rar”之后常见布局是一个 CSV 或 Excel 里混着多列字段。第一件事不是建模而是检查时间字段是否为 datetime 类型、索引是否排序、缺失值集中在哪个时间段。很多项目失败不是因为模型差而是数据里藏着 2019 年 6 月的一整段坏数据没被看见。import pandas as pd import numpy as np df pd.read_csv(load_price.csv, parse_dates[timestamp], index_coltimestamp) df df.sort_index() df df[~df.index.duplicated(keepfirst)] # 去重保留第一条 print(df.info()) missing df.isna().sum() print(missing[missing 0]) # 检查时间步长是否连续freq 要和你数据的真实采样率对齐 expected pd.date_range(startdf.index.min(), enddf.index.max(), freq1h) print(缺失时间点数量:, len(expected.difference(df.index)))这段代码先用parse_dates把时间戳转成索引再排序和去重。现实中经常遇到两个问题一是同一时间戳出现多行二是夏令时切换导致某些国家时间跳变国内数据少有这个问题但如果是欧美公开数据集就得先统一到 UTC。expected.difference(df.index)这一步会列出所有缺失时间点能直观看到是散点缺失还是整段缺失整段缺失通常意味着数据源断供单纯插值补不回来需要标注出来让模型知道那段时间不可信。提示拿到数据后先按小时画一条负荷和价格的曲线。视觉检查能发现程序查不出的问题比如价格列里莫名其妙出现了 9999或者连续多天重复同一段曲线这种问题不要试图用算法自动修复。2.2 负荷和价格的清洗策略必须分开做负荷和电价不仅分布不同脏数据的形态也不同。负荷的坏点通常是通讯中断导致的零值或负值电价则天然允许负值出现所以“负值即异常”这个判断对价格完全不成立。负荷用线性插值或滚动均值修整价格则应该以异常值检测为主而非直接替换。# 负荷缺失值用时间插值限制连续插值长度避免一段坏数据被强行拉直 df[load] df[load].interpolate(methodtime, limit6) # 负荷基于滚动窗口的 z-score 修整窗口取 24 对齐日周期 roll_mean df[load].rolling(24, centerTrue).mean() roll_std df[load].rolling(24, centerTrue).std() z (df[load] - roll_mean) / (roll_std 1e-6) df[load] df[load].mask(z.abs() 5, roll_mean) # 价格只处理缺失值不做负值替换对极端尖峰做 winsorize 截断 price_cap df[price].quantile(0.999) df[price] df[price].clip(upperprice_cap) df[price] df[price].interpolate(methodlinear, limit12)插值时limit6限制最多连续补 6 个缺失点超过的部分保持 NaN训练时让模型学会面对缺失而不把一条坏线段当成真实曲线。z.abs() 5的阈值对应大约万分之一的误伤率如果负荷曲线含有大量工业用户波动会更大阈值可以放宽到 6。价格做quantile(0.999)截断而不是直接丢掉因为价格尖峰本身携带供需紧张的信息截断只是防止单个异常点把模型权重拉偏。字段异常形态处理方式关键参数负荷零值、负值、断崖滚动 z-score 检测均值替换窗口 24阈值 5负荷短段缺失时间插值limit 6价格缺失线性插值limit 12价格极端尖峰分位数截断0.999 分位2.3 训练/验证/测试必须按时间切分不能随机洗牌时间序列预测最容易被忽视的问题就是数据泄露。如果随机把 5 月份的样本放进训练集、6 月份的样本放进验证集模型会因为“看见过未来”而虚高。做负荷和价格预测正确的做法是用TimeSeriesSplit或手动按时间比例切分。from sklearn.model_selection import TimeSeriesSplit # 适用于样本量较大的场景n_splits 决定滚动验证的次数 tscv TimeSeriesSplit(n_splits4) for train_idx, val_idx in tscv.split(df): train df.iloc[train_idx] val df.iloc[val_idx] # 注意这里只能用 train 的统计量做标准化val 不能参与 fit print(ftrain: {train.index.min()} ~ {train.index.max()}, val: {val.index.min()} ~ {val.index.max()})n_splits4表示做 4 轮滚动验证每一步训练集都比上一步大验证集永远是未来的连续段。特征工程里一旦用到滞后值shift()会自然让前几个样本出现 NaN所以特征构造之后要再dropna()一次。数据标准化时只能fit在训练集上否则验证集的信息通过均值混进了训练过程这是一个隐蔽但代价高昂的 bug。3. 电力负荷和价格预测的特征工程时间结构比算法更重要3.1 日历特征小时、星期、节假日的编码方式负荷和电价都有极强的时间周期一天内早晚双峰一周内工作日和周末形态完全不同。日历特征是这类预测成本最低、收益最高的输入。小时和星期用 int 编码还是 one-hot 编码需要想清楚树模型可以直接吃 int但线性模型和神经网络最好做周期变换避免“23 点和 0 点距离远”这种错误假设。def add_calendar_features(df): out df.copy() out[hour] out.index.hour out[dow] out.index.dayofweek out[month] out.index.month out[is_weekend] (out[dow] 5).astype(int) # 周期编码把 hour 映射到圆上的 sin/cos避免 23 和 0 被切开 out[hour_sin] np.sin(2 * np.pi * out[hour] / 24) out[hour_cos] np.cos(2 * np.pi * out[hour] / 24) out[dow_sin] np.sin(2 * np.pi * out[dow] / 7) out[dow_cos] np.cos(2 * np.pi * out[dow] / 7) return outis_weekend只区分工作日和周末对很多地区过于粗糙。比如周一的负荷形态和周二到周四有明显差异周五晚高峰会提前这些信息可以让模型自己从dow里学到不必人工细分。sin/cos编码对 LightGBM 这类树模型帮助有限但如果你后面要接 LSTM 或 GRU周期编码几乎不可或缺。节假日建议单独维护一个日期列表标记为is_holiday因为节假日负荷曲线非常接近周末但又略有偏移模型仅靠dow区分不了。3.2 滞后特征和滑动窗口让模型“看见”昨天和上周同一时刻负荷和电价是强自相关序列上一时刻的值对下一时刻有极强的解释力。滞后特征是时间序列预测里信息密度最高的一类特征但滞后阶数的选择直接影响模型质量。经验做法是取 1、2、3短时惯性、24昨日同时刻、48前日同时刻和 168上周同时刻价格数据还可以再加一个 336两周前同时刻捕捉更长的市场周期。def add_lag_features(df, cols, lags): out df.copy() for col in cols: for lag in lags: out[f{col}_lag_{lag}] out[col].shift(lag) return out df add_lag_features(df, cols[load, price], lags[1, 2, 24, 48, 168]) # 滑动窗口统计量必须在 shift(1) 之后计算否则当前值泄漏进特征 df[load_mean_24] df[load].rolling(24).mean().shift(1) df[load_std_24] df[load].rolling(24).std().shift(1) df[price_max_24] df[price].rolling(24).max().shift(1) df[price_min_24] df[price].rolling(24).min().shift(1)滞后特征必须加上shift()否则预测当前时刻 t 的 y 时特征里包含了 y 本身的值。很多人第一次做时序特征就栽在这里训练时分数极高线上预测时特征取不到未来值导致模型完全不可用。滑动窗口同理rolling(24).mean()计算的是包含当前时刻在内的窗口均值要再shift(1)才能用来预测当前时刻。价格特征里加入rolling最大值和最小值是为了捕捉过去一天的价格波动范围这个范围对次日价格走势有指示作用。3.3 温度与衍生特征让模型理解“热了多久”温度是负荷预测最核心的外部变量但不是直接用原始温度最好。空调负荷有累积效应连续三天高温时人们会开更久的空调负荷曲线会持续抬升。所以除了当前温度还要构造 HDD供热度日数Heating Degree Days和 CDD制冷度日数Cooling Degree Days这类累积指标。def add_weather_features(df, temp_coltemp, base_cool24, base_heat18): out df.copy() out[cdd] (out[temp_col] - base_cool).clip(lower0) out[hdd] (base_heat - out[temp_col]).clip(lower0) # 过去 168 小时的累积制冷需求反映持续高温的蓄热效应 out[cdd_accum_168] out[cdd].rolling(168, min_periods24).sum() out[hdd_accum_168] out[hdd].rolling(168, min_periods24).sum() # 温度变化率快速升温和缓慢升温对负荷的影响不同 out[temp_diff_24] out[temp_col].diff(24) return outbase_cool24表示超过 24°C 才开启制冷需求这是电力行业常用的基准温度但具体数值可以根据所在地区的用电结构调整。min_periods24允许前 24 个小时用部分数据计算累积量避免起始段产生大量 NaN。格点气象数据一般能提供未来几天的温度预报在预测阶段把预报温度作为特征输入模型就能利用温度的不确定性给出更合理的负荷区间。3.4 负荷和价格的公有特征与独有特征负荷和价格两个预测任务可以共享大部分特征比如日历、温度和滞后特征但有一类特征只对价格预测有意义系统备用容量、燃煤价格指数、日前市场出清价格本身的历史波动率。这些数据在国内的公开渠道不一定能拿到拿不到时可以用价格的历史尖峰频率作为替代特征。特征类别具体特征用于负荷用于价格日历hour, dow, month, holiday是是滞后load_lag_1/24/168是是滞后price_lag_1/24/168否除非做联合建模是天气temp, cdd_accum, hdd_accum是弱相关市场price 尖峰次数、历史波动率否是4. 电力负荷和价格预测的建模树模型起步要不要上序列模型4.1 用 LightGBM 建立基线参数怎么选才合理特征工程做完之后先用 LightGBM 或 XGBoost 搭基线不要一上来就上 LSTM。树模型对特征尺度不敏感、能自动处理非线性交互、训练速度快是时序预测最稳妥的起手式。代码框架如下。import lightgbm as lgb params { objective: regression, metric: mae, learning_rate: 0.05, num_leaves: 63, max_depth: 8, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, min_data_in_leaf: 50, verbosity: -1, } train_cols [c for c in df.columns if c not in [load, price]] X_train, y_load_train train_df[train_cols], train_df[load] X_val, y_load_val val_df[train_cols], val_df[load] model lgb.LGBMRegressor(**params) model.fit( X_train, y_load_train, eval_set[(X_val, y_load_val)], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)], )metric: mae对负荷预测比 mse 更稳因为负荷偶尔会有单点尖峰mse 会被个别坏样本带着走。num_leaves63对应深度 6 左右的树复杂度样本量在几万级别时不容易过拟合如果数据量超过 50 万行可以提升到 127。min_data_in_leaf50防止模型在叶子节点上用过少的样本做预测能显著降低验证集噪声。Early stopping 的 patience 设为 100学习率设为 0.05 时树模型通常需要 1000 轮以上才收敛没必要为了省时间把 early stopping 调太紧。价格预测的 baseline 同样用这套代码只是把目标列换成price但评估指标要换后面第 5 章详细说。4.2 价格和负荷分开建模还是联合建模看到“电力负荷和价格预测”这个标题很多人会问能不能一个模型同时输出两个值。技术上完全可以多任务学习让两个任务共享底层特征理论上能互相正则化。但工程上我一般建议先分开建原因有三个第一负荷的量纲在几十到几百 MW价格通常在几十到几百元/MWh两个目标尺度不同联合训练需要精细的 loss 加权第二负荷的误差主要由天气和日历因素驱动价格的误差则大量来自市场行为出错环节完全不同分开建更容易定位第三两个任务的评估指标不同分开训练可以分别调参互不干扰。方案优点缺点适用场景完全分开可独立调参、错误隔离清晰两套模型维护成本高默认方案单模型多输出训练简单、上线推理快量纲差异需要加权、调参困难快速出结果的原型共享底层 双头特征共享带来正则化效果实现复杂训练不稳定数据量 20 万行以上如果决定尝试共享模型推荐用共享特征编码器加两个回归头的结构而不是直接输出二维向量。共享层学到的时间周期表示对两个任务都有用但最后一层各自接独立的线性层这样至少可以单独控制两个头的 loss 权重。4.3 序列模型LSTM 在什么情况下值得用LightGBM 用滞后特征天然只会看到固定窗口LSTM 则能学到“该看多长的历史”。但 LSTM 在纯表格特征上的优势并不大它真正擅长的是从原始序列里自动抽取模式。如果你手里的特征只有负荷历史、没有气象数据LSTM 会比树模型好一些一旦加入温度、节假日等强特征两者差距会迅速缩小。LSTM 还有一个额外成本训练时间和调参难度远超树模型。一个简化的 PyTorch 序列模型至少包含嵌入层、LSTM 层和线性输出头。import torch import torch.nn as nn class LoadLSTM(nn.Module): def __init__(self, n_features, hidden_size64, num_layers2, seq_len168): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 if num_layers 1 else 0, ) self.head nn.Linear(hidden_size, 1) # 单步预测 def forward(self, x): # x shape: (batch, seq_len, n_features) out, _ self.lstm(x) return self.head(out[:, -1, :])seq_len168对应过去一周的观测LSTM 会在这个窗口内学周期模式。hidden_size64和num_layers2对小时级负荷数据是起步配置再大就需要更多样本支撑。模型中用了batch_firstTrue输入张量的第一个维度是 batch第二维是序列长度这是最容易弄错的地方。LSTM 对特征缩放很敏感训练前必须对每个特征做 StandardScaler而且 scaler 只能 fit 在训练集上。生成滑动窗口样本时可以用torch.utils.data.Dataset配合DataLoader来避免一次性加载全量数据到显存。5. 评估口径、滚动预测与模型更新技巧5.1 评估指标负荷看 MAPE价格看 MAE 和分位数误差负荷预测最常用的指标是 MAPE平均绝对百分比误差因为它对用户直观误差 3% 意味着预测值和实际值平均相差 3 个百分点。但 MAPE 有一个致命缺陷——实际负荷接近零时分母趋近于零单个误差被无限放大。所以计算 MAPE 之前要过滤掉低谷时段的样本或者改用 sMAPE。价格预测则不建议用 MAPE因为电价经常出现负值负值会让百分比误差完全失去意义。价格预测推荐 MAE平均绝对误差配合 RMSE均方根误差一起看MAE 稳定反映平均偏差RMSE 放大尖峰误差两者差距大说明模型的误差主要来自少数极端价格。from sklearn.metrics import mean_absolute_error, mean_squared_error # 负荷评估过滤低谷再算 MAPE mask y_test 0.1 * train_df[load].median() mape (abs(y_test[mask] - pred[mask]) / y_test[mask]).mean() * 100 # 价格评估同时看 MAE 和 RMSE价差越大说明尖峰没抓住 mae_price mean_absolute_error(y_price, pred_price) rmse_price np.sqrt(mean_squared_error(y_price, pred_price)) print(fMAE{mae_price:.2f}, RMSE{rmse_price:.2f}, 两者比值{rmse_price / mae_price:.2f})判断模型好坏不能只看单个指标。负荷模型除了 MAPE还要看高峰期早 8 点到晚 10 点的分段 MAPE因为调度对峰时误差更敏感。价格模型要单独统计绝对误差大于 50 元的样本占比这些大误差样本才是市场风险的主要来源。5.2 多步预测的两种落地策略直接多输出和递归滚动生产环境中只预测下一个小时通常不够业务要的是未来 24 小时甚至 168 小时的曲线。两种常见做法递归预测把当前步的预测值当成下一步的特征输入实现简单、但误差会逐步累积直接多输出让模型一次输出未来 24 个点借助 LightGBM 的multi_output支持可轻松实现代价是没法在预测过程中利用真实观测值做修正。# LightGBM 直接多输出每个目标是一个未来时刻共 24 个 future_hours 24 y_multi np.column_stack([ df[load].shift(-h).values for h in range(1, future_hours 1) ]) # 去掉最后 24 行因为对应目标不存在 X_multi df.drop(columns[load, price]).iloc[:-future_hours] y_multi y_multi[:-future_hours] model_multi lgb.LGBMRegressor(**params) model_multi.fit(X_multi, y_multi)这套直接多输出的写法有几个注意点。shift(-h)得到的是 h 小时之后的值所以数据末尾有 24 行无法构造目标必须裁掉。多输出回归中 LightGBM 共用默认参数即可不需要为每个头单独调参但如果发现前几个小时的误差远小于后几个小时说明序列本身的信息量在衰减此时可以考虑递归方案。更稳妥的做法是两者混用用直接多输出预测未来 6 小时再用递归方式把预测结果逐步填充到 24 小时兼顾短期精度和长期覆盖。5.3 滚动窗口重训与误差漂移监控电力数据是非平稳的负荷水平会随季节漂移价格的市场机制也会变化。模型上线后不能一劳永逸需要定期用新数据重训。常见做法是每周重训一次训练窗口用过去 52 周的数据并把上周的实际观测拼进特征后再预测下一周。def rolling_refit(df, refit_days7, horizon24): import datetime as dt start df.index.min() pd.Timedelta(days365) end df.index.max() - pd.Timedelta(dayshorizon * 2) cur start while cur end: train_win df.loc[:cur] test_win df.loc[cur:cur pd.Timedelta(daysrefit_days)] # 每步重新训练模型并记录误差滚动均值 model lgb.LGBMRegressor(**params) model.fit(train_win[train_cols], train_win[load]) yield cur, model, test_win cur pd.Timedelta(daysrefit_days)这个滚动框架的重训频率、窗口长度和 horizon 都应当作为独立参数。refit_days7适合周频重训如果数据波动大可以缩到 3 天但要注意训练耗时训练窗口用固定长度比如 365 天比用全部历史更好因为太老的样本对当前市场状态几乎没有参考价值。同时要监控两个指标模型在最近 7 天的滚动 MAE 是否连续上升以及预测值是否出现系统性偏移比如连续多天预测偏高。一旦滚动 MAE 比基准值高 15% 且持续 3 个周期就触发重训或者检查特征输入是否有字段供给出了问题。另外建议把每次重训的验证集误差记录成日志按月对比这样可以尽早发现市场机制变化导致的误差趋势恶化。本文还有配套的精品资源点击获取
返回列表