用Mathematica复现竞争零售商渠道策略博弈:从符号推导到均衡区域图
我接触这个项目的时候刚读完一篇讲竞争零售商渠道策略的中文论文模型不复杂但手推公式花了我整整两天算完还怀疑自己是不是算错了。后来咬咬牙把整套模型搬进 Mathematica从需求函数、利润函数到均衡价格、渠道结构比较全部用符号推导加数值实验跑了一遍效果意外地好。这篇博文就记录一下我完整的复现思路、核心代码逻辑和踩过的坑希望给同样在跟博弈论渠道论文搏斗的朋友一点参考。先交代一下这个复现项目的背景方便你判断这篇文章对你有没有用。我们要复现的论文简单说就是研究两个相互竞争的零售商在传统实体渠道、线上自营渠道、线上线下双渠道这几种策略里最终会选择哪种渠道结构作为博弈均衡。论文里的核心问题是渠道竞争强度、消费者对线上渠道的接受度、以及零售商之间互相抢客户的程度会怎样影响均衡结果。这个工作适合两类人参考一类是正在做供应链管理、渠道策略相关课题的研究生想快速验证文献里的结论另一类是已经在用 Mathematica 做经济学、管理学模型推导的研究者想看看符号计算在博弈论里能省多少事。如果你还停留在手推公式、再画图的阶段这篇文章能帮你把整个流程打通。1. 复现前的准备先把论文模型拆成可计算的模块1.1 论文到底在研究什么模型背景与问题拆解很多刚接触渠道策略论文的人拿到文章就急着找代码结果发现论文里根本没有代码只有密密麻麻的公式推导。我在复现前做的第一件事不是打开 Mathematica而是把论文的模型假设、变量定义和博弈时序全部拆成一个个可以独立计算的小模块。具体来说这篇论文的模型框架大致是这样的有两个竞争零售商记为零售商1和零售商2分别销售同质或近似同质的商品。每个零售商可以选择两种基本渠道传统线下渠道记为 R和线上渠道记为 E也可以同时选择两种渠道双渠道记为 D。消费者对线下渠道有既有的购物习惯对线上渠道有一个“接受程度”参数一般记为 θ取值范围在0到1之间。θ 越接近1说明消费者越愿意在线上购买。渠道之间有替代性比如消费者本来想去线下店但如果线上价格便宜就会转向线上下单这个替代力度直接影响两个零售商的价格战强度。两个零售商同时决策渠道结构然后在给定的渠道结构下进行价格竞争属典型的两阶段博弈第一阶段是渠道结构选择第二阶段是定价竞争。把这个框架拆清楚以后我意识到整篇论文的计算核心只有两个一是给定渠道结构组合下的均衡价格和利润二是四种渠道结构组合RR、ER、RE、EE 或带双渠道的更多组合之间的利润比较从而画出均衡结构的分区图。1.2 符号体系统一复现前先给所有变量“对表”论文复现最头疼的问题之一就是符号不统一。不同论文里同一个参数可能叫 α、β、γ也可能叫 a、b、c。Mathematica 对大小写敏感一旦你中途改符号后面的代码全部要跟着改非常容易出错。我的做法是先在纸上把整套符号体系列一张表写进代码注释里。比如我在这个项目里固定了这样一套命名符号含义Mathematica 中写法( a )市场基础需求量a( b )零售商自身价格敏感系数b( d )渠道间的交叉价格敏感系数d( \theta )消费者对线上渠道的接受度theta( c )单位销售成本线上线下可不同cR, cE( p_{i,j} )零售商 i 在渠道 j 的价格p[i, j]( q_{i,j} )零售商 i 在渠道 j 的需求量q[i, j]( \pi_i )零售商 i 的总利润Pi[i]统一符号这一小步看似花时间实际为我后面省了大量调试时间。如果你复现的是其他论文也建议先花半小时把符号表列好这样不管推导到哪一步代码和数据都能对得上。1.3 复现路线图从符号推导到数值实验的四步走我在动手写代码之前给自己定了一个四阶段路线后面所有工作都严格按这条线走避免想到哪算哪搭建需求函数与利润函数的基本框架用符号变量表示所有参数。对每种渠道结构组合求解定价阶段的最优反应函数和均衡价格。把均衡价格代回利润函数得到均衡利润再比较不同渠道结构下的利润大小。固定不同参数取值重复求解画出渠道结构均衡区域图和论文原图做对比。这个路线特别适合博弈论论文的复现因为博弈论模型天然就是“先解后比”先解出均衡再比较不同情形下的数值。后面第三大部分我还会展示每一步对应的 Mathematica 代码片段。2. Mathematica 符号推导把“手推公式”变成“机器求导”2.1 需求函数构造线性需求模型怎么设置才不失真渠道策略论文里用得最多的是线性需求模型因为它既能体现价格竞争又能方便地求解析解。论文里的需求函数一般这样写零售商 i 在线下渠道的需求( q_{i,R} a - p_{i,R} d(p_{j,R} p_{i,E} - 2p_{i,R}) \theta \cdot (\text{线上替代效应}) )当然不同论文写法差异很大有的会把线上渠道接受度放在需求截距项里有的会放在交叉项里。我在复现时遵循的是“保留论文核心机制、简化次要项”的原则需求函数必须包含价格对自身渠道的负向影响以及竞争渠道价格的正向影响否则后面的价格竞争就无从谈起。以我复现的模型为例我用了一套比较通用的设定假设每个零售商在某个渠道的需求等于基础需求减去自身价格影响再加上竞争渠道和竞争零售商的价格溢出效应。通过一个参数 ( d ) 控制渠道间的替代强度通过 ( \theta ) 控制线上渠道的吸引力。把需求函数写成 Mathematica 代码时有个小技巧不要直接写成一长串赋值语句而是定义成函数形式方便后面重复调用。我在 Notebook 里这样写(* 定义需求函数 *) q[i_, j_, s1_, s2_] : a - p[i, j] d * (Sum[Sum[p[k, l], {l, Channels[k]}], {k, 2}] - p[i, j]) theta * If[j E, 1, 0]; (* 定义每个零售商的可用渠道集合 *) Channels[1] : {R, E}; Channels[2] : {R, E};当然这段代码为了展示逻辑进行了简化实际运行的时候需要根据论文的渠道组合来调整求和范围。这里的关键是定义函数时把所有渠道的价格都显式写出来不要用全局变量否则后面的求导和代入特别容易混乱。2.2 一阶条件与最优反应函数Solve 还是 NSolve 要分清价格竞争阶段的核心就是每个零售商在自己的渠道上选择价格最大化自己的总利润。利润函数很简单该零售商所有渠道的价格-成本×需求之和。在给定另一家零售商的渠道策略和价格策略时每个零售商的最优价格通过一阶条件得到对自身利润函数关于自身价格求偏导令导数为零解出价格表达式。Mathematica 里这一步几乎是“无脑操作”但有一个容易踩的坑一阶条件通常是一个线性方程组Solve 是可以直接求符号解的。但如果论文的模型带了非线性项比如含 ( p^2 ) 或者服务水平决策Solve 可能长时间算不出来这时候要先检查是不是模型设定太复杂别急着换数值方法。我当时写的代码大致是这样的(* 零售商1的利润函数 *) Pi1[p1R_, p1E_, p2R_, p2E_] : (p1R - cR) * q[1, R, {p1R, p1E}, {p2R, p2E}] (p1E - cE) * q[1, E, {p1R, p1E}, {p2R, p2E}]; (* 一阶条件 *) eq1R D[Pi1[p1R, p1E, p2R, p2E], p1R] 0; eq1E D[Pi1[p1R, p1E, p2R, p2E], p1E] 0; (* 求解零售商1的最优反应函数 *) reaction1 Solve[{eq1R, eq1E}, {p1R, p1E}] // Simplify;如果你只是想在某个具体参数组合下求数值解就可以在求解前先把参数赋值然后用 NSolve。但复现论文解析结论时最好保留参数符号因为后面还要做利润比较和参数区间讨论符号解能直接给出判断条件。2.3 联立最优反应均衡价格的形成逻辑单方面求最优反应是不够的均衡要求两个零售商的最优反应同时成立也就是把两家零售商的价格解联立起来。这个“联立”动作背后的经济学直觉是你定价格的时候要考虑对方的反应对方定价格的时候也要考虑你的反应。最后的结果是大家都选了一个“给定对方价格时自己最优”的价格组合也就是 Nash 均衡。代码上实现也不复杂就是先把两个零售商各自的一阶条件列出来然后用 Solve 一次性解方程组。不过这里要特别注意如果零售商可以选择双渠道那么价格变量会非常多比如零售商1有线下价和线上价零售商2也有线下价和线上价方程组可能会有四五个方程。解出来的表达式会非常冗长。长表达式是 Mathematica 符号推导中最大的敌人。我的经验是每解出一个均衡价格立刻用 Simplify 或者 FullSimplify 化简然后存成一个规则列表。后面要算利润时直接把这个规则代入千万不要把整串表达式复制来复制去那样子既容易出错又拖慢计算速度。(* 联立求解均衡价格 *) eqs {eq1R, eq1E, eq2R, eq2E}; soln Solve[eqs, {p1R, p1E, p2R, p2E}] // FullSimplify; (* 保存最终均衡价格规则 *) equilibriumPrice soln[[1]];这里下标[[1]]也是一门学问Solve 可能返回多个解但在线性需求模型下通常只有一个经济上合理的解即价格为正、利润为正的解。如果出现多解一定要手动检查哪个解符合价格非负的条件别直接拿第一个解就用。3. 实操现场四种渠道结构组合的计算与比较3.1 渠道结构场景枚举RR、RE、ER、EE 还是带 D在论文模型里两个零售商各选一种或两种渠道组合起来会有多种结构。常见的有都只开线下RR零售商1开线下零售商2开线上RE两边都开双渠道DD一方双渠道、另一方单渠道DR、DE这里需要特别提醒组合本身不是越多越好。论文里如果只分析了几种代表性组合你就没必要把所有可能组合全枚举一遍否则算力消耗大且展示重点不突出。我当时是把论文里面原有的组合场景逐个实现然后额外补充了一个“双渠道 vs 双渠道”的场景作为自己的一点扩展。对于每一个场景我写了一个统一的函数输入是两个零售商各自选择的渠道集合输出是均衡价格和均衡利润。这样做的好处是代码复用率极高不需要每个场景都复制粘贴一遍求解逻辑。(* 通用计算函数给定渠道集合返回均衡价格与利润 *) computeEquilibrium[ch1_, ch2_] : Module[ {prices, profits}, prices solveForPrices[ch1, ch2]; profits computeProfits[prices, ch1, ch2]; {prices, profits} ];3.2 利润比较与渠道结构选择博弈有了四个场景下的均衡利润下一步就是画一个 ( 2 \times 2 ) 的支付矩阵看看两个零售商在“选线下”还是“选线上”之间最终会落在哪个均衡。这步用 Mathematica 做非常直观把每个零售商在四种组合下的利润列成一个矩阵再根据利润大小判断最优反应。比如给定零售商2选了线下零售商1比较自己选线下和选线上的利润哪个高就选哪个给定零售商2选了线上再比较一次。最后找出一组“互为最优反应”的策略那就是纯策略 Nash 均衡。代码层面其实不需要复杂的博弈论工具包用最原始的嵌套判断就能算出来(* 四种组合利润 *) profitTable { {RR, Pi1RR, Pi2RR}, {RE, Pi1RE, Pi2RE}, {ER, Pi1ER, Pi2ER}, {EE, Pi1EE, Pi2EE} }; (* 判断零售商1基于零售商2策略的最优反应 *) bestResponse1[ch2_] : Which[ profit1[{R, ch2}] profit1[{E, ch2}], R, True, E ];这个过程就是要解决论文里最核心的问题竞争零售商最终会走向线上化还是继续维持线下还是变成双渠道混战答案往往取决于参数比如线上接受度和渠道替代强度。复现到这里我已经能看懂论文主图的生成逻辑了。3.3 数值实验参数扫描与三维图复现符号推导做完了论文里很多结论是靠数值实验和图形呈现的这时候就需要用参数扫描画出均衡区域图。我看过很多新手在这一步犯同一个错误直接拿循环跑上百组参数每组都用 Solve 求一次解析解结果跑得奇慢无比。正确的做法是分两步第一步在符号层面完成所有求解把均衡价格的表达式存好第二步在数值实验阶段把参数代入表达式中快速计算而不是每次都重新求解。相当于把“求公式”和“代数字”两件事分离开了。我在复现“渠道结构均衡区域图”时写了一个双重循环横轴是线上渠道接受度 ( \theta )纵轴是渠道替代强度 ( d )对每个参数组合判断哪种渠道结构是 Nash 均衡然后用 RegionPlot 画出分区颜色图。(* 参数扫描判断均衡 *) equilibriumStruct[thetaVal_, dVal_] : Module[ {payoffs, eq}, payoffs computeAllPayoffs /. {theta - thetaVal, d - dVal}; eq findPureNash[payoffs]; eq ]; RegionPlot[ equilibriumStruct[theta, d] RR, {theta, 0, 1}, {d, 0, 1}, PlotStyle - Blue, PlotPoints - 50 ]这张图如果手画工作量巨大用 Mathematica 的 RegionPlot几分钟就能出图。但前提是你已经把符号推导部分做得足够干净否则参数代入的时候会报一堆 “Power::infy” 或者 “Infinity::indet” 的错误到时候排查起来非常头疼。4. 复现过程中的坑与排查技巧4.1 符号表达式太长FullSimplify 不是万能的第一道坎就是表达式太长。当你解出四价格方程组时输出的表达式可能连续十行都写不完直接看根本看不出经济含义。这时候你可能会想到 FullSimplify但 FullSimplify 在变量多的时候计算时间会急剧上升有时候一分钟都算不完一个式子。我的经验是先尝试 Simplify等确认结果正确后再有针对性地用 FullSimplify 处理要用于绘图的式子。还有一种节省时间的办法是把公共项提取出来用Collect按某个变量整理让表达式结构更清晰。实际项目里按theta和d做 Collect 之后很多看似混乱的项会突然变得有规律后面数值代入也快很多。(* 结构化化简 *) Collect[Simplify[soln[[1, 1, 2]]], {theta, d}]4.2 均衡解出现负价格或负需求先查参数范围论文复现中最常见、也最容易让新手懵的现象就是解出来的均衡价格或者均衡需求是负数。这不一定是你代码错了很多数理模型在特定参数区域就会产生不合理的“数学解”但论文中通常只关注有经济意义的参数空间。我当时遇到负价格的第一反应是检查代码后来才意识到要先把参数约束加进求解过程。比如 ( 0 \theta 1 )、( 0 d b ) 这类条件尤其在数值实验阶段务必要在参数扫描前用Assumptions声明或者直接在 RegionPlot 里限定坐标范围。这样不仅避免了无意义的解也让均衡区域图更贴合论文原图。另一个容易出问题的地方是交叉价格系数 ( d ) 的取值范围。d 必须小于自身价格系数 b否则需求函数丧失了“自身价格影响大于交叉影响”的基本性质会导致荒谬的结果。很多论文的原图能画出来就是因为参数范围选得对这一点在复现时一定要留意。4.3 不同文献符号差异大不要照搬代码建立自己的模块最后想强调一点复现论文不是把别人的代码拿过来跑一遍而是理解模型逻辑后用工具重建。我一开始也尝试找原作者是否有公开代码但大多数渠道策略论文只提供公式和图形没有代码附录。所以最终还是回到了“读公式—写代码—对图”这个流程上。不同论文即使模型相似符号体系和假设也可能不同比如有的论文假设线上渠道成本为零有的论文假设线上渠道有额外物流成本。这些微观差异对均衡结果影响很大照搬别人的代码反而容易出错。所以我会针对每个论文建立一套自己的符号和函数模块即使效率低一点也好过在别人的代码里找哪里该改、哪里不该改。5. 写在最后的实操心得这次复现做完最大的感受是 Mathematica 特别适合这种“符号推导 参数扫描 区域图”三位一体的博弈论论文复现。它不像手推那样容易出错也不像纯数值仿真那样看不到公式结构。只要你把需求函数、利润函数、均衡求解和绘图拆成清晰的模块一篇论文从想法到出图基本可以压缩在一天内完成。给你一个直接的建议不管你复现什么论文先从复现一两个关键图开始不要一上来就想把整篇论文所有拓展全部实现。先把主模型跑通确认你的结果曲线和论文原图长得一致再逐步加入参数变化、灵敏度分析和扩展场景。这样每一步都有对照出错也能快速定位。如果再往前扩展一步这套代码还很容易改成双渠道加服务竞争的模型因为核心的博弈求解框架是通用的只需要增加一个服务变量和对应的成本函数。我现在就是这么做的后续如果跑出新结果再来分享更多细节。