最近好几个做电力市场方向的朋友都在问售电公司购售电策略该怎么建模,尤其是储能和可再生能源预测误差这块怎么处理。说实话,这个问题我在项目里踩了大半年的坑,从最开始的确定性模型一路做到随机优化,中间被偏差考核、场景爆炸、求解超时各种问题折磨得不轻。趁着假期把整个思路和Matlab实现细节梳理一遍,希望能给正在做类似课题的同行省点时间。
这篇不是纯理论推演,是能直接上手的实战记录。核心思路是用场景法刻画风电和光伏出力的不确定性,把储能当成灵活调节资源纳入日前-实时两阶段决策框架,目标函数里同时考虑期望收益最大化和条件风险价值约束。完整代码基于Matlab R2022b + YALMIP + CPLEX实现,模型规模可控,普通笔记本跑起来没问题。
1. 售电公司的利润与风险博弈:为什么决策模型这么棘手
1.1 售电业务的基本盈利模式
先理清售电公司的角色。售电公司处于电网公司、发电企业和终端用户之间的夹层,核心业务是从批发市场买电,再以零售合同价格卖给用户,赚取中间价差。听起来简单,但实际操作中每一个环节都带着不确定性。
售电公司主要的电量来源有三个:一是自主投资或签约的风电场、光伏电站,二是中长期双边合同,三是在电力现货市场的日前和实时阶段购电。零售侧一般由固定价套餐和分成套餐构成,用户负荷曲线会有波动,但相对可控。真正让决策变难的是批发侧的不可控因素。
很多刚入行的朋友会问,既然中长期合同可以锁定大部分电量,为什么还要精心设计购售电策略?问题在于,中长期合同不可能覆盖全部负荷,总有偏差电量需要在现货市场平衡。现货市场价格波动剧烈,而且偏差电量的结算规则各个省份都不一样,有的按统一出清价,有的按峰平谷分段价,还有的引入偏差考核系数。一旦实际用电量与合同电量偏差超过阈值,就要支付惩罚性费用,这部分成本在决策模型中必须显式刻画。
1.2 可再生能源误差如何冲击购电决策
可再生能源出力预测误差是售电公司最大的风险源之一。风电的预测误差在不同时间尺度上表现差异很大:日前预测的均方根误差通常在15%-25%之间,4小时前预测能降到10%左右,而实时预测仍会有5%-8%的误差。光伏的误差特性不同,主要受天气类型影响,晴天的误差很小,多云天或阵雨天误差会显著放大。
这个误差直接影响购电决策。假设日前阶段你计划从风电场获得100MWh电力,但实际只发了80MWh,这20MWh的缺口必须从实时市场购买。如果实时电价是日前电价的1.5倍甚至更高,这一单就亏了。反过来,如果实际出力超出预期,多出来的电在实时市场可能卖不出好价钱,如果出清价低于成本,还得倒贴。
对策不是简单的预留备用容量,而是在决策模型中引入不确定性描述,让模型自己权衡"多买一点保供应"和"少买一点省成本"之间的最优平衡。
1.3 储能在其中扮演的角色
储能系统对售电公司的价值,主要体现在三个层面:一是能量搬移,在电价低谷充电、高峰放电,直接赚取峰谷价差;二是偏差修正,可再生能源实际出力与预测值的偏差可以通过储能充放电来对冲,从而减少在实时市场的高价购电;三是辅助服务,具备一定规模后还可以参与调频、备用等辅助服务市场,但这需要独立的决策模块。
在购售电策略模型里,储能不是简单地当作一个独立的收益单元来建模,而是作为耦合日前与实时决策的桥梁。日前阶段确定储能充放电计划和市场购电计划,实时阶段根据实际的风光出力偏差,储能进行二次调整。这种两阶段结构正是随机规划里典型的"here-and-now"与"wait-and-see"决策逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学模型搭建:目标函数、约束条件与不确定性刻画
2.1 目标函数:期望利润与风险度量
模型的目标函数我在不同版本里试过三种形式:期望利润最大化、期望利润最大化加CVaR约束、以及均值-方差形式。实际项目里用下来,期望利润 + CVaR约束的表达最实用,既保留了收益弹性,又能有效控制尾部风险。
目标函数的数学表达为:
[
\max \sum_{s=1}^{S} \pi_s \left( \sum_{t=1}^{T} R_{t,s} - C_{t,s}^{\text{day}} - C_{t,s}^{\text{real}} - C_{t,s}^{\text{dev}} \right) - \beta \cdot \text{CVaR}_\alpha
]
其中 (S) 是场景数,(\pi_s) 是场景概率,(R_{t,s}) 是时段 (t) 场景 (s) 下的售电收入,(C_{t,s}^{\text{day}}) 是日前购电成本,(C_{t,s}^{\text{real}}) 是实时平衡成本,(C_{t,s}^{\text{dev}}) 是偏差考核惩罚,(\beta) 是风险厌恶系数,(\text{CVaR}_\alpha) 是置信水平 (\alpha) 下的条件风险价值。
CVaR的线性化表达是很多初学者容易卡壳的地方,后面代码部分会给出具体实现。这里先说明一个关键点:CVaR不是简单地用历史样本的尾部均值来替代,而是需要引入辅助变量 (\eta) 和 (z_s),把风险项转换成线性约束才能交给求解器处理。
2.2 储能运行约束与市场交易约束
储能系统建模的核心约束包括:充放电功率上下限、荷电状态(SOC)转移约束、SOC上下限约束,以及充放电互斥约束。
SOC转移方程:
[
E_{t,s} = E_{t-1,s} + \eta_c P_{t,s}^c \Delta t - \frac{1}{\eta_d} P_{t,s}^d \Delta t
]
其中 (\eta_c) 和 (\eta_d) 分别是充放电效率,(\Delta t) 是时段间隔。充放电互斥约束用一个大M法或者二进制变量来实现:
[
0 \le P_{t,s}^c \le P_{\max}^c \cdot u_{t,s}
]
[
0 \le P_{t,s}^d \le P_{\max}^d \cdot (1 - u_{t,s})
]
购售电约束方面,日前市场的购电量 (q_{t,s}^{\text{day}}) 在日前阶段就需要确定,不能随场景变化(非预期性约束);实时市场的调整量 (q_{t,s}^{\text{real}}) 可以依赖于场景。功率平衡约束是每个时段连接电源、储能、市场和负荷的核心等式:
[
P_{t,s}^{\text{RES}} + P_{t,s}^d + q_{t,s}^{\text{day}} + q_{t,s}^{\text{real}} = L_t + P_{t,s}^c
]
其中 (P_{t,s}^{\text{RES}}) 是场景 (s) 下的可再生能源出力,(L_t) 是负荷需求。
这里有一个容易被忽略的细节:实时市场的购电量有正负之分(缺电时买入,盈余时卖出),如果写成 (q_{t,s}^{\text{real}} \ge 0) 就限制住了售电公司向电网反送电的可能。实际建模中应该用两部分变量表示买入和卖出,或者直接让 (q_{t,s}^{\text{real}}) 取自由变量,成本项用分段函数处理。
2.3 场景生成:蒙特卡洛模拟与误差分布选择
不确定性建模是整个模型的关键。这里采用蒙特卡洛模拟生成可再生能源出力的随机场景。
风电出力预测误差分布,实测数据拟合下来通常用正态分布或者t分布都能有不错的近似。但要注意,风电出力是有上下界的(0到装机容量),直接用无界正态分布会产生超出物理边界的样本值,需要对样本做截断处理。光伏出力的误差分布更接近Beta分布,因为光照强度本身服从Beta分布,出力误差也随辐照度非线性变化。
具体操作时,我通常把预测值当作均值,按预测时长的不同设置不同的标准差系数。日前预测标准差系数设为15%,4小时前设为10%,实时设为6%。对于每个场景,风电的实际出力等于预测值加上一个正态扰动,然后裁剪到物理边界内。光伏场景还需要考虑天气类型,可以在场景生成之前先抽样决定天气类型,再根据天气类型抽样出力误差。
场景数量的选择直接影响求解时间。理论上场景数越多,随机规划的近似越精确,但模型规模线性增长,CPLEX求解时间随整数变量爆炸性增长。我在项目里测试过,50个场景和200个场景的最优解差距在2%以内,但求解时间相差20倍以上。实际使用时建议先用50个场景做参数敏感性分析,确定方案后再用200个场景精算。
3. Matlab代码实现的架构与关键模块
3.1 整体代码结构与数据流设计
代码架构方面,我建议按模块拆分,这样后续调整模型或换数据集都比较方便。目录结构如下:
code复制power_retailer/
├── main.m # 主程序入口
├── data/ # 数据文件目录
│ ├── wind_forecast.mat
│ ├── pv_forecast.mat
│ ├── load_curve.mat
│ └── price_data.mat
├── scenarios/
│ ├── gen_wind_scenarios.m
│ ├── gen_pv_scenarios.m
│ ├── scenario_reduction.m
├── model/
│ ├── build_optimization.m
│ ├── constraints_energy_storage.m
│ ├── constraints_cvar.m
│ └── objective_function.m
└── results/
└── output_results.m
主程序main.m的执行流程是:加载数据 → 生成场景 → 场景削减 → 构建优化模型 → 求解 → 输出结果 → 敏感性分析。数据流是单向的,每个模块只依赖前序模块的输出,这样调试时可以单独查看中间结果。
3.2 场景生成模块的实现细节
风电场景生成的Matlab实现如下:
matlab复制function scenarios = gen_wind_scenarios(forecast, capacity, num_scenarios, sigma_ratio)
% forecast: 日前预测出力序列 (T x 1)
% capacity: 风电场装机容量 (MW)
% num_scenarios: 场景数量
% sigma_ratio: 标准差系数,默认0.15
T = length(forecast);
scenarios = zeros(T, num_scenarios);
for s = 1:num_scenarios
% 生成正态扰动
noise = sigma_ratio * capacity * randn(T, 1);
% 实际出力 = 预测值 + 扰动,裁剪到物理边界
scenarios(:, s) = max(0, min(capacity, forecast + noise));
end
end
这个看似简单的函数有两个值得注意的细节。第一,噪声的幅度应该与装机容量挂钩而不是与预测值挂钩,因为误差的绝对值更接近装机容量的某个比例,直接乘预测值会导致低出力时段误差被低估。第二,裁剪操作虽然简单,但不能只在最后做一次,因为极端场景下很多样本会被压到边界,导致场景分布失真。更稳妥的做法是生成大量备选场景(比如500个),裁剪之后再用场景削减算法筛选出最优子集。
光伏场景生成要考虑时间相关性。光伏出力有很强的日周期特性,白天的误差模式与夜间差异很大。一个实用的做法是,先判断每个时段是否处于可发电时段(辐照度大于阈值的时段),只在可发电时段加入噪声,夜间时段出力严格为0。
3.3 优化模型的YALMIP建模与求解
优化模型的建模我用YALMIP工具箱配合CPLEX求解器。YALMIP的优点在于语法简洁,可以快速把数学模型翻译成代码,而且更换求解器只需要改一行配置。对于小白用户,也可以用Matlab内置的linprog和intlinprog函数,但模型表达会繁琐很多,尤其是带CVaR和二进制变量的混合整数线性规划,用YALMIP能减少至少一半的代码量。
目标函数和约束的建模代码:
matlab复制function [model, sol] = build_optimization(par, scenarios)
% 定义决策变量
q_day = sdpvar(par.T, 1); % 日前购电量
q_real = sdpvar(par.T, par.S); % 实时调整量
p_ch = sdpvar(par.T, par.S); % 储能充电功率
p_dis = sdpvar(par.T, par.S); % 储能放电功率
soc = sdpvar(par.T+1, par.S); % 荷电状态
u = binvar(par.T, par.S); % 充放电状态 (1充电, 0放电)
eta_cvar = sdpvar(1); % CVaR辅助变量
z_cvar = sdpvar(par.S, 1); % 场景尾部损失变量
% 目标函数
profit = 0;
for s = 1:par.S
revenue = par.pi * par.retail_price' * par.load;
cost_day = par.day_price' * q_day;
cost_real = sum(par.real_price(:, s) .* q_real(:, s));
cost_dev = par.deviation_penalty * sum(abs(q_real(:, s)));
profit = profit + par.scenario_prob(s) * (revenue - cost_day - cost_real - cost_dev);
end
objective = profit - par.beta * (eta_cvar + 1/(1-par.alpha) * sum(par.scenario_prob .* z_cvar));
这里需要特别解释一下CVaR的线性化写法。CVaR的定义是损失分布的尾部均值,在优化模型中通过以下约束实现:
matlab复制for s = 1:par.S
loss_s = -(revenue_s - cost_day_s - cost_real_s - cost_dev_s);
constraints = [constraints, loss_s - eta_cvar <= z_cvar];
end
constraints = [constraints, z_cvar >= 0];
变量 (z_s) 表示场景 (s) 的损失超过阈值 (\eta) 的部分,目标是让 (\eta + \frac{1}{1-\alpha}\sum_s \pi_s z_s) 最小化。这种表达把CVaR从非线性定义转化为线性约束,是随机规划里很标准的技巧。初学者容易犯的错误是把CVaR直接写成所有尾部场景损失的平均值,忽略了 (\eta) 本身也是决策变量,这样求出来的结果会严重偏保守。
储能约束的构建:
matlab复制for t = 1:par.T
for s = 1:par.S
constraints = [constraints,
0 <= p_ch(t, s) <= par.P_ch_max * u(t, s),
0 <= p_dis(t, s) <= par.P_dis_max * (1 - u(t, s)),
soc(t+1, s) == soc(t, s) + par.eta_c * p_ch(t, s) - p_dis(t, s)/par.eta_d,
par.SOC_min <= soc(t+1, s) <= par.SOC_max,
q_day(t) + q_real(t, s) + p_dis(t, s) + scenarios(t, s) == par.load(t) + p_ch(t, s)];
end
end
功率平衡约束里,我把可再生能源出力放在等式左边,表示它直接供给负荷,多余的或者不足的由储能和购电来平衡。这种表达方式比较直观,而且后续如果要增加弃风弃光约束,只需要在等式左边加一个松弛变量。
求解器调用:
matlab复制options = sdpsettings('solver', 'cplex', 'verbose', 1, 'debug', 1);
sol = optimize(constraints, -objective, options);
注意YALMIP默认是最小化目标函数,所以这里对目标函数取负号。
4. 场景削减与求解效率提升
4.1 同步回代消除法的原理
场景数越多,模型求解越慢,但直接减少场景数会损失随机规划的精度。场景削减(scenario reduction)就是在保证概率分布逼近精度的前提下,用较少的场景替代原始的场景集合。
我用的同步回代消除法(Simultaneous Backward Reduction,SBR)是电力领域最常用的场景削减方法,核心思想是迭代地删除对整体概率分布影响最小的场景,并把被删除场景的概率按距离加权分配给保留的场景。
算法的核心步骤:
- 计算任意两个场景之间的Kantorovich距离(通常用欧氏距离)
- 对每个场景,找到与其距离最近的另一个场景,记录距离值
- 删除使概率加权距离最小的场景,即距离最小且概率权重最低的场景
- 把被删场景的概率累加到离它最近的保留场景上
- 重复步骤2-4,直到场景数达到目标值
Matlab实现采用递推方式更高效,避免每次循环都重新计算所有距离矩阵。代码如下:
matlab复制function [scen_red, prob_red] = scenario_reduction(scen_orig, prob_orig, target_num)
% scen_orig: 原始场景矩阵 (T x N)
% prob_orig: 原始场景概率 (1 x N)
% target_num: 目标场景数量
[T, N] = size(scen_orig);
scen_red = scen_orig;
prob_red = prob_orig;
active = true(1, N); % 记录场景是否保留
remaining = N;
while remaining > target_num
% 只考虑活跃场景
active_idx = find(active);
M = length(active_idx);
% 初始化最小距离和对应索引
min_dist = inf(M, 1);
nearest_idx = zeros(M, 1);
for i = 1:M
for j = 1:M
if i ~= j
d = norm(scen_red(:, active_idx(i)) - scen_red(:, active_idx(j)));
if d < min_dist(i)
min_dist(i) = d;
nearest_idx(i) = active_idx(j);
end
end
end
end
% 找到要删除的场景
weight_dist = prob_red(active_idx) .* min_dist;
[~, del] = min(weight_dist);
del_idx = active_idx(del);
near_idx = nearest_idx(del);
% 概率转移
prob_red(near_idx) = prob_red(near_idx) + prob_red(del_idx);
prob_red(del_idx) = 0;
active(del_idx) = false;
remaining = remaining - 1;
end
scen_red = scen_red(:, active);
prob_red = prob_red(active);
prob_red = prob_red / sum(prob_red); % 归一化
end
这里用active逻辑数组标记场景是否保留,避免实际删除矩阵列带来的索引混乱。距离计算用二重循环,对于几千个场景实时缩减会比较慢,工程上可以用KD树加速,但一般应用场景量级下二重循环就够用了。
实际项目中,单个场景距离用5分钟或1小时分辨率的多维时间序列计算。如果直接对整个序列求欧氏距离,高维距离度量会稀释关键时段的差异,建议对关键时段(如峰荷时段)的误差赋予更高权重。
4.2 求解时间分析与参数调优
同样的模型,换了个求解器配置,时间可能相差10倍。下面是我测试过的不同配置下求解时间的数据,供参考:
| 配置项 | 场景数 | 变量数 | 约束数 | 求解时间 |
|---|---|---|---|---|
| CPLEX默认 | 50 | 15,200 | 22,500 | 245秒 |
| CPLEX+启发式开启 | 50 | 15,200 | 22,500 | 86秒 |
| CPLEX+MIP专注 | 50 | 15,200 | 22,500 | 152秒 |
| Gurobi 10.0 | 50 | 15,200 | 22,500 | 93秒 |
| Gurobi 10.0 | 100 | 30,200 | 45,000 | 382秒 |
CPLEX开启启发式算法后求解时间显著下降,但注意MIP gap也稍有上升,从默认的0.01%升到0.05%。实际工程中0.05%的gap完全够用,因为输入的预测数据本身误差就是十几个百分点,追求过高的求解精度没有实际意义。
还有一个小技巧:把储能SOC变量的初始值设为0.5(50%容量),比设为0可以缩短求解时间。原因在于初始SOC=0会迫使模型在第一个时段必须充电,增加了不必要的约束;而0.5的初值给了模型更大的可行域,搜索空间更平滑。
5. 算例分析:策略结果与敏感性解读
5.1 算例参数设定
为了验证模型,我构造了一个典型的省级售电公司场景。基础参数如下:
| 参数 | 数值 |
|---|---|
| 调度周期 | 24小时,1小时间隔 |
| 风电场装机容量 | 100 MW |
| 光伏装机容量 | 50 MW |
| 储能容量 | 40 MWh |
| 储能最大充放电功率 | 10 MW |
| 储能充放电效率 | 0.95 / 0.95 |
| 零售电价 | 0.65 元/kWh |
| 日前市场电价 | 峰谷分段(详见数据文件) |
| 实时市场电价 | 日前电价基础上加随机波动 |
| 偏差考核系数 | 0.15 元/kWh |
| 场景数 | 50(削减后) |
负荷曲线采用典型冬季日负荷,最大负荷80 MW,最小负荷35 MW,早晚两个峰值。电价曲线按照许多省份现货市场的实际情况设置为两个峰段,午间光伏大发时段电价偏低,晚高峰电价最高。
5.2 最优策略结果分析
先看储能充放电策略的结果。模型给出的最优策略很符合直觉:在凌晨1点到4点的低谷时段充电,在早高峰8点到10点和晚高峰18点到21点放电。储能系统一天完成两个充放电循环,每个循环实现约0.25元/kWh的价差收益,扣除效率损耗后每天给售电公司带来的额外利润大约是3.5万元。
可再生能源场景对策略的影响在实时购电计划上表现得最明显。在50个风电场景中,有12个场景出现了晚高峰时段风电出力低于日前预测的情况,其中3个极端场景的偏差超过20 MW。模型通过储能提前在午间光伏大发时段多充电,将这些场景的实时高价购电需求从平均12 MWh降低到5 MWh以下,这就是储能作为风险对冲工具的核心价值。
比较有意思的是,当我调整CVaR的置信水平 (\alpha) 从0.9变到0.99时,模型的决策行为发生了明显变化。(\alpha=0.9) 时模型更激进,午间低谷时段购电量更大,储能充放电次数更多;(\alpha=0.99) 时模型更保守,储能始终维持较高的SOC水平,避免在极端场景下无电可放的情况。
5.3 储能容量对策略收益的影响
储能容量从10 MWh增加到80 MWh时,系统总利润的变化呈明显的边际递减趋势。10 MWh到30 MWh区间,每增加10 MWh容量带来约1.8万元/天的利润提升;30 MWh到80 MWh区间,每增加10 MWh容量只带来约0.6万元/天的利润提升。这个结果可以给储能投资决策提供参考:并不是储能越大越好,容量超过一定阈值后,额外的充放电循环受到市场价差和负荷曲线的限制,边际收益快速衰减。
还要注意,这里用的是理想化储能模型,没有考虑电池循环寿命衰减。实际项目中,如果考虑电池寿命成本,最优储能容量会比理想模型小很多,因为频繁的深度充放电会加速电池老化。后续扩展中可以在目标函数中增加电池老化惩罚项,即 (C_{\text{aging}} = c_{\text{deg}} \times (P_c + P_d) \times \Delta t),其中 (c_{\text{deg}}) 是每MWh吞吐量对应的老化成本,通常取50-100元/MWh。
6. 踩坑实录:这段代码是我一路调出来的
6.1 二进制变量引起的求解爆炸问题
最初版本我用了每个时段、每个场景的充放电状态二进制变量 (u_{t,s}),50个场景就是 (50 \times 24 = 1200) 个二进制变量。CPLEX在默认配置下求解时间超过10分钟,完全无法接受。
后来做了一次改进:把充放电状态变量从场景维度中剥离,只在日前阶段决策,即 (u_t) 不随场景变化。这样的物理含义是:储能充放电计划在日前就已经确定,实时阶段只调整充放电功率的大小。优化结果从经济性角度几乎不变(因为日前制定的充放电时段已经是最优的),但求解时间从10分钟降到了2分半,效果立竿见影。
这个改动的实际逻辑是:充放电状态的决定因素主要是电价结构,而电价结构在日前已知,实时电价虽然有波动但不会改变峰谷时段格局。所以充放电时序在日前确定是合理的,实时只需要确定充放电功率大小。
6.2 YALMIP的sdpvar定义顺序对性能的影响
YALMIP的变量定义顺序会影响内部矩阵的生成效率。如果先定义二进制变量再定义连续变量,CPLEX求解器处理起来会更顺手。另外,如果一个变量既出现在非线性约束里又出现在线性约束里,YALMIP会将其自动升级为非线性模型,导致求解器选择错误。排查这类问题的方法是调用check函数检查模型类型和约束状态。
有一次我无意中把soc(t+1) == soc(t) + eta_c * p_ch - p_dis/eta_d写成了soc(t+1) == soc(t) + eta_c * p_ch - p_dis * (1/eta_d),从数学角度看完全等价,但YALMIP把 1/eta_d 当成了一个独立变量,把线性约束误判成了双线性约束,求解器换成了BMIBNB,求解时间直接爆炸到几个小时。后来把所有常数项都预先计算好,再遇到类似问题就迎刃而解了。
6.3 场景削减的技巧与陷阱
同步回代消除法有个容易踩的坑:当原始场景数量超过2000时,直接计算所有场景之间的两两距离矩阵会需要几百MB内存,运行时间也非常感人。我遇到过一个场景,4小时分辨率的风电场景有8760个场景,两两距离矩阵要占约600MB内存,直接导致Matlab内存溢出。
解决办法是分块计算距离,或者先用K-means聚类做一次粗糙的预削减,把场景数从几千降到500左右,再用同步回代消除法精削减到50个。这样既保证了精度,又避免了大矩阵的内存问题。
还有一个常见的误区:削减后的场景概率分布会在局部区域出现偏差,特别是概率小的极端场景容易被消除掉。而正是这些极端场景对CVaR约束的影响最大。解决方法是设置一个最小概率阈值,任何场景的概率不能低于 (1/(3 \times N_{\text{target}})),防止极端场景被合并掉。
6.4 价格与负荷数据的同步处理
这是最容易疏忽的地方。我最早测试模型时,直接把现货市场价格数据、负荷数据和风光出力数据拼在一起使用,结果模型报了一堆莫名其妙的不可行解。后来才发现,三个数据源的分辨率不一样,价椟数据是15分钟一个点,负荷数据是1小时一个点,风光出力是5分钟一个点。模型内部的 (\Delta t) 统一按1小时计算,所有数据都必须重采样到1小时分辨率。
重采样也要讲究方法,不能简单粗暴地用interp1插值。价格数据用均值重采样更合理(表示该小时内的平均成交价格),负荷数据也适合用均值,而风光出力数据如果原始分辨率高于目标分辨率,用均值可以平滑毛刺;如果原始分辨率低于目标分辨率,用前向填充会更合理,保持预测的信息结构。
6.5 如何验证模型的正确性
一个残酷的事实是,优化模型的代码即使运行无报错,结果也可能是错的。我的验证方法是分三步走。
第一步,把随机场景替换成确定性预测值(所有场景相同),此时模型应该退化为一个纯确定性优化问题,结果与单独写的确定性模型完全一致。如果两个模型的结果对不上,说明随机模型的约束或目标函数写错了。
第二步,把储能容量设为0,模型退化为纯购售电问题,此时储能相关的所有约束应该自动满足,模型不会报不可行。这一步验证储能约束与功率平衡约束之间的兼容性。
第三步,把CVaR约束的 (\beta) 设为0,模型退化为期望利润最大化。然后逐步增大 (\beta),观察最优解的期望利润是否单调下降。如果曲线不是单调的,说明CVaR约束的实现有bug。
这三步都通过之后,模型才基本可信。我在给客户的交付文档中会附带这三步验证的结果图表,作为模型正确性的佐证,这在学术审稿或项目验收时很加分。
7. 从论文到实战:延伸方向与实用建议
模型本身已经能解决售电公司购售电策略的基础问题,但实际商业场景中还有很多可以扩展的方向。
一个方向是多时间尺度协同。日前现在只做了24小时的单时段决策,更完备的框架应该包含年度中长期合同、月度合同、日前市场、日内市场和实时市场五个层级。每个层级有不同的决策频率和不确定性来源。把这几个层级耦合起来建模,模型会变成一个大规模多阶段随机优化问题,普通求解器很难直接处理,通常需要借助拉格朗日松弛或者随机对偶动态规划方法。
另一个方向是需求响应参与。现有的模型把负荷当作刚性约束,实际上售电公司可以通过价格激励引导用户调整用电行为。把需求响应加入模型后,负荷曲线本身变成决策变量的一部分,模型会更有意思。常见做法是引入可中断负荷和可转移负荷两种类型,分别建立响应量约束和经济补偿约束。
最后聊一下模型的可解释性。随机优化最优解只给出一组数字,但业务人员往往想知道"为什么这样决策"。一个务实的办法是在优化完成后,固定决策变量,逐场景回代计算对应的利润和电量平衡情况,输出每个场景下储能的充放电动作、实时购电量和偏差考核费用。把结果整理成报表,业务人员就能直观地看到模型在不同天气、不同价格情境下的应对逻辑,这比一组孤零零的最优解有价值得多。
代码和数据我整理成了一套完整的案例,包括多个测试算例和详细的注释说明,放在个人项目页上了,需要的可以自取。由于各地现货市场规则差异较大,参数数据建议根据自己的实际场景替换,模型主体结构不需要改动。跑通了之后,你们会发现跟着教程最快入坑的其实早就不是数学本身了,而是数据对齐和求解器调参这些看似琐碎实则决定成败的细节。
