用Mathematica复现供应链博弈论文:单供应商双零售商模型解析 复现供应链博弈论文最难的不是读公式而是把自己当成作者重新走一遍建模、推导、求解的完整流程。这篇博文要聊的就是一个典型的单供应商双零售商模型用Mathematica从零求解的全过程包括建模思路、符号推导、数值验证、踩坑记录和扩展方向。文章会按实际复现的顺序展开把每一步的思路和代码都摊开来讲适合正在写供应链运营、博弈论相关论文或者想用Mathematica做优化求解的研究生和科研人员参考。1. 模型拆解单供应商双零售商到底在解什么问题1.1 这个模型为什么值得复现供应链里一个供应商、两个零售商的结构非常经典它介于最简单的单链和复杂的多级网络之间。既有纵向的上下游博弈关系又有横向的零售商竞争关系恰恰是研究渠道冲突和横向竞争的最小结构单元。论文里最常见的就是构建一个Stackelberg博弈供应商作为领导者先行动两个零售商作为跟随者同时行动然后通过逆向归纳求解均衡解。复现这类论文的价值在于作者在发表时通常会把复杂的推导步骤大量省略只给关键结论。期刊审稿人往往只关心结果是否正确但如果你要真正把模型用起来就必须把每一步推导重新走一遍。用Mathematica做这件事特别合适因为它的符号计算能力可以直接复现论文里的解析推导又能在参数复杂时无缝切换数值求解。1.2 基本假设与符号约定复现之前第一件事是建立一张完全属于自己的符号表。论文里的符号体系往往很混乱同一个变量在不同章节可能用了不同的下标。我一定要先做符号映射把论文里的符号翻译成Mathematica里清晰的变量名。以我复现的那个模型为例核心假设如下一个供应商以单位成本c生产单一产品以批发价w销售给两个零售商零售商ii1,2以零售价p_i销售产品面临的需求函数为线性形式两个零售商的销售价格会互相影响体现竞争关系供应商是Stackelberg领导者先制定批发价w两个零售商观察到w之后同时制定零售价格p_1和p_2。这个模型的核心目标是求解供应商的最优批发价w_opt、两个零售商的最优零售价p_1_opt和p_2_opt进而计算各方的均衡利润。这里有个容易忽略的地方线性需求函数是最常被选择的设定主要原因在于它能同时保证利润函数凹性、反应函数线性从而可以解析求解。论文一旦改用非线性需求函数解析解往往就做不出来了那时候就只能靠数值模拟。1.3 关键参数的经济学含义在参数设计上不同的论文处理方式不同。有的是对称设定即两个零售商的需求函数完全相同这种设定下均衡解具有对称性p_1p_2计算量大幅降低。有的是非对称设定比如一个零售商品牌知名度更高或者一个零售商退货率更低这种设定下需求函数的截距项或价格敏感系数不同求解难度和结论也没有对称情形那么漂亮。我复现论文时最关注的一个参数是交叉价格敏感系数记为θ它刻画了两个零售商之间的竞争强度。θ0意味着完全没有竞争两个零售商各自垄断自己的顾客群θ0说明一个零售商降价会抢夺另一个零售商的需求θ越大横向竞争越激烈。这个参数直接决定了供应链中双重边际效应的严重程度也是论文里最常做敏感性分析的对象。2. Mathematica求解路线符号推导与数值验证双通道2.1 为什么选Mathematica而不是MATLAB或Python在复现这类供应链博弈论文时工具选择很关键。MATLAB和Python的强项是数值计算做参数扫描、画图很顺手但面对解析推导必须手动完成或者依赖SymPy这类符号库写起来很别扭符号化简能力也不够强。Mathematica的核心优势有两个。第一是符号求导和方程求解几乎是一行命令的事D[]、Solve[]、Reduce[]这些内置函数可以直接处理抽象符号表达式而且化简能力特别强论文里那种冗长的表达式展开后往往能被Simplify[]压成很紧凑的形式。第二是数值优化函数的鲁棒性NMaximize[]、FindRoot[]可以直接接符号表达式方便在解析推不出来的时候快速跑一个数值版本对照。最实用的一个模式是先用符号推导确定模型结构和理论解一旦发现解析解过于复杂就退到数值求解同时用多个参数组合反复验证数值解的稳定性。这种双通道模式是我这次复现里最核心的工作方法。2.2 逆向归纳求解的标准操作流程Stackelberg博弈的通用解法是逆向归纳也就是从最后行动的局中人开始往前倒推。在这个模型里最后行动的是两个零售商所以第一步是给定供应商的批发价w求解两个零售商的定价均衡第二步把零售商的均衡反应函数代回供应商的利润函数再求解供应商的最优批发价。第一步的核心代码框架如下需求函数以简化的线性形式为例(* 零售商i的利润 *) retailerProfit[i_] : (p[i] - w) * (a[i] - b[i]*p[i] θ*(p[3-i] - p[i])); (* 一阶条件对p[i]求偏导 *) foc Table[D[retailerProfit[i], p[i]] 0, {i, 1, 2}]; (* 求解两个零售商的定价反应函数 *) reaction Solve[foc, {p[1], p[2]}] // FullSimplify;这里有个陷阱常规的线性需求可写成q_i a_i - b_ip_i dp_j但如果写成a_i - b_ip_i θ(p_j - p_i)含义就变成了交叉价格差带来的需求转移。两种形式求出来的反应函数结构不同代入供应商利润后得到的最优批发价也不一样。复现论文第一步就吃透原作的设定形式千万不要想当然。2.3 凹性验证求导之前先确认有极值可求很多初学的人在逆向归纳里犯的最隐蔽错误是根本不验证目标函数的凹凸性就直接求导、解一阶条件。一阶条件只是极值点的必要条件如果函数不是凹的对最大化问题来说解出来的点可能是极小值也可能是鞍点。在这类模型里零售商的利润函数通常是关于自身零售价格的凹函数因为需求随自身价格上升而线性下降利润函数中-p_i^2的项带来负的二阶导。但两个零售商同时博弈时不能只看单个零售商的凹性还要确认反应函数的交点是一个稳定的纳什均衡。用Mathematica验证凹性的通用做法是检查二阶导数是否处处小于零(* 检查零售商利润函数的二阶条件 *) D[retailerProfit[1], {p[1], 2}] // FullSimplify代入参数数值后二阶导必须是负数。这一步骤看起来很基础但它决定了整个逆向归纳的有效性值得在代码里留一栏专门输出。3. 完整复现实操从纸面推导到最终图表3.1 手推和代码推导同步走我个人的习惯是拿到一篇论文的模型部分先在纸上把核心推导自己走一遍不借助任何工具。这么做不是为了显示手算能力而是为了在符号推导时建立直觉。论文里那些化简步骤如果自己手推一遍往往就能发现作者跳过的关键技巧比如某一步用到了隐函数求导某一步用到了对称性假设。手推完成后再用Mathematica把同样的过程复现一遍。这个时候代码的作用是双重验证既验证手推结果也验证原论文的结论。在我复现的那篇论文里手推时发现了原文一处符号错误。原文在推导供应商利润时把零售商反应函数代入后丢了一项虽然不影响最终均衡解的定性结论但最优批发价的表达式对不上。这是一个典型收获——复现论文经常能发现原作里的笔误或排版错误。3.2 对称情形与非对称情形的分别处理当两个零售商的需求参数完全对称时即a_1a_2、b_1b_2逆向归纳的求解可以大幅简化。由于两个零售商的利润函数结构完全相同反应函数也对称均衡解必然满足p_1p_2。对称假设下可以直接设p[1]p[2]p把两个一阶条件压缩成一个求解量减少一半。这种简化对调试代码特别有用因为可以先在对称情形下验证代码的正确性再逐步放开对称性限制。非对称情形就麻烦多了。两个零售商需求函数的截距、价格敏感系数、竞争强度都不同时p_1和p_2的解析表达式会变得极其冗长可能占据好几页。Mathematica的Simplify[]面对这种表达式也经常无能为力。此时就要转换策略锁定一组基准参数比如a_1100、a_290、b_1b_21、θ0.5、c10、w30先用数值方法求解定价均衡再代入供应商利润做数值优化最后用表格扫描参数看均衡解的变化趋势。3.3 核心代码与运算过程下面这套代码是我在非对称情形下实际跑通的求解流程包含了供应商利润的构建和最优批发价的数值求解(* 参数设定 *) params {a[1] - 100, a[2] - 85, b[1] - 1, b[2] - 1.2, θ - 0.5, c - 10}; (* 零售商利润函数 *) retailerProfit[i_, w_] : (p[i] - w) * (a[i] - b[i]*p[i] θ*(p[3-i] - p[i])); (* 给定w联立求解两个零售商的一阶条件 *) pOpt[w_] : Module[{eqs, sol}, eqs Table[D[retailerProfit[i, w], p[i]] 0, {i, 1, 2}]; sol NSolve[eqs /. params, {p[1], p[2]}]; (* 筛选出零售价高于批发价的合理解 *) First[Select[sol, (p[1]/.# w p[2]/.# w) ]] ]; (* 供应商利润批发价减去成本再乘以总量 *) supplierProfit[w_] : (w - c) * ((a[1] - b[1]*p[1] θ*(p[2]-p[1])) (a[2] - b[2]*p[2] θ*(p[1]-p[2]))) /. params /. pOpt[w]; (* 数值求解最优批发价 *) wStar w /. Last[NMaximize[{supplierProfit[w], w c}, w]]这套代码里有几个值得注意的地方。第一在NSolve返回的两个解里通过约束条件p[i] w筛掉了无经济意义的解。零售价低于批发价意味着零售商倒贴钱卖货这在经济学上不合理但在代数方程里它确实是一个数学解。不筛选就直接代入供应商利润后续算出来的均衡往往荒谬。第二供应商利润函数里用了需求函数的具体表达式而不是直接把零售商的最优订购量当作变量。这里要特别小心因为在Stackelberg博弈中供应商无法直接决定零售量只能通过w间接影响。如果直接在供应商利润里写总量等于零售商均衡订购量之和就需要先把零售商的最优反应代入这正是前面pOpt[w]做的事。第三NMaximize要求目标函数在给定w时能瞬间求出数值所以pOpt[w]必须写成Module形式的函数保证每次调用都重新求一次零售商的定价均衡。这个细节如果漏了结果就是w不变时供应商利润也是错误的。3.4 结果验证与论文图表对比求解完成后最关键的一步是把结果和原论文的图表进行比对。我通常做三件事输表格、画曲线图、检查边界条件。表格方面我输出w、p_1、p_2、供应商利润、两个零售商利润这几个核心数值和论文里给出的数值算例逐一对照。画图方面用ParametricPlot或Plot把均衡结果随参数变化的曲线画出来比如w随θ变化的曲线或者p_i随a_2变化的曲线看形状是否与论文一致。有个极其重要的对照标准无论参数怎么变化均衡批发价必须恒大于供应商的单位成本c否则供应商亏本模型无意义零售价必须恒大于批发价否则零售商亏本。这些约束看起来简单却是判断求解结果是否有效的第一道关卡。我第一次复现时画出来的零售价随θ变化的曲线在θ1.2的区间出现了价格上限失效的情况p_2一度低于w。排查了半天发现问题出在需求参数设置上我用的参数并没有保证需求函数的每个系数在全部参数区间内都为正当θ过大时需求函数本身都可能变成负值。后来通过给θ加上可行域约束这个问题才解决。4. 常见问题与排查技巧实录4.1 符号推导不出解析解的应对策略这是复现论文时最常撞的一堵墙。原文可能给了解析表达式但你在复现时发现Solve或Reduce根本跑不出结果或者算出一个极其冗长、无法化简的表达式。遇到这个情况我的处理顺序是先确认模型设定和原文完全一致再看是否遗漏了某个隐含的对称性假设。很多时候论文是在模型对称的前提下给的解析解你却在非对称设定下求解自然推不出那么简洁的结果。如果模型设定确实一致那就果断转数值求解用NSolve替换Solve用NMaximize替换Maximize并验证多个参数组合的稳定性。数值解的缺点是没法直接分析单调性和敏感性但可以用参数扫描来弥补。比如把θ从0.1以0.1为步长扫到1.0记录每个θ下的均衡解然后看结果的变化趋势照样能得出经济学结论。4.2 最隐蔽的坑一阶条件求导对象错误在Stackelberg博弈里供应商先动、零售商后动这个博弈时序决定了逆向归纳时求导对象必须严格区分。求解零售商定价时对p[i]求导w是外生参数求解供应商批发价时对w求导p[i]已经被替换成w的反应函数。实操中很容易犯的错误是在供应商利润里没有先把p[i]替换成pOpt[w]就求导结果把p[i]当成与w无关的常数来处理求出来的最优w实际上不是均衡解。这种错误在数值上表现为利润曲面没有极值或者极值出现在边界上。排查方法很简单打印不同w下的供应商利润如果利润函数关于w是单调的就说明零售商反应函数没代入对。4.3 筛选多重解的标准与原则非线性方程组经常给出多个解哪个才是我们想要的均衡解除了p_i w这个硬约束还要关注解的稳定性。在古诺和伯川德博弈里通常要求反应函数满足稳定条件比如当θ相对于b_i足够小时均衡才稳定。如果θ太大两个零售商的反应函数相交处的均衡可能是不稳定的这在现实中的含义是竞争太激烈会导致价格战均衡无法维持。实操上我会在筛选解时加一个可配置的过滤条件列表把p[i] w、需求q[i] 0、利润为正这三项全部作为筛选条件。这三个条件不能写成硬编码因为不同参数下剩余解的数量可能不同代码要能输出所有满足条件的解再由人判断哪个是经济意义上的均衡。4.4 常见问题速查表问题Solve返回空集或无法找到解。排查方向检查模型设定是否与原文一致检查变量是否冲突检查是否有隐含假设未加入比如p[1]p[2]的对称性假设。问题求解结果不满足经济含义。排查方向检查是否筛选了合理的解需求函数是否在参数区间内保持为正零售价是否高于批发价。问题NMaximize结果不稳定每次运行结果不同。排查方向优化函数可能存在多个局部极值改用不同初始点或用FindRoot解一阶条件验证。问题数值解与论文图表对不上。排查方向先核对符号表再看参数单位是否一致最后检查论文图表的横纵轴含义有时论文画的不是均衡值本身而是相对偏差或比率。问题Mathematica求导结果包含复杂条件表达式。排查方向给变量加上假设条件用Assuming[]包裹或者用FullSimplify配合复杂度函数强制化简。5. 从复现到扩展这个模型还能怎么玩复现一篇论文的终点不应该停在与原文结果一致。一旦代码跑通模型就成了你的工具这时候可以往前多走几步。在这套单供应商双零售商模型里我实际尝试过三个方向的扩展。第一个扩展是改需求函数形式。把线性需求改成乘法需求或指数需求观察最优解结构的变化。结果是解析推导几乎不可行但数值解可以稳定地跑出来。这个扩展对写论文特别有价值因为审稿人经常质疑线性需求的假设太强如果能在非线性设定下验证核心结论仍然稳健论文的理论说服力会明显增强。第二个扩展是引入不对称的博弈时序。原模型假设零售商同时行动实际上可以改成零售商1先定价、零售商2后定价的序贯博弈。这时候求解顺序会变成零售商2在观测到p_1后做最优反应零售商1在预见到这层反应后定价最终形成一个三阶段的Stackelberg博弈。Mathematica处理这种多阶段博弈很方便只要在逆向归纳时多包一层函数嵌套即可。第三个扩展是在需求函数里加入随机扰动项。这个扩展会让模型从确定性问题变成随机优化问题求解思路发生根本变化。零售商的最优定价取决于需求分布的期望供应商的决策取决于零售商反应函数的期望。由于期望运算和求导可以交换顺序线性需求下仍然可以得到解析解。如果再用数值模拟加蒙特卡洛来做随机性验证整个模型的说服力就上了一个台阶。这三个扩展方向在实际研究里都非常常见复现者可以根据自己课题的需要灵活选用。复现论文本身是第一步能把模型改造成自己的工具才算真正吃透。6. 复现论文的经验提炼最后把我这轮复现里最有价值的几条实操经验集中说清楚。一是永远先建立符号对照表。不要看着论文里的公式直接敲代码一定要先把原文符号映射成自己熟悉的变量名并写清楚含义、单位、取值范围。这个工作表看起来繁琐但能避免大量后期调试。二是调试优先级先数值后符号。我见过很多人一上来就跑符号求解等半天解不出来才心慌。高效的做法是先给一组基准参数用数值求解跑通全流程确认模型逻辑没毛病再回头做符号求解和化简。符号解的价值在于可以分析单调性、验证理论性质但如果求不出来数值解已经足以支撑很多结论。三是画图永远是调试利器。任何时候对结果不确定就把目标函数或最优解随参数变化的曲线画出来。曲线形状异常往往意味着代码里有隐藏bug。比如利润曲线突然出现折点大概率是筛选条件在某个参数值附近把有效解排除了这时候就需要回看筛选条件是否合理。四是不要迷信论文的结论。复现过程中如果发现和原文不一致先别急着怀疑自己逐项核对符号、参数、约束条件。我在这次复现中确实发现原论文有一处表达式符号错误是通过代入参数数值验证、对比利润曲线后确认的。这个发现本身就是一种有价值的产出写进综述或方法学笔记里完全没有问题。五是善用Mathematica的Documentation和Stack Exchange。很多人嫌查文档浪费时间实际上Mathematica的官方文档里每个函数页面都有大量可复制的例子遇到语法问题先查文档比盲目试错快得多。卡关超过二十分钟就去搜Stack Exchange用英文描述问题关键词基本都能找到现成答案。复现论文这件事看着是在重复别人的工作实际上是在训练自己从零构建模型的能力。下一次遇到新的供应链博弈问题你手里就有一套经过实战检验的建模、求解、验证、扩展流程这比单纯读十篇论文都有用。