ARTICLE DETAIL

资讯详情

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

售电商主从博弈套餐设计:多级市场时序与Matlab实现要点

售电商主从博弈套餐设计:多级市场时序与Matlab实现要点 这篇博文我打算从“复现者在论文之外最需要知道的几件事”切入。市面上的EI论文讲解大多只停留在模型公式和结论真正决定成败的是博弈结构怎么搭、多级市场时序怎么对齐、Matlab里迭代求解怎么稳住。这篇就直接围绕这些展开把我复现过程中的思路、取舍和踩坑都摊开讲。1. 为什么售电商的套餐设计必须引入主从博弈先说一个很多刚接触这个方向的人都会问的问题零售套餐的定价直接做优化不就行了吗为什么非得上主从博弈我最初也有这个疑惑直到我真正把用户侧的响应行为加进模型之后才明白单纯优化和博弈建模之间的本质区别。售电商这个角色很特殊它夹在两头中间。一头是批发侧的电力市场不管是中长期合约、日前市场还是实时平衡市场购电价格基本不受单个售电商控制它是市场出清的结果另一头是零售侧的用户他们手里的选择权反而越来越大——分时电价、阶梯套餐、定制化方案越来越普及用户会根据电价调整自己的用电行为。也就是说售电商的收益不是由一个决策问题决定的而是由“售电商定价”和“用户响应”两个决策主体之间的交互共同决定的。这种“你先出价、我后响应”的结构天然就是主从博弈Stackelberg Game的标准应用场景。售电商是领导者Leader它先制定零售套餐和价格用户是跟随者Follower在给定套餐下做出自己的用电决策。这里的关键在于售电商在做决策时不能假设用户需求是固定不变的必须把用户的理性响应纳入自己的优化里。这跟传统的单层优化模型有本质区别——单层优化里需求曲线是外生给定的主从博弈里它变成了内生变量。我在建模时把这个问题抽象成了三个要素。参与者方面是售电商-用户两级策略空间方面是售电商的套餐定价变量用户的用电量调整变量收益函数方面是售电商的利润函数用户的用电效用函数。这套抽象看起来简单但后续所有Matlab实现都是围绕这个框架展开的。如果你在复现时发现自己写的模型怎么也说不通“用户为什么会在这个套餐下选择这个用电量”那大概率是博弈层级没有理清楚。2. 多级市场购电时序与决策变量设计先买还是后买直接影响套餐定价售电商的购电策略不是一次性行为。电力市场是分时间尺度出清的这段时间上的配合关系恰恰是套餐设计能否落地的关键约束。完整的购电链路通常是这样的。第一层是中长期合约市场交易周期以月、季度甚至年为尺度价格相对稳定作用是锁定基础电量、规避现货价格波动风险。第二层是日前市场提前一天申报第二天的购电曲线市场按小时或更细粒度统一出清价格随时间变化。第三层是实时平衡市场在实际运行前很短的时间内出清用于平衡预测误差和突发情况价格波动最剧烈。售电商的整体购电成本就是这三层采购之和。我不建议一上来就把三层全部建模进去否则你的Matlab程序会因为维度爆炸而很难调试。更合理的做法是先做两层——中长期合约加日前市场先把主从博弈的逻辑跑通再扩展实时层。在我复现的这篇论文里零售套餐包含两个维度一个是分时电价套餐不同时段价格不同另一个是阶梯用电套餐用电量超过某个阈值后价格进入下一个阶梯。这两个维度各自有对应的用户响应机制。分时价格刺激用户在时间维度上转移用电阶梯价格刺激用户控制总用电量。有意思的是两种套餐同时存在时售电商的决策变量不是简单地叠加而是要考虑它们之间的交互——比如分时价格如果定得太高用户可能直接缩减总用电量而这一步缩减会同时影响阶梯收入。决策变量因此分成了两层。上层变量是售电商的套餐价格向量以及各市场购电量分配下层变量是各类用户的用电量包括分时用电量和总用电量的调整。这个分层结构我建议在写代码之前就用矩阵维度把它固定下来。比如套餐有24个时段的电价再加上3个阶梯阈值和3个阶梯价格上层变量大概是30维左右用户群体按典型用电模式聚成若干类每类用户在24个时段都有决策变量下层变量就是24乘以用户类别数。在程序里我通常用一个结构体数组来管理这些变量而不是一股脑塞进一个大向量里。这样后面做灵敏度分析和结果可视化时取数据会特别方便。预处理阶段先把时段、用户类别、套餐档位这些索引全部建立好后面所有矩阵运算都用索引对齐就不会出现“数据维度对不上”这种低级错误。3. 主从博弈模型的核心拆解上层利润与下层效用的相互作用模型这块是整个复现最需要花时间吃透的部分。很多人Matlab代码写得溜但模型一复杂就开始蒙根本原因是没有把上下层各自的优化逻辑和它们之间的耦合关系讲清楚。上层的售电商利润函数我拆成了四个部分。第一是零售收入也就是向用户卖电的收入第二是中长期市场购电成本第三是日前市场购电成本第四是实时市场平衡成本或收益。其中零售收入是套餐价格乘以用户用电量的乘积而这个用电量恰恰是下层用户的决策结果这就构成了上下层之间的第一层耦合。零售收入的核心在于套餐价格越高单位利润越大但用户会减少用电导致售电量下降价格低则反之。这个此消彼长的关系决定了上层优化不是简单求极值而是找一个均衡点。下层的用户决策我采用的是效用最大化框架。每类用户有一个用电效用函数这个函数通常是凹函数意味着用户不会无限增加用电量边际效用是递减的。给定售电商的套餐价格之后用户会选择一个用电量水平使得用电效用减去电费支出的净收益最大化。这个净收益最大化问题的一阶条件很关键。用户在某个时段的用电决策满足“边际效用等于该时段的边际电价”这个条件。对于阶梯套餐情况复杂一些因为总用电量跨越阶梯阈值时边际成本会突变。这时候需要用分段线性函数来处理电费函数并引入0-1变量来标识用户落在哪个阶梯区间。上下层的交互过程在数值上可以这样理解给定上层价格下层用户在Matlab里通过求解一个带约束的二次规划就能得到最优用电量把用户的最优响应返回给上层上层通过迭代调整价格直到双方的策略不再变化——这个不动点就是Stackelberg均衡。求解方法上文献里常见的有两类。一类是KKT条件转换法把下层问题用KKT条件替换进上层问题再把互补松弛条件用大M法或罚函数法线性化最后整体变成一个单层的混合整数规划。另一类是迭代求解法比如差分进化、粒子群加下层规划嵌套求解每一轮迭代都重新计算用户的最优响应。我复现时首选了KKT转换路线因为它能借助CPLEX这类商业求解器收敛稳定性远好于启发式迭代。不过KKT路线有个前提就是下层的用户问题必须是凸优化问题。这一点在论文里通常会满足——效用函数取二次凹函数约束是线性的。但如果你的套餐模型引入了非线性阶梯结构就需要先做线性化处理否则KKT条件的强对偶性不成立模型结果会有偏差。4. Matlab实现的关键模块与数据结构设计代码架构这件事我是在经历了一次彻底推翻重写之后才想明白的。第一次写的时候我图省事把所有代码堆在一个脚本里结果光是对着输出结果查公式就查了三天。后来老老实实拆成模块每个模块负责一件事调试效率完全不一样。我建议你按这样的模块拆分来实现。数据定义模块集中定义所有常量、参数、时段集合、用户类别。比如用户数、分时时段数、阶梯档位数量、市场参数、负荷基准值。所有标幺值换算在这里统一完成后面所有模块引用同一个参数结构体。上层模型构建模块负责构建售电商利润表达式定义套餐价格变量的上下界、市场购电量的平衡约束。这里要特别注意购电量平衡约束要写成等式的形式即总购电量等于总售电量加上网损否则模型会出现电力不平衡结果明显失真。下层模型构建模块负责定义每类用户的用电效用函数和电费支付函数生成用户侧约束。如果是用YALMIP建模求解这里就是把约束和目标函数二元组表达出来的地方。KKT拼接与求解模块把下层问题的KKT条件通过YALMIP写入上层优化问题同时把互补松弛条件用大M法或逻辑约束改写最后指派CPLEX/Gurobi求解。这个模块是整套代码的灵魂也是出错率最高的地方。结果解析模块把求解结果从求解器返回的变量里提取出来还原成套餐价格、各时段购电量、用户响应电量等物理量并做数据检验。我实际操作时用的是YALMIP加CPLEX的组合。YALMIP的好处是建模层非常友好尤其适合快速验证模型逻辑CPLEX负责处理后半部分的混合整数线性规划求解。如果你用的是Matlab R2023b或更高版本需要注意CPLEX和YALMIP的版本兼容性建议用CPLEX 12.10以上版本亲测跟Matlab 2022b以上的适配问题较少。底层数据结构上我强烈建议以“类”的思维来组织代码——不一定要真的用Matlab的classdef但要把相关的变量集合成结构体。比如params结构体放所有系统参数user_data结构体放用户用电基线、效用系数、弹性参数market_data放各市场价格曲线。这样做的最大好处是当你需要跑上百组参数做灵敏度分析时只要循环里改对应结构体的字段即可不会出现全局变量满天飞的恐怖局面。求解配置方面有几个坑值得提前说。第一MIPGap不要默认我一般设到0.01%到0.1%之间。太松会导致结果波动大太紧会让复杂算例算到天荒地老。第二互补松弛条件的大M值要根据电价和用电量的量纲来标定我在一组典型参数下用M1e6得到了稳定结果但换了一组数据后就出现了数值病态后来改成分段标定才解决。第三求解器输出的dual variable处理要小心YALMIP里获取对偶变量的方式有特定语法写错会静默给错误结果最好打印出来手动核验一两个约束的互补性。5. 多级市场购电策略如何与零售套餐联动前面讲的模型结构里购电策略和零售套餐是两个独立的决策块但实际上它们必须联动优化。这是我复现过程中反复体会到的。先说为什么必须联动。梯级套餐的定价上限理论上可以定得很高但如果过高用户用电量下降总购电量减少中长期市场锁定的大电量就变成负担——你签了合约就必须履约用不完就要在实时市场贱卖反而是亏损。反过来如果零售价格定得过低虽然能刺激用户用电但购电侧成本可能超过零售收入依然会亏损。所以套餐价格和购电比例本质上是同一个决策坐标系下的两个维度。我在模型里把这种联动通过购电量平衡约束和套利激励约束来体现。购电量平衡约束要求总购电量严格等于所有用户的销售电量之和这个约束在KKT拼接后依然要精确满足套利激励约束则是把中长期市场与日前市场的价差信号传导到零售套餐定价里——比如日前市场高峰时段预测价格很高那么零售分时套餐的高峰时段价格就应该相应上浮但这个上浮幅度不是直接复制日前价格而是经过用户响应弹性折算后的值。这里有一个很有操作感的细节市场价差矩阵的设计。我在数据处理阶段把中长期市场的分时段价格、日前市场的价格预测、实时市场的历史波动区间整理成三个矩阵然后计算各层之间的期望价差与波动率。主从博弈模型里的购电决策实际上就是在这三个矩阵的约束下动态分配购电比例。如果价差为正且稳定就倾向于增加中长期锁定比例反之则增加现货比例。这个逻辑看起来像常识但在建模时要把它转化为数学表达式不能拍脑袋定比例。联动求解时我遇到过一个问题用户响应后的用电量曲线和市场价格的峰值时段发生错位。比如用户把空调负荷转移到夜间谷时段导致谷时段购电量上升、峰时段购电量下降整体购电成本反而优化了。但如果谷时段的现货价格并没有预期那么低这种转移就会导致成本恶化。这说明套餐定价必须放在真实的购电成本曲线上来计算不能脱离市场数据单独设计。我这边的做法是在主循环里先给定一组初始套餐价格求解购电策略然后根据购电侧的边际成本曲线重新调整套餐价格再次求解用户响应如此迭代。这种迭代机制在主从博弈的框架下是可以收敛到均衡的但它对价格调整步长很敏感。步长太大振荡步长太小收敛慢。我最终用了一个自适应步长前后两轮的套餐价格差超过阈值时缩短步长稳定时拉大步长效果不错。如果你直接套用论文里的参数可能一次就收敛了。但换了实际数据比如某个省份的真实负荷和市场价格就必须处理量纲和标幺化问题。我的建议是所有价格型参数统一标幺后再进入模型基值选为基准时段的平均购电成本。这样既避免数值病态又方便对结果做直观解读。6. 复现过程中最典型的三个坑与排查链路代码跑不出预期结果是复现论文的日常。这里我挑三个我在这个项目里真正踩过的坑每个都附带完整的排查链路希望你能少走几周弯路。坑一下层用户问题的KKT条件对偶变量方向反了。这是一个非常隐蔽的错误。下层问题是用户最大化净效用约束条件是“用电量大于等于0”和“阶梯用电量限制”。当我手动推导KKT条件并写入YALMIP时把不等式约束的对偶变量符号写反了。表面上看目标函数值变化不大但套餐价格的解严重偏离论文结论。排查链路是这样的我先打印所有约束的互补松弛残差发现有一个约束对偶变量始终为零但约束取等号于是回溯到该约束的一阶条件重新推了一遍符号才发现是拉格朗日乘子的符号约定问题。这个坑提醒我写入求解器之前先用手算一个两时段两用户的小算例验证对偶变量比直接跑完整算例再猜错因靠谱得多。坑二互补松弛条件的大M法数值病态。在用大M法处理互补条件时M值太大CPLEX会报告numerical troubleM值太小某些本应成立的互补条件被错误割断。我在一组参数下运行正常换一组就出现“整数无解”。排查后发现问题出在M值与市场电价量纲不匹配。我的做法是将M值设为电价量纲的100倍左右同时使用分段线性约束而非单一的大M表达式。如果你用的是YALMIP可以考虑用implies来写逻辑约束让求解器内部处理这个大M比手动指定更鲁棒。坑三多级市场购电量的比例约束写成了不等式。这个错误更基础但危害很大。购电比例约束如果写成小于等于模型会“聪明地”只买最低价市场的电导致零售套餐价格异常。我排查时发现各市场购电量之和小于总售电量那么多余电量凭空消失模型当然失真。确认约束类型后我把所有市场的购电量总和改成等式约束并加了各市场购电量占比的上下限约束整个模型才回到正轨。这里要提醒的是等式约束对KKT转换影响很大必须确保它们在拼接后依然严格成立否则强对偶条件会被破坏。除了这三个坑还有一类问题是结果数值合理但物理上荒谬。比如某个时段的用户响应电量是负值。这类问题基本都出在用户效用函数参数设置上——二次函数的一次项系数太低导致边际效用长期低于电价用户的最优用电量变成零甚至负值。处理方式很简单对用户用电量加一个宽松的下限约束并检查效用参数是否符合物理直觉。复现这类论文我的总体经验是不要奢望一次性把完整代码写完再调试那是效率最低的方式。先把模型降到“单层优化固定用户需求”验证购电策略模块的正确性再引入用户响应最后叠加主从博弈的KKT转换。每加一层都保留上一层的输出结果做对照。这样出问题时你能立刻定位到是哪一层引入的错误。7. 一个实用的小技巧用灵敏度分析验证博弈均衡的唯一性主从博弈模型有一个绕不开的理论问题——均衡是否唯一。很多论文默认存在唯一均衡但实际模型里由于套餐变量的非线性耦合可能出现多均衡或者无均衡的情况。这时候直接在代码层面验证就非常重要。一个我跟同行交流后反复使用的方法是对关键套餐价格变量做网格扫描。具体操作是把某个时段的零售电价从下限到上限均匀取20个点对每个点固定这个变量求解完整的KKT拼接模型看对应时段的目标函数值和用户响应电量变化是否平滑。如果出现跳跃或不连续说明模型存在多个局部均衡点需要检查模型线性化是否引入伪解。另一个更直接的做法是改变初始值。用两组差异较大的初始价格向量分别启动迭代求解对比最终收敛的均衡点是否一致。如果一致大概率是唯一均衡如果不一致模型就可能存在多均衡这时需要在论文里说明选取哪个均衡的准则比如选择社会总福利最大的均衡。我在这个项目的实际算例中稳定性结果是让人满意的——不同初始值最终都收敛到了同一组零售套餐价格和购电分配方案。这很大程度上得益于下层问题严格的凸性以及上层目标函数在有效约束区间内的拟凹性。如果你在复现时遇到多个均衡先回去检查套餐模型的线性化处理是否引入了非凸分段这是最常见的原因。另外灵敏度分析本身也是论文里很好的补充素材。把几个核心参数——比如中长期的锁定比例、用户的价格弹性系数、阶梯阈值——逐一做变化观察零售套餐价格和购电策略的响应方向如果能跟经济学直觉对得上那模型的可靠性就更有说服力。写论文或做项目汇报时这种分析图是审稿人和领导都很买账的。8. 从代码实现到结果复现最后的验证闭环代码全部跑通之后还有一个必须做的环节就是把模型输出的数值还原成物理量验证它们是否满足所有约束并且与论文的算例结果规律一致。这一步被很多人忽略但它恰恰是“复现”两个字的意义所在。我习惯在结果解析模块里加一个自动校验函数逐项检查以下内容第一所有时段的购电量之和是否等于销售电量之和误差允许在1e-8之内第二每个用户的最优用电量是否在允许区间内且满足阶梯套餐的0-1变量约束第三KKT残差是否足够小这可以通过“用户边际效用减去边际电价”来衡量第四售电商的利润是否大于零如果利润为负即使模型收敛参数设置也一定有问题。校验通过之后再关注结果规律与论文是否一致。比如分时套餐价格的峰谷比通常应该大于日前市场价格的峰谷比这反映了售电商对用户响应不确定性的风险溢价。如果这个比例严重偏离论文值可能是用户弹性参数设置不合理。我在复现时论文里的用户弹性参数是0.3到0.5之间我在实际参数标定时取了0.4得到的峰谷比与论文结果在一个量级差异主要来自负荷基线数据不同这属于正常范围。最后把这个闭环做成一个标准化的主运行脚本输入是市场和用户参数结构体输出是结果结构体和校验报告。这样不管以后换什么数据集都能一套流程跑完也方便在同一个框架下扩展新的套餐类型或多级市场成员。到了这个时候整个复现工作才算真正闭环。我自己的体会是复现论文本质上不是比谁的代码高级而是比谁把每个环节的“为什么”想得更透。模型结构、数据、代码三者对齐结果自然就出来了。
返回列表