做电力市场研究的朋友应该都有体会,售电公司这行最头疼的两件事,一是零售侧怎么设计套餐才能既吸引用户又不亏本,二是批发侧怎么在多个市场里买电才能把成本压下来。而这两个问题偏偏又耦合在一起:套餐定得不好,用户的用电行为就会变得很难看,购电成本跟着飙升;购电策略不合理,套餐定价又没有成本支撑。这就像左手和右手互相牵制,单边优化怎么都绕不过去。
今天想聊的这个项目——“基于主从博弈的售电商多元零售套餐设计与多级市场购电策略”,恰好就是一套把这两个耦合问题同时解决的方案。它来自EI收录论文的模型复现,用Matlab完整实现了主从博弈框架下的双层决策模型,适合电力市场方向的研究生、售电公司从事策略分析的从业者,以及正在学双层优化和博弈论的算法爱好者参考。
我前前后后把这套代码改过几个版本,踩过不少坑,也总结了一些工程化落地的心得。下面直接按建模思路、核心模型、求解实现、Matlab实操排查这几个维度拆开讲,尽量让你拿到代码时少走弯路。
1. 项目整体设计与核心建模思路
1.1 售电公司的双重身份决定了必须用博弈
先把业务逻辑想清楚。售电公司在电力市场链条里的位置很特殊:向下游用户卖电,电价可以自己定;向上游发电企业或批发市场买电,电价只能被动接受。这种“上半场是价格接受者、下半场是价格制定者”的双重身份,是理解这个项目所有建模逻辑的起点。
如果只用传统优化方法,通常的做法是把套餐设计和购电策略拆成两个独立问题:先假定用户用电量是固定的,优化购电成本;再根据购电成本反推套餐价格。但问题在于,用户从来不是固定负荷。你定了峰谷电价,用户就会把负荷从高峰挪到低谷;你设了阶梯电价,用户就会卡着阶梯阈值控制用电量。用户的这些行为反馈,又会反过来改变售电公司的购电曲线和成本。如果一开始没把这种反馈关系建进去,算出来的套餐再好也是纸上谈兵。
主从博弈(Stackelberg博弈)正是为这种“先决策、后响应”的层级关系设计的。售电公司先公布零售套餐,是博弈中的领导者;用户看到套餐后再调整自己的用电行为,是跟随者。用户的最优反应,又会作为约束进入售电公司的决策模型。这样一来,两个层面的目标函数和约束条件互相嵌套,形成一个完整的双层优化问题,套餐和购电策略在同一个框架下联合求解。
1.2 整体架构:上层管套餐、下层管负荷、中间用市场衔接
说到这套方案的建模结构,可以用一句话概括:上层售电公司决策零售套餐和购电计划,下层用户决策用电量,两者通过零售电价和用电负荷耦合,购电策略再通过多级市场价格传导进来。
具体来说,上层模型的目标是最大化售电公司净利润,包含零售收入、购电成本两大部分。决策变量包括零售套餐参数(比如分时电价、阶梯价格、固定服务费)和购电组合(中长期合约电量、日前市场购电、实时市场调整)。下层模型的目标是用户效用最大化,决策变量是每个时段的用电量,约束包括用电上下限、可转移负荷比例、总用电量范围等。
这里需要特别注意,上下层之间有双向的信息传递机制。上层把零售电价作为参数传给下层,下层通过求解用电效用最优化问题,把最优用电曲线返回给上层,上层再在新负荷曲线下重新评估利润。整个模型算到最后,要找到一个“用户对所有套餐方案的最优反应都是这个方案”的均衡点,也就是Stackelberg均衡。
从实现上讲,这个双层结构不能直接交给求解器。常用的做法是把下层优化问题用KKT条件替换,变成上层的约束,从而把双层问题转化为单层混合整数规划。这个转化过程是这个项目的核心关键点,后面我会详细拆。
2. 零售套餐设计的建模细节与用户响应逻辑
2.1 多元零售套餐怎么落到数学表达式里
先说套餐设计。现实中售电公司的零售套餐花样很多,但归纳起来核心的定价维度就那几个。这个项目里我重点讲三种常见类型在Matlab里的建模方式:
第一种是分时电价套餐。把一天24小时分成峰、平、谷三类时段,每个时段对应不同电价。数学上就是一组分段常数函数:c_t = c_peak(高峰时段)、c_flat(平段)、c_valley(低谷时段)。决策变量是各时段电价,约束条件包括电价上下限,以及峰谷价差的合理区间——价差太小,削峰填谷的激励不够;价差太大,用户可能会投诉或者选择别家。
第二种是阶梯电价套餐。设定一个或者多个电量阈值,用电量落在哪个区间,就执行对应价格。这个模型的难点在于用户的总用电量一开始是未知的,它取决于电价本身,所以阶梯阈值、阶梯价、用户响应之间形成联动。在多层阶梯下,建模需要用0-1变量判断用户落入哪个阶梯区间,这也是模型变成MILP的主要原因之一。
第三种是“固定服务费+电量电价”的两部制套餐。每月固定收一笔服务费,覆盖基础成本和固定投资,然后每度电再收一个电量电价。这种套餐的好处是收入稳定,坏处是用户会觉得“不管用不用电都要交钱”,参与度可能打折。
实际建模中,这三种套餐往往是组合出现——比如用户可以选择分时电价套餐,然后在这个基础上再叠加阶梯折扣。组合套餐的好处是用户面更广,不同类型的用户(工业用户、商业用户、居民用户)都能找到适合自己的方案。
2.2 用户用电响应模型:效用函数和弹性矩阵
用户侧模型的常见做法是用效用函数描述用户的用电满意度。最常用的二次效用函数形式是:
U(q) = α·q - (β/2)·q²
其中q是用电量,α和β是效用参数,α越大说明用户对用电的需求越迫切,β越大说明用户的饱和效应越强。用户的最优用电量就是在每个时段使边际效用等于电价的位置,即∂U/∂q = 电价。
从用户角度看,目标函数是净效用最大化,也就是用电效用减去电费支出。用户面临的问题可以写成:
max Σ[α_t·q_t - (β_t/2)·q_t² - p_t·q_t]
s.t. 每个时段用电量上下限约束,相邻时段负荷变化率约束,总用电量范围约束。
这套用户响应模型很经典,它在经济学上对应消费者理论中的“需求函数”推导:给定价格,用户通过优化得到需求曲线。把用户的KKT条件推导出来,得到的最优用电量和电价之间的关系,本质上就是弹性系数矩阵的体现。
实操中α、β这些参数的获取是难点。最可靠的方法是拿到真实的负荷数据,用最小二乘法做参数辨识。如果项目阶段没有实测数据,也可以参考同行业的负荷弹性研究报告来设定合理区间,再用灵敏度分析验证结果对参数变化的稳定性。
2.3 释放一点:为什么主从博弈比“两阶段随机优化”更适合这里
这里有必要展开一个容易混淆的点。很多人可能第一反应是:这不是个随机优化问题吗?市场价格不确定,需求不确定,用两阶段随机规划不就行了吗?
两阶段随机优化能做,但它和主从博弈的视角不同。随机优化里的“不确定性”来自外部环境(市场价格、新能源出力等),决策者只是被动应对不确定性。而主从博弈处理的是“策略性互动”——用户不是天然的不确定变量,他们会根据你的套餐主动调整行为。这属于内生反馈,不是外生随机性。
换句话说,随机优化假设用户负荷是一个随机变量,而主从博弈把用户看成有独立决策能力的理性主体。这个区别很关键,因为它直接决定了模型的解读方式:在主从博弈下,套餐设计的每一分钱定价都要考虑到用户会“反击”,而随机优化里没有这种策略对抗关系。
3. 多级市场购电策略与双层模型转化
3.1 中长期、日前、实时三级购电框架
购电侧的多级市场结构可以从风险管理的角度去理解。电价波动风险大,所以不能把宝全押在某一个市场上,要分层配置。
中长期合约市场对应的是“基本盘”。提前数月甚至一年锁定一部分电量,价格锁定,消除大部分价格风险。在模型里,中长期购电量的特点是:在日前决策之前就必须确定,相当于第一阶段决策变量,购电价格是已知的常数。
日前市场对应的是“调整层”。在运行前一天,根据负荷预测和新能源出力预测,在中长期的基础上补足缺口或者卖出富余。日前市场电价是分时波动的,通常用一组场景来表示不确定性。
实时市场对应的是“纠偏层”。运行当天,实际负荷和预测值不可能完全一致,偏差部分通过实时市场平衡。实时电价往往比日前高,用来惩罚预测不准造成的偏差。
这三层市场放在同一个模型里的逻辑是:中长期锁定的比例越高,价格风险越小,但电量结构可能越偏离实际需要,导致日前和实时调整量变大;反之,中长期买太少,日前价格大幅波动时风险又控制不住。这就是一个典型的风险-成本均衡问题。
3.2 购电成本函数建模与耦合约束
在模型里,购电成本分三块:
C_total = C_longterm + C_dayahead + C_real
- C_longterm = 中长期合约电量 × 合约价格(常数,一次性决策)
- C_dayahead = Σ(日前购电量_t × 日前价格_t)(每个时段都不一定,但日前价格已知或场景已知)
- C_real = Σ(实时调节量_t × 实时价格_t)
这里有个关键耦合约束:每个时段的实际负荷q_t必须等于中长期电量、日前购电量和实时调整量之和(考虑正负调节,多买的部分算卖回市场)。这个约束把用户侧的负荷决策和购电策略紧紧绑在一起。
如果进一步考虑风险偏好,可以在目标函数里加一个风险惩罚项,比如用条件风险价值(CVaR)来控制最坏场景下的购电成本。项目的基础版本可以不加,但对有CVaR要求的场景,这套框架稍微改造一下就能扩展。
3.3 KKT条件转化:把双层问题掰成单层
这是整个项目里最有技术含量的一步。前面提到,上下层模型嵌套,不能直接求解。业界最成熟的处理方法是KKT条件替换法,把下层优化问题转化为它的KKT最优性条件,作为上层模型的约束条件。
具体来说,下层用户问题是一个凸二次规划,目标函数二次可微,约束是线性的,所以KKT条件是充要的。写下用户问题的KKT条件,包含三部分:
- 拉格朗日函数对q_t的偏导等于0(平稳性条件)
- 原可行条件和对偶可行条件
- 互补松弛条件:对偶变量乘以对应的约束松弛量等于0
前两类是线性的,直接加入上层模型即可。麻烦的是互补松弛条件——两个非负数的乘积等于0,这是一个非线性等式。在Matlab里,处理它的标准做法是用大M法引入0-1变量。如果原问题的某个约束是g(x) ≤ 0,其对应的对偶变量是λ,那么互补松弛条件λ·g(x) = 0就等价于两个约束:
g(x) ≤ M·z λ ≤ M·(1-z)
其中z是0-1变量,M是一个足够大的正数。这样就把非线性项变成了线性不等式,整个模型升级为MILP,可以交给求解器处理。
为什么这么麻烦还没有替代方案?因为双层问题在理论上即使上下层都是凸问题,整体仍然可能非凸,KKT转化搭配MILP是目前商业求解器能稳定求解的可靠路径。学术界研究过其他方式,比如迭代启发式算法,但收敛性没有保证,容易被局部最优卡死。
这里有一个很重要的注意事项:大M参数的选择直接影响求解质量。M太大会导致数值病态,求解器容易产生不可行的伪解;M太小又会错误地截断可行域。实操中我一贯的做法是先求解一个稍宽松的松弛问题,根据对偶变量的量级来估计一个合理的M,通常取对偶变量理论上限的10到100倍。
4. Matlab实操过程与代码实现要点
4.1 准备工作:工具箱选择和脚本框架
先说环境。这套代码我用的是YALMIP搭配求解器,主用求解器是GUROBI或者CPLEX这类商业求解器都行,没有的话用MATLAB自带的intlinprog也可以,只是规模大时速度会差一些。
YALMIP是一个建模工具箱,它的优势是用数学表达式的角度写约束,代码可读性非常高,非常适合双层优化模型这类结构复杂的项目。写约束时几乎和数学表达式一样,后面维护也方便。
代码从结构上划分,通常包含五个模块:
- 参数初始化脚本——所有原始数据的录入(负荷参数、电价场景、用户效用系数)
- 下层用户决策问题的约束构建函数
- KKT条件转化函数
- 上层主问题(含KKT条件转化后的约束)组装
- 求解与结果输出模块
4.2 数据准备与场景设计
模型里需要准备的数据大致包括:
| 数据类别 | 具体内容 | 来源建议 |
|---|---|---|
| 典型负荷曲线 | 24小时基准负荷,区分工作日/周末 | 历史实测或模拟参考 |
| 用户效用参数 | α、β序列 | 负荷弹性回归或行业报告 |
| 中长期合约价格 | 单一常数 | 市场合同报价 |
| 日前市场价格 | 24小时价格序列,可能多场景 | 历史日前市场出清价 |
| 实时市场价格 | 24小时价格序列 | 历史实时市场出清价 |
| 电价上下限 | 零售电价可行区间 | 政策监管要求 |
实操里有几点建议。负荷曲线建议取夏/冬两个典型日分别建模再聚合,比单日模型更稳定。日前电价场景,如果数据充足,用K-means聚类把历史电价聚成3~5个典型场景,配上概率权重,比简单取平均更接近真实分布。用户效用参数中的β系数,控制在让用户需求的弹性在合理范围内(一般价格弹性绝对值在0.1到0.5之间),太激进的弹性会让模型振荡。
4.3 核心代码实现分步拆解
下面按YALMIP的写法给一个核心骨架,方便你对照理解每个子模块的作用。
首先是用户下层问题的KKT条件生成。如果用YALMIP的KKT命令可以直接生成,但为了深入理解模型结构,我建议先手写一遍:
% 定义用户决策变量(负荷) q = sdpvar(24, 1); % 24小时用电量 % 用户问题:最大化效用 - 电费 % 目标函数 U = alpha'*q - 0.5*q'*diag(beta)*q - price'*q % 约束条件 Constraints_user = []; Constraints_user = [Constraints_user, q_min <= q <= q_max]; % 总用电量约束 Constraints_user = [Constraints_user, sum(q) >= Q_total_min, sum(q) <= Q_total_max]; % 用KKT命令直接获取KKT条件 % [KKT_sys, details] = kkt(Constraints_user, Objective_user, q);如果不用YALMIP的KKT自动生成工具,手写方式如下:
% 拉格朗日乘子 lambda_low = sdpvar(24, 1); % 下限约束的对偶变量 lambda_high = sdpvar(24, 1); % 上限约束的对偶变量 mu_sum_low = sdpvar(1); mu_sum_high = sdpvar(1); % 平稳性条件:alpha - beta.*q - price - lambda_low + lambda_high - mu_sum_low + mu_sum_high = 0 Stationarity = alpha - beta .* q - price - lambda_low + lambda_high - mu_sum_low + mu_sum_high == 0; % 互补松弛条件(用大M法) M_big = 1000; z_low_low = binvar(24, 1); z_high_high = binvar(24, 1); % 对每个时段t: % lambda_low(t) <= M * z_low(t) % (q_min - q(t)) <= M * (1 - z_low(t)) % lambda_high(t) <= M * z_high(t) % (q(t) - q_max) <= M * (1 - z_high(t))上层售电公司的问题就比较直接了。决策变量有零售电价、中长期电量、日前购电量、实时调节量等,目标函数是收入减去三项购电成本。
% 上层决策变量 price_retail = sdpvar(24, 1); % 零售分时电价,作为套餐的核心参数 E_long = sdpvar(1); % 中长期合约电量(常数) E_day = sdpvar(24, 1); % 日前市场购电量 E_real = sdpvar(24, 1); % 实时市场调节量(正买负卖) % 目标函数:零售收入 - 购电成本 Revenue = sum(price_retail .* q); % 售电收入 Cost_long = E_long * price_long; Cost_day = sum(E_day .* price_day); Cost_real = sum(E_real .* price_real); Profit = Revenue - Cost_long - Cost_day - Cost_real; % 耦合约束 Constraints_top = []; Constraints_top = [Constraints_top, q == E_long + E_day + E_real]; Constraints_top = [Constraints_top, E_long >= 0, E_day >= 0]; % 实时调节量允许正负把上下层组装起来,加上KKT条件和二进制变量,生成MILP问题,最后调用求解器:
% 组装完整问题 Constraints_total = [Constraints_user_kkt, Constraints_top, ...]; Objective_total = -Profit; % 最小化负利润 % 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'mipgap', 0.01); optimize(Constraints_total, Objective_total, ops);这里main脚本的完整流程是:加载参数→构建约束→求解→后处理出图。后处理部分建议至少输出三类结果:最优套餐价格曲线(随时间变化)、用户负荷响应曲线(对比优化前后的负荷曲线)、成本和利润组成柱状图。这三张图是论文展示的核心图,也是你判断结果是否合理的第一手依据。
5. 常见问题与排查技巧实录
5.1 求解不收敛或者找不到可行解
这是双层优化项目里出现频率最高的问题。我的排查顺序很固定:
先检查大M设置。大M太大,MILP的LP松弛界面会出现数值问题,求解器会报“Numerical issues";大M太小,可行域被错误截断,报“Infeasible problem”。我试过的经验值是:让M尽量小,但确保它比所有对偶变量的理论上限大10倍以上。判断方法:先在有解但不含互补松弛条件的松弛问题里查一下对偶变量的量级,再据此定M。
其次检查二进制变量总数。24时段的双层问题加阶梯套餐的多级判断,二进制变量很容易过百,MILP一旦上了几百个二进制变量,求解时间会指数级增加。如果求解器半小时还出不来,建议先固定一部分设计变量(比如先固定阶梯阈值只优化分时价格),验证可行性后再放开。
最后,还要留意互补松弛条件的配对方向。KKT条件转化时,一个常见的笔误是把原问题约束方向搞反,导致互补松弛条件永远无法满足。这种问题没有语法错误,但结果永远是infeasible。排查时需要回到原始约束,逐一检查“原始松弛×对偶变量=0”的配对关系。
5.2 结果很奇怪,套餐价格算出来几乎全是边界值
如果你得到的分时电价要么贴着上限、要么贴着下限,中间值很少,大概率是模型里缺少足够强的平滑约束或者惩罚项。这种情况我遇到过,核心原因是套餐价格对下层用户决策的杠杆作用太强,模型倾向于采用“极限定价”策略。
解决方法是加入约束限制峰谷价差,或者对用户效用的β参数做调整。增加用户对电价的敏感性(提高β)会让需求曲线更富有弹性,套餐价格变化对负荷的影响更明显,模型才有动力去寻找中间价格。另一个更工程化的方法是给零售电价的变化范围设定一个窄一点的区间,让模型被迫在区间内部做文章。
5.3 灵敏度分析该怎么做,重点看哪几个参数
这个项目里值得做灵敏度分析的参数,我建议重点看三个:
- 用户效用参数β:β增大意味着用户对电价更敏感,套餐定价空间变小,购电策略中日前市场的占比应该上升。
- 日前市场价格波动幅度:波动越大,中长期合约配比应该越高。
- 实时市场价格惩罚系数:惩罚越高,模型会约束日前购电尽量贴近实际负荷,减少实时调整量。
做法上,不需要重新跑整套流程,可以写一个循环脚本,对每个目标参数从0.8倍基准值到1.2倍基准值扫一便,画出关键输出指标(净利润、套餐均价、负荷峰谷差)的变化曲线。这组结果往往比主算例本身更有说服力,审稿人和管理层都很吃这一套。
5.4 参数合理性,别让模型“算得漂亮但跑不通”
最后说一个容易被忽略的问题:模型参数和实际业务脱节。代码算出来的结果可能很漂亮——利润很高、负荷曲线很平稳——但仔细一看,零售电价定为每千瓦时三块钱,这在任何现实市场里都不可能被监管通过。模型漂亮不等于业务可行。
我的习惯是在写完模型后做一次“业务合理性审查”:每个决策变量的最优值都对照现实约束检查一遍,看电价是否落在政策允许区间,看中长期电量占比是否在合理范围内,看用户负荷变化是否符合电价弹性常识。这一步不花太多时间,但能避免在汇报时被问住。
结语与实操心得
整套项目做下来,我最大的体会是:主从博弈建模本身不难,难的是工程化落地时那些“隐形的坑”——大M怎么取、互补松弛怎么配对、MILP规模怎么控制、参数怎么标定。这些经验在论文里往往一笔带过,可恰恰是代码能不能跑通、结果能不能用的关键。
如果你是第一次上手这类项目,我建议不要一上来就追求模型的复杂性。先跑通一个最简版本:分时电价套餐配单日前市场,24时段,10个用户聚合体,不加阶梯不加实时市场。在这个骨架上验证KKT转化和MILP求解没问题了,再逐步加功能模块。我自己的经验是,一个能跑的简单模型,远比一个跑不动的复杂模型有价值。
最后分享一个平时不太会写在文档里的小技巧:调试双层优化模型时,优先验证“固定上层变量时,下层能否复现出理论最优解”。拿一个已知结果的简单场景,手动固定套餐价格,用单独的优化器解下层用户问题,再将结果与双层联合求解的用户部分对比。对得上,说明KKT转化没问题;对不上,问题大概率出在互补松弛条件或大M配合上。这招在调试效率上的提升,真的是谁用谁知道。