做电力市场方向的算法复现,最怕遇到两类题目:一类是模型稀碎,论文里全是“显然可得”,代码根本对不上;另一类是模型完整,但求解器一跑就是十几个小时,收敛了算你运气好。这个“基于主从博弈的售电商多元零售套餐设计与多级市场购电策略”就属于后者——模型结构清晰,博弈层级分明,但真正动手用Matlab复现一遍,坑全藏在细节里。
我算了一下,这套代码跑通之后,正好覆盖了售电公司决策链路上最核心的两个环节:对下怎么给用户设计零售套餐,对上怎么在批发市场里分配购电比例。中间用主从博弈把两个环节串起来,形成一个上下层联动的闭环决策模型。这个思路放在今天依然不过时,而且很适合新手入门博弈论与电力市场交叉方向的第一个完整项目。这篇文章把我从读论文到写出可运行Matlab代码过程中踩过的坑、摸出来的路,完整走一遍,希望能给正在啃这个方向的同学一点参考。
1. 先说清楚:这个复现项目到底在做什么
1.1 标题拆解:三个关键词背后是一整套决策链路
“主从博弈”“售电商”“多元零售套餐”“多级市场购电策略”,这四个词组拆开看都不难理解,但合在一起,代表的是一个上下两层、环环相扣的优化问题。
“主从博弈”是Stackelberg博弈的中文学术叫法,核心思想是决策有先后:一个领导者先表态,跟随者再根据领导者的决策做自己的优化。在电力零售场景里,角色分工非常自然——售电商先公布套餐电价,用户看到电价后调整自己的用电行为。售电商做决策时又必须预期到用户的这种反应,所以不能拍脑袋定价,必须把“用户怎么响应价格”这件事解出来,再放进自己的利润优化里。
“售电商”就是电力零售商,从批发市场买电然后卖给终端用户。它既不是发电厂,也不是用户,属于中间商环节。中间商的难点在于两头受挤:上游购电价格有波动,下游用户有选择权,利润空间需要靠精细化的价格策略“抠”出来。
“多元零售套餐”对应的是售电商对下游用户的价格设计。用户可以选固定电价套餐,也可以选分时电价套餐,不同套餐对应不同的价格结构,目的是差异化引导用户改变用电行为,实现削峰填谷,同时提升售电商利润。
“多级市场购电策略”对应的是售电商对上游批发市场的购电决策。电力批发市场通常不是单一市场,而是分多个时间尺度——中长期合同、日前市场、实时市场等等。购电策略就是决定在哪个市场买多少电,既要保证成本低,又要控制价格波动风险。
所以这个题目本质上不是在做一个孤立的定价模型,而是在建一个一套完整的售电公司决策链路:上游购电决策、下游零售定价、两者之间的博弈联动,一步到位。
1.2 选中这个题目的理由:为什么主从博弈特别适合售电定价
做这个方向之前我也想过,售电公司定价不就是个利润最大化问题吗?直接建个优化模型解一下不就行了?
问题没这么简单。如果直接把用户需求当成固定参数来优化电价,得到一个价格,然后把这个价格扔给用户,用户实际反应会和你的假设完全不同。用户的用电量会随着电价变化而变化——电价高了就用得少,电价低了就用得多。这个弹性效应如果不在定价时考虑进去,算出来的“最优价格”只是纸上谈兵。
主从博弈正好给了这个问题一个规范的数学框架。它把决策拆成两层:领导者(售电商)先给出价格,跟随者(用户)根据价格做用电量优化。核心是我们回到了一个博弈问题,决策是序贯的:售电商定价时就要“站在用户的角度”预判用户会如何反应。这种结构比单层优化更贴近真实市场运行方式,而且从学术上说,模型的解释力强很多。
再往下想,如果能把这个双层结构跑通,这个方法论完全可以平移去做其他领域的定价问题,比如共享出行平台定价、云资源定价、电信流量套餐设计。这也是我当初愿意投入时间复现这个题目的原因——模型的可迁移性很强,学一次到处用。
2. 核心模型框架:主从博弈是如何运作的
2.1 Stackelberg博弈的核心思想与数学表达
Stackelberg博弈的核心关键词就两个:序贯决策和逆向归纳。序贯决策是说决策有先后,逆向归纳是说要先算清楚跟随者的最优反应,再回头确定领导者的最优决策。
放到售电场景里,过程是这样的:
- 领导者(售电商)选择零售套餐价格策略;
- 跟随者(用户)看到价格后,在预算和用电效用之间做权衡,决定用多少电;
- 用户的用电量汇总起来,又决定了售电商的总售电量;
- 售电商再根据这个总售电量,去批发市场安排购电计划。
数学上,主从博弈可以写成这样一个双层优化框架:
- 上层目标(售电商利润最大化): max 利润 = 零售收入 - 购电成本 决策变量是套餐价格
- 下层问题(用户效用最大化): max 用户效用 - 电费支出 决策变量是各时段用电量
这里最关键的难点是:上层的问题里嵌套着下层的最优化问题,不能直接把下层变量当常数来优化。
2.2 双层模型的构建:上层定价,下层响应
下层用户建模是整个模型的地基。大多数论文会把用户的用电效用定义成一个关于用电量的二次凹函数:
U(q) = a * q - 0.5 * b * q^2
其中a和b是描述用户偏好的参数。用户要最大化“用电效用 - 电费支出”:
max [a * q - 0.5 * b * q^2 - p * q]
对q求导并令导数为零,得到:
a - b * q - p = 0
也就是说:
q = (a - p) / b
这个形式太重要了。它把用户对价格的反应直接变成了一个线性需求函数,价格越高用电越少,参数b决定了用户对价格的敏感程度。b越大,用户越倾向于节省电,需求变化对价格越敏感。
有意思的是,这个线性需求函数在后面代码实现的时候给我们省了很大的麻烦。因为下层优化如果是线性或二次凸问题,KKT条件就有闭式表达,整个求解过程能大大简化。这也是为什么很多论文都采用二次效用函数——它既符合经济学直觉,又保证了数学上的可解性。
上层问题是售电商的利润最大化。利润等于零售收入减去购电成本。零售收入是各套餐价格乘以对应用户的用电量,购电成本则依赖各市场购电量与价格。上层问题的决策变量是各类套餐的定价,同时还要满足一些约束,比如价格设定不能导致用户效用为负,否则用户就不会选择这个套餐。
2.3 求解路径:KKT条件与MPEC转化
双层优化最大的难点在于:上层问题的约束里包含下层最优化问题,没法直接喂给求解器。解决这个问题的标准手法是用下层问题的KKT条件替换下层优化问题,得到单层但带互补约束的数学规划问题,简称MPEC。
MPEC问题的麻烦之处在于互补约束,也就是这种形式:0 <= λ ⊥ F(x) >= 0,意思是λ和F(x)至少有一个等于0。这种约束让整个问题不再满足经典的约束规范,求解器无法直接处理。
处理互补约束的常见办法是大M法,引入一个足够大的常数M,把互补约束转成混合整数线性约束:
- F(x) >= 0
- λ >= 0
- F(x) <= M * z
- λ <= M * (1 - z)
- z是0-1变量
这样MPEC问题就变成了一个MILP或MIQP问题,可以交给Gurobi或CPLEX这类商用求解器直接求解。
这里藏着一个实际经验:大M的取值非常讲究。取太小,会错误地截掉可行域;取太大,会引起数值病态,导致求解精度下降。我在复现过程中试过从100到1万的不同取值,最稳定的区间大概在价格的5到10倍量级。如果你发现同样的模型在不同M值下结果差异很大,首先要怀疑的就是大M取值不当。
除了直接用KKT条件转化,还有另一条路线:用迭代法在上层和下层之间反复横跳。先给定一个价格,求解下层用户响应,得到用电量后带回上层,更新价格,再传给下层,如此循环直到价格收敛。这条路线实现简单,但收敛性没有保证,遇到多均衡场景容易震荡。
3. 多元零售套餐设计与多级市场购电策略拆解
3.1 套餐设计:价格信号如何引导用户改变用电行为
多元零售套餐是这个模型的下游模块,核心目标是用价格杠杆引导用户调整用电行为。现实中的用户不是铁板一块,有的对价格敏感,有的更看重用电稳定,有的用电时间长、有的集中在峰期。一套价格走天下必然损失利润,多元套餐本质上是对用户群体的细分。
我实现的模型里加入了几类典型套餐:
- 固定电价套餐:一天24小时价格不变,适合风险厌恶型用户。这类用户不希望电价波动,愿意接受一个中等偏高的统一定价换取确定性。
- 分时电价套餐(TOU):把一天分成峰、平、谷三个时段,峰时电价高、谷时电价低。这类套餐鼓励用户把用电从高峰时段挪到低谷时段。
- 实时电价套餐(RTP):直接跟随批发市场价格波动,批发价高零售价就高,批发价低零售价就低。用户承担了价格波动的风险,但有可能获得更低的平均电价。
不同套餐的设计不是孤立的,它们共享同一个用户群。用户在多个套餐之间做选择,会尽量挑选让自己效用更高的那个。也就是说,上层定价时必须同时考虑套餐间的竞争关系,确保每一类特定套餐都至少能吸引到一部分特定属性的用户,否则这个套餐就是无效的。
算例中我设置了一个比较常见的分时电价情境:峰时段电价1.2元/kWh,平时段0.75元/kWh,谷时段0.4元/kWh,固定电价套餐取0.9元/kWh。用户需求弹性较大时,分时电价能把一部分高峰负荷挪到低谷,从系统角度看是削峰填谷,从售电商角度看是降低了高峰时段的高价购电压力。
3.2 购电策略:批发市场的层级结构与购电组合优化
多级市场购电策略是这个模型的上游模块。真实电力批发市场不是一天只买一次电,而是按照不同时间尺度滚动交易:
- 中长期市场:提前几个月到几周签购电合同,价格相对稳定,锁定基础电量,控制风险。
- 日前市场:运行日前一天确定第二天的发用电计划,价格波动开始显现。
- 实时市场:运行实时平衡偏差电量,价格波动最大,但只涉及少量电量。
购电策略就是决定这三个市场各自购多少电。逻辑上,中长期购电比例高,则成本稳定但可能错过低价机会;实时市场比例高,则有机会吃到低价红利但风险也大。一般思路是中长期锁定70%到80%的基础电量,剩下在日前和实时市场里优化分配。
模型里把购电成本拆成三部分之和,各市场的购电价格用场景集合描述,通过条件风险价值或者方差项来控制购电成本的波动风险。
这里有一个很容易踩的坑:不要把所有用户负荷都当成一个总负荷来匹配购电策略。因为套餐结构不同,分时电价用户和固定电价用户的负荷曲线差异很大。分时用户的负荷会明显向谷时段迁移,如果购电策略还用原始的聚合负荷曲线,会高估峰时段购电需求,低估谷时段购电需求,成本算出来是偏高的。正确做法是先算出各套餐用户的优化后负荷曲线,再加总去匹配购电决策。
3.3 两级决策的联动逻辑:套餐定价与市场购电是同一个硬币的两面
很多第一次做这个题目的人,最容易把“零售套餐”和“购电策略”当成两个独立的问题先后去解。先随便定个套餐价格,算出来用户负荷,再做购电优化——这个顺序做出来基本是垃圾结果。
原因是这两个问题有强耦合:购电成本取决于总负荷曲线,总负荷曲线取决于用户用电量,用户用电量取决于套餐价格,套餐价格又必须覆盖购电成本设定利润空间。任何一个环节变动,都会顺着链条传到底。
举个具体例子:如果购电成本上升,售电商需要提高零售电价来维持利润。但电价一提高,价格敏感型用户就会降低用电量甚至改选其它套餐,导致总售电量下降。如果售电量下降幅度过大,提高电价反而可能导致收入降低。这就是经济学里的“需求弹性反噬”。
主从博弈模型的精妙之处就在这里:它把这两件事放在同一个框架下一并解决。上层优化里,零售价格和购电分配同时作为决策变量出现;下层用户响应既是定价的依据,也是购电需求的计算前提。两个决策在同一个优化问题里互相制约、互相支撑,最终同时输出套餐定价方案和购电分配方案,不是两步走,而是一步到位。
4. Matlab代码实现:从模型到可运行代码的关键步骤
4.1 代码整体架构与文件规划
拿到题目后我没有立刻开写,而是先盘了一下代码结构。Matlab项目最怕所有变量堆在一个m文件里,跑的时候各种命名冲突和调试地狱。我的经验是函数化、模块化,一个功能一个文件,主程序只做流程控制。
复现这套代码,文件规划大致如下:
| 文件名 | 功能 |
|---|---|
| main.m | 主流程控制,初始化参数,调用求解函数,输出结果 |
| data_para.m | 所有参数的集中定义,包括用户参数、市场电价、套餐初始值 |
| lower_user.m | 下层用户响应求解函数,输入价格,输出各时段用电量 |
| upper_retailer.m | 上层售电商利润函数,输入价格和电量,输出利润值 |
| solve_mp ec.m | 将双层问题转成MPEC后用求解器求解 |
| iterate_solve.m | 迭代求解方案:上层给价格、下层响应、更新价格循环 |
| output_plot.m | 结果可视化与数据导出 |
核心变量我用struct封装,比如data.users里放用户数、参数a和b,data.market里放各市场价格曲线,data.para里放迭代容差、大M值等关键参数。这样一个struct穿梭在各个函数之间,传参方便,后期调整参数也不用到处找。
4.2 参数设置与数据预处理
参数设置是复现的重头戏,也是结果好坏的分水岭。论文原文给的参数往往不够齐全,很多细节需要自己补。我这套代码的关键参数如下:
- 时间尺度:24时段模拟一天
- 用户数:大规模场景设500个用户,小规模测试用100个用户
- 用户弹性参数b:基准值0.2,灵敏度分析时在0.05到0.8之间取值
- 用户偏好参数a:根据基准负荷反推,确保在基准价格下电量处于合理区间
- 套餐类型:固定电价、分时电价、实时电价三类
- 批发市场层级:中长期、日前、实时三个市场
用户参数预处理的逻辑值得多说一句。很多复现卡在“参数对不齐”上——基准价格下算出来的用电量跟论文差得离谱,本质是因为参数a和b设置不匹配。建议用这个方法校准:先设定一个基准价格p0和基准电量q0,然后让需求函数过这个点,反解a = p0 + b * q0。这样你控制了b后,a自然适配基准场景,结果不会跑偏。
市场电价数据默认用高斯分布生成场景集。中长期价格均值取0.45元/kWh,标准差0.02;日前市场均值0.55元/kWh,标准差0.08;实时市场波动最大,均值0.6元/kWh,标准差0.15。标准差越大,反映市场不确定性越强,购电策略在风险成本上的倾向就越保守。
4.3 主循环:上下层迭代求解的实操细节
我实现的代码里面,默认提供两种求解路径:一是直接求解MPEC版本,用YALMIP建模,调Gurobi解MILP;二是双层迭代版本,不依赖商用求解器也能跑通。
先说迭代版本,这个实现思路直观,适合Debug。伪代码结构如下:
% 初始化 price_init = [0.9; 1.0; 0.85]; % 各套餐初始电价 price = price_init; alpha = 0.25; % 阻尼系数,防止价格震荡 tol = 1e-4; max_iter = 100; for k = 1:max_iter % 给定价格,求解下层用户响应 [q_user, ~] = lower_user(price, data); % 根据用户响应电量,求解上层购电优化 [q_market, cost_total, profit_current] = upper_retailer(q_user, data); % 上层还需要同步调整个别套餐价格 % 用梯度方向更新,并用阻尼系数限制步长 new_price = update_price(price, q_user, cost_total, data); % 价格收敛判据 if norm(new_price - price) < tol price = new_price; break; end % 阻尼更新,避免震荡 price = price + alpha * (new_price - price); end这里有个非常实操的细节:阻尼系数alpha一定要加。不加阻尼的时候,价格更新特别容易出现振荡,上一轮价格1.0,下一轮跳到1.3,再下一轮又跌回0.9,来回晃就是不收敛。加了个0.25的阻尼之后,价格步长被压住,稳定性明显提升。
再说MPEC路线的关键代码思路。用YALMIP建模时,先定义连续变量价格p、电量q,再定义0-1变量z来处理互补约束。核心代码结构如下:
% 决策变量 p = sdpvar(3, 1); % 三类套餐价格 q = sdpvar(24, 1); % 24时段用户总用电量 z = binvar(3, 24); % 互补约束线性化辅助0-1变量 % 目标函数:零售收入 - 购电成本 - 风险项 profit = sum(p .* q) - sum(cost_market .* q_market) - risk_cost; % 约束:KKT条件的互补松弛线性化 for i = 1:3 for t = 1:24 Constraints = [Constraints, ...]; % a - b*q_t - p_i <= M1*(1-z(i,t)) % lambda_i >= 0 % ... end end ops = sdpsettings('solver', 'gurobi', 'verbose', 2); optimize(Constraints, -profit, ops);这条路径的优点是精度高,一次求解直接得到最优解,不用像迭代法那样碰运气。缺点是对求解器有依赖,YALMIP必须正确配好Gurobi或CPLEX,而且模型规模上去后求解时间会剧增。
我的体感是这样的:小算例(100用户,24时段,3套套餐)Gurobi几秒内就能解出来;用户规模上到1000,时段扩展到96个点,求解时间会暴涨到几十秒甚至几分钟。如果只是想验证模型思路,用100用户24时段就够了,没有必要一上来就跑大规模。
5. 常见问题与排查技巧实录
5.1 迭代不收敛时的排查方向
迭代求解不收敛,是复现这类博弈模型最折磨人的问题。我整理了一份排查清单,按优先级排列:
- 先看价格是否在两个值之间来回震荡。震荡代表没有阻尼或者阻尼太小,把alpha从0.25降到0.1试试。
- 再看下层用户响应是否有闭式解。如果你用的是数值优化求解下层问题,每次求到的解有微小差异,上层基于这些差异更新价格,很难收敛。
- 检查用户参数b是否过小。b太小意味着用户对价格极度不敏感,需求量基本不随价格变动,博弈问题在数学上接近退化,迭代自然难收敛。
- 排查是否存在多均衡解。可以试试从不同的初始价格出发多次运行,看最终收敛到哪个均衡。如果不同初值收敛到不同结果,说明系统有多均衡,你需要结合具体场景解释哪个均衡更合理。
5.2 求解器选型与性能对比
Matlab环境下的求解器选型,我试过三种路线:
- YALMIP + Gurobi:最推荐。模型建模灵活,求解MILP和MIQP性能强,尤其在非线性约束线性化之后,大规模问题也能应对。
- YALMIP + CPLEX:同样可靠,调用方式几乎一样,区别在于Gurobi在某些二次约束场景下面表现更好。
- Matlab自带quadprog/intlinprog:适合小规模教学演示,不用额外装工具箱。400用户以下跑起来没问题,用户规模一大就力不从心了。
选求解器的核心原则是:先判断问题类型。如果模型全部线性化,是MILP,intlinprog也能凑合;如果保留了二次项,是MIQP,就上Gurobi或CPLEX。不要没搞清问题类型就开始写建模代码,否则后面全是“求解器不支持”的报错。
5.3 参数灵敏度分析中的几个坑
跑灵敏度分析时我踩过一个很经典的坑:对b(用户价格敏感系数)做灵敏度扫描,得到的结果曲线是锯齿状的。
一开始以为是模型写错了,检查了很久才发现,原因是每个b值下模型都存在多个局部最优解,求解器每次都随机收敛到不同的解。解决办法是给求解器设置固定的随机种子,或者从上一个解的热启动值继续搜索,这样相邻的灵敏度点结果连续,曲线就平滑了。
另一个坑是综合购电成本的市场风险权重参数。这个参数在目标函数里控制购电成本波动的惩罚力度。我试过把风险权重从0.1扫描到10,结果发现:权重太小,购电策略倾向于全压中长期市场,风险暴露高但成本低;权重太大,策略变得极度保守,几乎所有电量都在中长期锁定,失去了在日前市场博弈低价的收益空间。这个参数没有“正确答案”,完全取决于售电商的风险偏好水平。
最后分享一个工具层面的小经验:如果你也在复现这类双电优化模型,建议在Matlab代码目录下建一个result文件夹,每次运行自动把价格、电量、利润结果导出为Excel或CSV。我吃过无数次亏——跑了一小时得到一个很不错的解,结果忘了保存,后面调参把代码覆盖了,最优解再也找不回来。数据落地是复现项目的最后一道保险。
这个Topic后续还可以继续深挖的方向也很多,多时段联动定价、多类用户差异化套餐、考虑用户转换成本、结合强化学习的动态定价,每一个都够开一个新题目来做。但前提是你先把这套主从博弈的底子打牢——模型结构吃透了,后面怎么扩展都走得稳。