复现这篇竞争零售商渠道策略的论文,项目标题里写的是mathematic,实际指的就是Wolfram Mathematica,学术界一般直接叫Mathematica。这篇博文把整个复现过程从头捋一遍,从模型设定、符号推导到均衡判定、数值画图,再包括我踩进去又爬出来的几个坑。如果你正在复现运营管理、产业组织、供应链方向的理论模型,或者刚拿到一篇渠道策略论文想用Mathematica验证结论,这篇内容应该能帮你少走好几天弯路。
先说清楚,论文复现不是把作者公式抄进notebook就完事。真正的复现要把决策逻辑完整还原,消费者需求怎么定义、零售商之间怎么竞争、渠道选择博弈时序是什么、用的是纳什均衡还是子博弈精炼均衡,这些底层问题理不清楚,代码写得再漂亮也对不上结果。
1. 复现项目到底在做什么
1.1 论文复现的三层目标
复现一篇论文,表面上是重算一遍结果,实际是在做三件事。
第一是验证。作者在论文里声称,某些参数条件下双渠道是均衡,某些条件下传统渠道是均衡,我们得独立算一遍,确认这些结论站得住脚。独立这个关键词很重要,最好不直接参考作者给的中间推导,而是从需求函数开始用另一套工具链重推。这样能避免被作者的计算错误带偏,也能发现自己理解上的盲区。
第二是精细理解模型。论文里到处是“经计算可得”,背后可能省掉了好几页代数。复现时要把那些省略的步骤补回来,补的过程中你对模型机制的理解会完全不一样。比如竞争强度对线下价格的影响,看公式结论感受不深,自己推导时才会意识到,渠道结构变了,价格反应函数的斜率也会变。
第三是为扩展研究打基础。很多人不是从零建模型,而是在已有模型上加设定,比如多一个渠道、换一种需求函数、引入不对称成本。复现一遍相当于把地基打牢,后面扩展才敢放开手脚。
1.2 竞争零售商渠道策略的典型设定
这类论文的模型框架高度相似。市场上有两个竞争零售商,记为A和B,销售可替代产品,价格竞争。每个零售商可以选两种渠道策略:只做线下传统渠道,简写为T;或者线上线下双渠道,简写为D。
关键在决策时序。第一阶段,两个零售商同时决定是否开通线上渠道。第二阶段,给定渠道结构,双方做价格竞争。求解用逆向归纳,先解第二阶段的价格均衡,再回到第一阶段比较利润,判断什么渠道组合能成为纳什均衡。
需求侧通常用线性需求函数,因为有良好的解析性质,能推出闭合解。竞争强度用交叉价格系数表示,系数越大,产品替代性越强,价格竞争越激烈。消费者渠道偏好用线上渠道的天然需求占比表示,其余比例归线下。线上渠道本身有权衡,它有覆盖增量市场的优势,但要付出单位配送成本,还要付开通固定成本。
论文关心的核心问题是:双渠道均衡在什么条件下出现?双渠道一定占优吗?竞争强度、消费者渠道偏好、线上成本与固定成本如何影响均衡渠道结构?最终答案都落在参数空间的分区上,所以我们复现的最后一步几乎总是画区域图。
1.3 复现流程拆解
我把复现流程固定成五步。
第一步,把模型设定翻译成Mathematica符号表达式,包括参数假设、需求函数、利润函数。第二步,对每种渠道组合求解第二阶段价格均衡,写成反应函数再联立。第三步,把均衡价格代回利润函数,得到第一阶段各策略组合下每个零售商的利润。第四步,根据利润比较构造纳什均衡条件,通常是一组不等式。第五步,在参数空间上绘制均衡区域图,和论文结果对照。
流程听起来简单,实际每一步都有隐藏陷阱。符号解太长、多解筛选、不等式化简不干净,这些后面单独讲。先说说为什么要用Mathematica,而不是Python或者Matlab。
2. 为什么选Mathematica做这种复现
2.1 符号推导和规则求解是核心能力
复现这类模型,真正的难点不在编程,而在符号推导。要解联立方程、化简带参数的高次表达式、判断不等式成立条件,这套需求几乎是为Mathematica量身定做的。
求解方程用Solve,求偏导用D,解不等式系统用Reduce,化简用Simplify和FullSimplify,替换表达式用斜杠点。这套语法看起来有些怪,但组合起来非常顺手。举个例子,求解混合渠道结构下的价格均衡,手算可能要一整页纸,Mathematica里就是几行代码,而且符号推导不出错,不会因为手算漏项导致后续结果漂移。
更重要的是不等式处理。复现中要判断一个零售商有没有动机偏离当前渠道策略,本质是判断一组带参数的不等式是否成立。Reduce可以在给定参数范围内输出完整的条件分支,这部分Sympy目前还做不到这么干净。
2.2 符号、数值、可视化在一个环境里完成
我坚持用Mathematica的另一个原因是流程完整。复现过程中经常需要从符号推导直接跳到数值验证,再跳到画图。比如先符号解出均衡价格,马上代入一组具体参数看数值,再画RegionPlot看区域变化,整个过程在同一个notebook里连续完成。
Manipulate这个交互式控件对理解模型帮助很大。拖动竞争强度或者固定成本的滑块,均衡区域实时变化,模型的比较静态一目了然。给导师或者合作者展示的时候,这种动态演示也比静态图片有说服力。
2.3 需要提前接受的几个缺点
Mathematica也不是没有毛病。第一,语法和主流语言差异大,函数名首字母大写、方括号传参、用规则替换数据,新手适应期不短。第二,全符号计算容易失控,尤其是不加约束直接对高次系统求解,跑几个钟头出不来是常事。第三,表达式过长时输出可读性差,满屏都是带一堆参数的分式,需要及时Simplify或封装成函数。
但从复现论文这个场景看,这些缺点都可以接受。Python加上Sympy虽然开源免费,复杂不等式化简时会明显吃力,而这类模型最后的均衡条件恰恰就是一堆不等式。
3. 核心模型搭建与符号推导实操
3.1 从需求函数开始,参数、变量与假设
我习惯把模型先完整写在纸上,再翻译成代码,避免边写边改逻辑混乱。参数方面,用a表示基础市场规模,用θ表示产品替代系数,用λ表示消费者天然倾向线上渠道的比例,用c表示线上渠道单位配送服务成本,用F表示开通线上渠道的固定成本。
开头的清理操作也很重要。每次新建notebook,第一行执行ClearAll["Global*"]`,把之前遗留的变量全部清掉。这个习惯救了我很多次,不然上次定义的pA会在下一次运行中悄悄混进当前计算,导致看似无法解释的错误。
然后是需求函数。我采用一个经典的线性需求框架。当零售商A只做线下渠道时,它的需求是:
DA[PA_, PB_] := a - PA + theta * PB这里的含义是基础市场规模减去自身价格,再加对手价格乘以替代系数。如果零售商开通双渠道,需求按消费者渠道偏好拆成线下和线上两部分。A线下需求为(1 - lambda)(a - PAr + theta * PBeff),线上需求为lambda (a - PAo + theta * PBeff),其中PBeff是B在两个渠道上的加权平均价格:
PBeff[pBr_, pBo_] := (1 - lambda) pBr + lambda pBo这个加权平均设定背后的直觉是:一个双渠道零售商对竞争对手形成的价格压力,等于它在两个渠道价格的平均水平。这样做能保持需求函数的线性结构,适合做解析推导。
参数约束我会显式写出来:
assume = {a > 0, theta > 0, theta < 1, lambda > 0, lambda < 1, c > 0, F > 0};后面做Reduce或者Simplify时把这些假设条件作为约束传进去,能大大减少无意义解的出现。
3.2 四种渠道结构下的利润函数与一阶条件
四种渠道结构分别是TT、DD、TD和DT,其中TD和DT在对称设定下结论相同,所以实际只需要处理TT、DD、TD三类。
先看最简单的TT结构。零售商A的利润函数是价格乘以需求:
ProfitA_T[pA_, pB_] := pA * DA[pA, pB];一阶条件就是利润对自身价格求导等于零:
focTT = D[ProfitA_T[pA, pB], pA] == 0;解出反应函数为pA = (a + theta * pB)/2。由于对称性,令pA = pB = p,得到均衡价格p = a/(2 - theta),均衡利润为:
profitTT = p * (a - p + theta * p) /. p -> a/(2 - theta)结果化简后是a^2/(2 - theta)^2。这个过程本身就展示了复现论文时常用的小技巧,不要急着把两个零售商都写进去,利用对称性先减少变量,计算量会小很多。
再看DD结构。由于完全对称,可以假设线下价格相同,线上价格也相同,分别记为pr和po。A线下利润和线上利润在价格上恰好是可分的,所以可以分开优化:
ProfitA_DD[pr_, po_] := pr * (1 - lambda) (a - pr + theta * pr) + (po - c) * lambda (a - po + theta * po) - F;对pr求导得到pr = a/(2(1 - theta)),对po求导得到po = a/(2(1 - theta)) + c/2。注意线下价格和线上配送成本无关,这个反直觉的结论来自需求函数中线下、线上市场彼此独立,线上成本只会完全转嫁到线上价格。
最难的是TD混合结构。A只做线下,B做双渠道。这时A的需求会受B加权平均价格影响,而B的线下和线上价格又分别受A价格影响,形成一个三方程联立系统。我在代码里这样写:
ProfitA_TinMixed[pA_, pBr_, pBo_] := pA * (a - pA + theta * ( (1 - lambda) pBr + lambda pBo)); ProfitB_DinMixed[pA_, pBr_, pBo_] := pBr * (1 - lambda) (a - pBr + theta * pA) + (pBo - c) * lambda (a - pBo + theta * pA) - F; eqMixed = { D[ProfitA_TinMixed[pA, pBr, pBo], pA] == 0, D[ProfitB_DinMixed[pA, pBr, pBo], pBr] == 0, D[ProfitB_DinMixed[pA, pBr, pBo], pBo] == 0 }; solMixed = Solve[eqMixed, {pA, pBr, pBo}] // Simplify;运行后输出的表达式会很长,但这正是复现的价值所在。混合结构下A作为单渠道零售商,面对双渠道对手时定价逻辑会发生明显变化,后面画均衡区域图时,这就是划分混合均衡区域的关键素材。
3.3 均衡渠道结构的判定与参数区域划分
第二步解出价格均衡后,把均衡价格代回利润函数,得到每种渠道结构下的利润值。然后进入两阶段博弈的均衡判定。
以TT均衡为例。A不偏离到D的条件是,B选择T时,A在T下的利润不低于A单方面切换到D下的利润。也就是profitA_TT >= profitA_DT。这里profitA_DT表示B做T、A做D时A的利润。同理,B也要满足对称条件。
DD均衡的条件则是profitA_DD >= profitA_TD,即B做D时,A不要想退回T。注意这个条件方向很容易写反,我每次都要对着博弈树重新捋一遍。
代码层面,我建议把这个条件定义为布尔表达式:
condDD = (profitA_DD >= profitA_TD) && (profitB_DD >= profitB_TD); condTT = (profitA_TT >= profitA_DT) && (profitB_TT >= profitB_TD);然后可以用Reduce求解参数条件:
Reduce[condDD && (assume /. List -> And), {theta, lambda}] // FullSimplify不过要注意,直接对包含F的不等式做全符号求解会非常慢,甚至跑不完。我的经验是分两步走。第一步,保留少数参数符号,比如固定a = 1,把c和F赋值,这样Reduce只在二维参数空间上做不等式化简,速度快很多。第二步,用数值扫描验证符号区域边界。实际操作中,论文里的参数分区图大都是固定其他参数后,在二维平面画的,所以这种降维处理并不会丢失核心信息。
4. 数值模拟与论文图形复现
4.1 参数设定与代码组织
数值模拟之前,我习惯把均衡利润表达式封装成只依赖参数的函数,而不是每次复制粘贴一大段结果。比如:
ClearAll[evalProfit]; evalProfit[structure_, aVal_, thetaVal_, lambdaVal_, cVal_, FVal_] := Module[ {a = aVal, theta = thetaVal, lambda = lambdaVal, c = cVal, F = FVal}, Switch[structure, "TT", profitTT, "DD", profitDD, "TD", profitTD ] ];这样组织代码的好处是后期画图时不需要重复推导,直接调用即可。参数取a=1,c=0.2,F=0.1,θ范围取0到0.8,λ范围取0到1。
4.2 用RegionPlot还原均衡区域图
RegionPlot是还原参数分区图的主力函数。你只需要把均衡条件写进第一个参数,指定坐标范围,图形就出来了:
RegionPlot[ {condDD, condTT, condMixed}, {theta, 0, 0.8}, {lambda, 0, 1}, PlotLegends -> {"双渠道均衡", "传统渠道均衡", "混合渠道均衡"}, BoundaryStyle -> Directive[Thick, Gray], PlotStyle -> {Opacity[0.3], Opacity[0.3], Opacity[0.3]} ]注意condMixed要仔细定义。混合结构有两种取向,A为T和B为D,或者反过来。对称设定下两者条件相同,画图时合并成一个区域。如果坐标系里的区域边界和论文不一致,不要急着改代码,先回到均衡条件,确认不等式方向是否写反。
实际复现中我不建议一次性把所有条件丢进RegionPlot。先单个画condDD,再画condTT,最后叠加混合区域,这样出错时能立刻定位是哪一层的逻辑问题。
4.3 用Manipulate做动态参数探索
静态图只能反映一组参数的结果,我更喜欢用Manipulate交互式探索:
Manipulate[ RegionPlot[ {condDD, condTT, condMixed}, {theta, 0, 0.8}, {lambda, 0, 1}, PlotLegends -> {"DD", "TT", "Mixed"} ], {c, 0, 0.5}, {F, 0, 0.2} ]拖动滑块的时候能非常直观地看到固定成本F上升,双渠道均衡区域缩小,传统渠道均衡区域扩大。这种动态演示对理解模型机制很有帮助,也适合在组会或者答辩时使用。
5. 复现过程中的常见问题与排查技巧
5.1 一阶条件符号求解卡住怎么办
Solve输入进去,Mathematica转圈几小时不出结果,这个问题几乎每个人都会碰到。我的排查顺序固定如下。
先检查方程数量是否等于变量数量。少一个条件就会让符号求解陷入长时间计算。再检查变量之间是否存在可分离结构,比如DD结构下线下和线上价格互不耦合,就应该分开解,不要强行整个方程组一起Solve。
然后是加假设条件。Solve本身不直接利用Assumptions,但Reduce可以利用。如果Solve跑不动,改成Reduce,把参数范围约束写进不等式系统,通常能更快得到结果。
最后的大招是数值探路。先给参数赋一组具体数值,用FindRoot求数值均衡,确认解的存在性和唯一性,再回头推符号解。数值能算出来但符号跑不动的时候,基本可以确定是表达式复杂度过高,这时需要人工分析替换变量。
5.2 多解、增根和边界解筛选
符号求解经常返回多组解,其中绝大多数没有经济学意义。我的筛选步骤是两步。
第一步做经济意义筛选,要求价格为正、需求为正、利润为正:
validSols = Select[sol, (pA > 0 && pB > 0) /. # &];第二步做稳定性筛选,即二阶条件。价格竞争均衡需要在利润函数的严格凹点取得。用Hessian矩阵判断:
hessian = D[ProfitA_TT[pA, pB], {{pA, pB}, 2}]; NegativeDefiniteMatrixQ[hessian]线性需求模型下这个条件相对简单,但混合结构里还是有必要确认一次。
5.3 数值结果和论文对不上,排查方向
这是最打击人的环节。我的排查顺序是固定的,符号定义、参数假设、均衡条件、坐标范围。
先核对需求函数形式。有些作者用的是q = a - p + θ(p_j - p_i)这样的相对价格形式,而我这里用的是q = a - p_i + θ p_j的绝对价格形式。两者模型本质不同,结论也不同,复现时必须有意识地对照原论文。
再核对成本处理位置。有的论文把线上单位成本放在价格里,记为po - c;有的把它折进渠道效用,变成需求函数里的项。放错位置结果会完全对不上。
然后是均衡判定方向。混合结构均衡的偏离方向特别容易搞反。最后看画图范围,论文可能用了归一化参数,比如令a=1,θ的定义域上限可能取1而不是0.8,坐标范围不对也会让图形看起来不一致。
5.4 性能优化技巧
能用数值的地方尽量别走符号。想验证某个命题在某个参数点是否成立,直接用具体数值算,不要每一次都从符号解开始。
批量扫描参数空间时用ParallelTable替换Table,多核并行能明显提速。曾经画一次均衡分区图需要扫密集网格,换并行后耗时从十几分钟降到两分钟。
如果要对同一个大表达式反复代入不同参数求值,可以考虑Compile生成更快的数值函数。线性需求模型一般用不上,但模型一旦加入消费者效用函数之类的高复杂度项,性能差距会非常大。
6. 几点实操心得与扩展方向
6.1 复现论文的正确打开方式
经过这次复现,我最大的体会是:复现一篇论文,大部分时间花在理解模型和调试边界条件上,真正写代码的时间只占一小部分。
拿到论文后我先不急着打开软件,而是花半天把模型结构吃透,把变量表、参数表、均衡条件全部手写一遍。这一遍认真写完,后面代码基本就是按图索骥。
还有一个小技巧,给每个渠道结构单独建一个代码块,命名清楚,比如“3.1 TT结构一阶条件”“3.2 DD结构一阶条件”。等所有结构都建完,再统一做利润比较和画图。这样即使后面发现某个结构推导错了,修改范围也很局部,不会把其他部分连带搞乱。
6.2 这个模型还能往哪些方向扩展
复现完成后,这个模型的可扩展性很强。
第一,成本不对称。假设A的线上成本cA和B的线上成本cB不同,均衡区域会从对称结构变成更丰富的分区,可能推出更有意思的结论。
第二,渠道偏好内生。消费者不是固定比例偏好线上,而是基于价格和体验自行选择渠道。这会引入渠道层面的交叉价格弹性,模型复杂度会上一个台阶。
第三,订单履约方式。线上订单可以选择配送到家或者到店自提,固定成本从一次性投入变成分阶段开通成本,这也是近几年文献里比较活跃的方向。
顺带说一句,复现过程中我还养成了一个习惯,每算完一个结构,就把均衡价格的表达式用传统数学记号写在笔记本上,再和代码输出对照。这看起来笨,但确实帮我逮住了好几处手写推导和代码定义不一致的问题。别太相信自己脑子里的记号,写出来才靠得住。