ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

跨渠道退货双渠道闭环供应链博弈模型Python代码复现与避坑指南

跨渠道退货双渠道闭环供应链博弈模型Python代码复现与避坑指南

简介:这份资源面向供应链管理领域的研究人员、高校物流管理专业师生及双渠道销售企业管理者,聚焦消费者跨渠道退货场景下的双渠道闭环供应链决策难题。资源以Stackelberg博弈和Nash均衡为理论框架,系统对比集中式与分散式决策下的最优定价策略与利润分配,并探讨跨渠道退货率与消费者渠道偏好对供应链决策的影响。压缩包内为1个PDF文件,约624KB,完整呈现模型假设、需求函数构建、集中式与分散式求解及结果分析等核心内容,并附有可运行的Python代码及逐段解释,便于读者复现研究结论。已有41人学习下载。读者可借此掌握不同权力结构下的定价规律,理解退货率与定价之间的非线性关系,并获取收益共享契约设计与渠道协调机制的实践参考,适合作为论文复现与数据分析的实操素材。

1. 跨渠道退货下的双渠道闭环供应链:一份能跑通博弈模型的 Python 代码包

线上买、线下退,或者线下试、线上买——这种跨渠道退货行为正在把传统的双渠道定价模型搅成一团乱麻。很多做供应链优化的朋友拿到论文里的均衡解推导,第一步就卡在“零售商反应函数怎么代回制造商利润”上,更别提用代码把 Stackelberg 博弈和 Nash 均衡跑出数值结果。这份资源就是冲着这个痛点来的:它把一篇《考虑跨渠道退货的双渠道闭环供应链决策研究》的完整复现过程拆成了可运行的 Python 脚本,从需求函数、退货修正、集中式联合定价,到制造商主导的 Stackelberg 博弈求解,再到海塞矩阵凹性验证和敏感性分析,全部有代码、有注释、有输出。适合供应链管理方向的研究生、做定价策略的运筹优化工程师,以及需要快速验证渠道协调机制的产品运营。如果你正在找一份能直接改参数、看结果、验证命题的代码底稿,这份东西省掉的是从公式到可执行脚本之间那层最磨人的翻译工作。

2. 模型拆解:需求函数、退货修正与利润构成的代码映射

2.1 参数体系与需求函数的向量化写法

拿到一份供应链博弈代码,第一件事不是跑minimize,而是把参数表和需求函数对齐。原模型里市场总需求alpha=1000、消费者线下偏好theta=0.6、自身价格弹性beta=1.2、交叉价格弹性gamma=0.8,这四个数决定了两个渠道的需求基数。需求函数本身是线性的:

import numpy as np from scipy.optimize import minimize # 基础参数 alpha = 1000 # 市场总需求 theta = 0.6 # 消费者对实体店的偏好比例 beta = 1.2 # 需求价格弹性(自身渠道) gamma = 0.8 # 需求交叉价格弹性 c_m = 20 # 制造商生产成本 c_r = 10 # 零售商运营成本 def demand(p_m, p_r, alpha, theta, beta, gamma): D_r = alpha * theta - beta * p_r + gamma * p_m D_m = alpha * (1 - theta) - beta * p_m + gamma * p_r return D_r, D_m

这里有个容易翻车的点:beta和gamma的数值关系决定了两个渠道是替代还是互补。当beta > gamma时,自身价格效应大于交叉效应,需求函数才符合正常商品逻辑。原代码里beta=1.2、gamma=0.8,满足这个条件,所以后续优化不会出现价格越高需求越大的反直觉结果。如果你把gamma调到大于beta,minimize会给你一个负价格或者直接不收敛,这不是代码 bug,是模型设定出了问题。

2.2 退货修正:从毛需求到净销售量的转换逻辑

跨渠道退货的核心麻烦在于:需求函数算出来的是“卖出去了多少”,但利润要按“最终留下多少”来算。原模型用了一个除法修正:

mu = 0.3 # 线下→线下退货率 phi = 0.2 # 线上→线上退货率 epsilon = 0.1 # 线上→线下交叉退货率 def actual_sales(D_r, D_m): Q_m = D_m / (1 - phi - epsilon) Q_r = D_r / (1 - mu) assert mu * D_r + epsilon * D_m <= Q_r, "退货量超过实体店承受能力" return Q_r, Q_m

Q_m = D_m / (1 - phi - epsilon)这个式子的含义是:线上渠道最终留下的净销量是D_m,但为了达到这个净销量,实际需要发出的货量要除以留存率。phi + epsilon是线上渠道的总退货比例,包括退回线上和跨渠道退到线下两部分。assert那行是原代码里少有的防御性编程,检查线下渠道收到的退货量不能超过它的实际处理能力。我一般会把这个断言改成if加raise ValueError,因为assert在python -O模式下会被跳过,生产环境里容易埋雷。

2.3 集中式决策的利润函数与边界约束

集中式决策是把制造商和零售商当成一个整体来优化,决策变量只有两个价格:线上直销价p_m和线下零售价p_r。利润函数要把退货残值和处理成本都算进去:

delta = 15 # 退货残值 h_r = 5 # 线下退货处理成本 h_m = 4 # 线上退货处理成本 h_mr = 6 # 交叉退货处理成本 def centralized_profit_with_returns(p): p_m, p_r = p D_r, D_m = demand(p_m, p_r, alpha, theta, beta, gamma) Q_r, Q_m = actual_sales(D_r, D_m) revenue_m = (p_m - c_m) * D_m return_m = (delta - h_m) * phi * Q_m + (delta - h_mr) * epsilon * Q_m revenue_r = (p_r - c_m) * D_r return_r = (delta - h_r) * mu * Q_r total_profit = revenue_m + revenue_r + return_m + return_r return -total_profit result = minimize( centralized_profit_with_returns, [50, 60], bounds=[(0, None), (0, None)] ) optimal_p_m, optimal_p_r = result.x print(f"集中式最优定价: 线上={optimal_p_m:.2f}, 线下={optimal_p_r:.2f}")

bounds=[(0, None), (0, None)]是必须加的,否则minimize可能试探负价格,导致需求函数算出超过市场总需求的荒谬值。初始猜测值[50, 60]不是随便写的,它大致对应成本加成定价的直觉范围,能让求解器更快收敛到有经济意义的局部最优。如果你换成[500, 600],大概率会跑到边界上去,因为高价区间的利润函数斜率已经变了。

3. 分散式决策:Stackelberg 博弈的代码实现与求解顺序

3.1 零售商反应函数的解析解与数值解对照

分散式决策下,制造商先动,定批发价omega和直销价p_m;零售商观察到这两个值后,再优化自己的零售价p_r。原代码给了两种写法:一种是数值优化minimize,一种是解析解直接代入。先看数值版:

def retailer_profit(p_r, p_m, omega): D_r, _ = demand(p_m, p_r, alpha, theta, beta, gamma) net_D_r = D_r * (1 - r_r) return -(p_r - omega - c_r) * net_D_r def manufacturer_profit(p): p_m, omega = p res = minimize( retailer_profit, x0=60, args=(p_m, omega), bounds=[(0, None)] ) optimal_p_r = res.x[0] D_r, D_m = demand(p_m, optimal_p_r, alpha, theta, beta, gamma) net_D_m = D_m * (1 - r_m) profit = (omega - c_m) * D_r * (1 - r_r) + (p_m - c_m) * net_D_m return -profit result = minimize(manufacturer_profit, [50, 40], bounds=[(0, None), (0, None)]) optimal_p_m, optimal_omega = result.x

这段代码的求解顺序是“嵌套优化”:外层minimize每试探一组(p_m, omega),内层就要跑一次零售商的minimize。计算量是外层迭代次数乘以内层迭代次数,参数多的时候会明显变慢。原代码里x0=60是零售价的初始猜测,这个值如果离真实最优太远,内层求解可能不收敛,外层就会拿到一个噪声很大的利润值,导致整个 Stackelberg 均衡解漂移。

3.2 解析解代入:从反应函数到制造商目标函数

更稳的做法是用解析解。零售商利润对p_r求一阶条件,可以得到:

def retailer_reaction(p_m, omega): numerator = alpha * theta + gamma * p_m + beta * omega / 2 - beta * O + gamma * M p_r_opt = numerator / beta return p_r_opt H = ((delta - h_m) * phi + (delta - omega_tilde - b) * epsilon) / (1 - phi - epsilon) G = (delta - omega_tilde) * mu / (1 - mu) O = (omega_tilde - h_r) * mu / (1 - mu) M = (omega_tilde - h_mr + b) * epsilon / (1 - phi - epsilon)

这里H、G、O、M是退货相关参数的聚合项,原代码把它们单独拎出来是为了让反应函数的表达式更短。omega_tilde=25是退货转移价格,b=8是制造商对零售商的跨渠道补贴。这两个参数在解析解里直接进入分子,说明退货转移价格越高,零售商越有动力抬高零售价——因为它从退货处理中也能分到更多收益。

把retailer_reaction代回制造商利润函数后,外层优化就变成了一维搜索,速度快很多:

def manufacturer_profit_analytical(p): p_m, omega = p p_r = retailer_reaction(p_m, omega) D_r, D_m = demand(p_m, p_r, alpha, theta, beta, gamma) Q_r, Q_m = actual_sales(D_r, D_m) direct_sale = (p_m - c_m) * D_m wholesale = (omega - c_m) * D_r online_returns = (delta - h_m) * phi * Q_m cross_returns = (delta - omega_tilde) * (mu * Q_r + epsilon * Q_m) subsidy_cost = -b * epsilon * Q_m total_profit = direct_sale + wholesale + online_returns + cross_returns + subsidy_cost return -total_profit result = minimize(manufacturer_profit_analytical, [50, 40], bounds=[(0, None), (0, None)]) p_m_opt, omega_opt = result.x p_r_opt = retailer_reaction(p_m_opt, omega_opt) print(f"Stackelberg均衡: p_m*={p_m_opt:.2f}, ω*={omega_opt:.2f}, p_r*={p_r_opt:.2f}")

3.3 海塞矩阵验证:用 SymPy 确认凹性条件

数值解出来之后,怎么知道它真的是最大值而不是鞍点?原代码用 SymPy 算海塞矩阵:

from sympy import symbols, diff, Matrix, simplify p_m, omega = symbols('p_m omega') p_r = (alpha * theta + gamma * p_m + beta * omega / 2 - beta * O + gamma * M) / beta D_m = alpha * (1 - theta) - beta * p_m + gamma * p_r D_r = alpha * theta - beta * p_r + gamma * p_m pi_m = (p_m - c_m + H) * D_m + (omega - c_m + G) * D_r Hessian = Matrix([ [diff(pi_m, p_m, p_m), diff(pi_m, p_m, omega)], [diff(pi_m, omega, p_m), diff(pi_m, omega, omega)] ]) print("海塞矩阵行列式:", simplify(Hessian.det())) print("二阶导数:", [simplify(Hessian[0, 0]), simplify(Hessian[1, 1])])

判断标准是:行列式大于零,且两个主对角线元素都小于零。如果行列式算出来是负的,说明利润函数在均衡点附近不是凹的,数值解可能只是局部最优。我一般会把这个验证步骤单独写成一个check_concavity()函数,每次改完参数就跑一遍,避免拿着一个不稳定的均衡解去做敏感性分析。

4. 避坑与排查:跑通这份代码常见的五个翻车点

4.1 退货率参数导致除零或负留存率

现象:运行actual_sales时抛出ZeroDivisionError,或者Q_m算出负数。

原因:phi + epsilon被设成大于等于 1,比如phi=0.6、epsilon=0.5,留存率1 - phi - epsilon变成负数。现实中退货率不可能超过 100%,但调参时容易手滑。

解决:在参数定义后加一行校验assert phi + epsilon < 1 and mu < 1,把非法组合挡在求解之前。如果确实需要研究高退货率场景,把epsilon上限设在 0.4 左右,给留存率留出正数空间。

4.2 嵌套优化不收敛导致均衡解漂移

现象:每次运行manufacturer_profit得到的optimal_p_m和optimal_omega都不一样,波动范围超过 5%。

原因:内层minimize的x0=60离真实零售价太远,或者bounds设得太窄,求解器在边界上反复试探。外层拿到的利润值带有内层求解误差,优化方向就被带偏了。

解决:优先用解析解retailer_reaction替代内层数值优化。如果必须用数值版,把x0改成上一轮外层迭代得到的p_r,做热启动;同时把内层tol调到1e-8,减少噪声。

4.3 海塞矩阵行列式符号与预期相反

现象:Hessian.det()输出负值,但论文里说利润函数是凹的。

原因:beta和gamma的数值关系不满足凹性条件。原模型要求beta > gamma,如果调参时把gamma设成 1.5、beta保持 1.2,交叉价格效应压过自身价格效应,利润函数就变成凸的,均衡解从最大值变成最小值。

解决:检查beta和gamma的大小关系,确保beta > gamma。如果研究场景确实需要gamma > beta,那就要换模型设定,不能硬套这份代码的凹性验证逻辑。

4.4 退货补贴b的符号搞反

现象:制造商利润随b增大而增大,和直觉相反。

原因:原代码里subsidy_cost = -b * epsilon * Q_m,b是制造商付给零售商的补贴,所以是成本项。如果误写成+b * epsilon * Q_m,补贴就变成了收入,利润函数的经济含义完全变了。

解决:在代码注释里明确标注每个利润项的符号方向。我习惯在参数定义区加一行# b: 制造商→零售商补贴,正数表示制造商支出,避免后续改代码时搞混。

4.5 敏感性分析循环里参数污染

现象:跑epsilon_range循环时,第一轮结果正常,后面几轮价格突然跳变。

原因:循环里直接改了全局变量epsilon,但H、M、O这些聚合项是在循环外算好的,没有跟着更新。制造商利润函数里用的还是旧的H和M,导致定价决策和实际退货率不匹配。

解决:把H、M、O的计算封装成函数,每次循环重新算一遍:

def compute_aggregates(epsilon, mu, phi, delta, h_m, h_r, h_mr, omega_tilde, b): H = ((delta - h_m) * phi + (delta - omega_tilde - b) * epsilon) / (1 - phi - epsilon) G = (delta - omega_tilde) * mu / (1 - mu) O = (omega_tilde - h_r) * mu / (1 - mu) M = (omega_tilde - h_mr + b) * epsilon / (1 - phi - epsilon) return H, G, O, M

然后在循环里调用这个函数,确保每次迭代的聚合项和当前epsilon一致。

5. 敏感性分析与渠道协调:从数值实验到管理启示

5.1 交叉退货率对定价的边际影响

原代码最后给了一段敏感性分析的框架,但只画了epsilon对p_m和p_r的影响。实际用的时候,我建议把theta也加进去,做双参数扫描:

import matplotlib.pyplot as plt epsilon_range = np.linspace(0.05, 0.2, 20) theta_range = np.linspace(0.4, 0.8, 5) results = [] for theta_val in theta_range: theta = theta_val p_m_list, p_r_list = [], [] for eps in epsilon_range: epsilon = eps H, G, O, M = compute_aggregates(epsilon, mu, phi, delta, h_m, h_r, h_mr, omega_tilde, b) res = minimize(manufacturer_profit_analytical, [50, 40], bounds=[(0, None), (0, None)]) p_m_opt, omega_opt = res.x p_r_opt = retailer_reaction(p_m_opt, omega_opt) p_m_list.append(p_m_opt) p_r_list.append(p_r_opt) results.append((theta_val, p_m_list, p_r_list)) for theta_val, p_m_list, p_r_list in results: plt.plot(epsilon_range, p_m_list, label=f'p_m (θ={theta_val:.1f})') plt.plot(epsilon_range, p_r_list, linestyle='--', label=f'p_r (θ={theta_val:.1f})') plt.xlabel('交叉退货率 ε') plt.ylabel('最优价格') plt.legend() plt.show()

跑出来你会看到:epsilon增大时,线上价格p_m下降,线下价格p_r上升。原因是交叉退货把线上渠道的退货成本部分转移到了线下,线下零售商通过处理退货获得残值和补贴,有动力抬高价格来筛选消费者。theta越大,这个分化越明显,因为线下渠道的需求基数本来就大,退货处理规模效应更显著。

5.2 收益共享契约的参数设计

原论文提到用收益共享契约来协调分散式决策下的利润分配,但代码里没有展开。我一般会补一个简单的协调验证:

def coordinated_profit(p_m, p_r, omega, sharing_ratio): D_r, D_m = demand(p_m, p_r, alpha, theta, beta, gamma) Q_r, Q_m = actual_sales(D_r, D_m) total = (p_m - c_m) * D_m + (p_r - c_m) * D_r total += (delta - h_m) * phi * Q_m + (delta - h_mr) * epsilon * Q_m total += (delta - h_r) * mu * Q_r pi_m = sharing_ratio * total pi_r = (1 - sharing_ratio) * total return pi_m, pi_r pi_m_coord, pi_r_coord = coordinated_profit(p_m_opt, p_r_opt, omega_opt, 0.6) print(f"协调后制造商利润: {pi_m_coord:.2f}, 零售商利润: {pi_r_coord:.2f}")

sharing_ratio是制造商分到的总利润比例。协调的目标是让pi_m_coord >= pi_m且pi_r_coord >= pi_r,即双方都比分散式决策下过得更好。如果找不到这样的sharing_ratio,说明契约参数需要调整,比如改变omega_tilde或b。

5.3 一个我踩过的坑:初始猜测值对均衡解的影响

最后说一个血泪经验。这份代码里minimize的初始猜测值[50, 40]不是随便写的,它对应的是成本加成定价的直觉范围。我有一次为了“加速收敛”,把初始值改成[100, 80],结果求解器跑到了一个高价均衡,利润反而比论文里的基准值低了 15%。排查了半天才发现,利润函数在低价区间有一个局部最优,初始值太远就跳不过去了。

从那以后我每次跑这类博弈模型,都强制走一遍“多初始值扫描”:用np.linspace生成五组初始值,分别跑minimize,取利润最大的那组作为最终解。虽然多花几秒钟,但能避免拿着一个局部最优当全局最优去写论文。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进