☰
蔬菜定价补货联合优化:数学建模C题从数据到源码落地
2026/10/31 1:13:59 网站建设 项目流程

简介:这份资源面向参加数学建模竞赛、准备毕业设计或课程设计的高校学生,聚焦2023年高教社杯全国大学生数学建模竞赛C题「蔬菜类商品的自动定价与补货决策」,提供完整源码与配套论文资料,帮助读者快速理解赛题思路并复现建模流程。压缩包共110个文件,约72.74MB,包含8个Python脚本、16个xlsx数据表、12个h5与12个pkl模型文件、12个joblib中间结果、36张png图表,以及pdf、doc、md、m、rmd等说明文档,覆盖数据处理、神经网络建模、结果可视化与论文撰写各环节。目前已有852人学习下载。资源中代码注释较为完整,新手也能对照理解,模型按辣椒类、食用菌、茄类、花菜类、花叶类、水生根茎类等品类分别训练,可直接用于毕设、期末大作业或课程设计,具备较高的参考与复用价值。

1. 蔬菜定价补货这道题,为什么每年都有人卡在“算得出、卖不动”

2023年高教社杯全国大学生数学建模竞赛C题,题目叫“蔬菜类商品的自动定价与补货决策”。很多人第一次看觉得就是个优化问题,真上手才发现:数据里藏着损耗、时段、品类替代、价格弹性,还有“今天不卖完明天就烂”的硬约束。它要解决的不是单纯算个最优解,而是让超市在每天凌晨决定:每个蔬菜单品定什么价、补多少货,才能在损耗和利润之间找到平衡。适合谁?适合正在做数学建模、想拿这道题练手,或者做零售定价、生鲜补货算法落地的同学和工程师。这道题的核心矛盾很直接——定价高了卖不动,补货多了烂在架上,补货少了又断货。下面我按“数据怎么读、模型怎么搭、代码怎么跑、坑在哪”的顺序,把这道题从论文到源码的落地路径拆开讲。

2. 先看清数据:蔬菜品类、时段与损耗的三角关系

2.1 附件数据到底给了什么

这道题通常会给几个附件:销售流水、批发价、品类信息、损耗率。销售流水里一般包含日期、单品编码、销量、销售单价、是否打折;批发价按日期和单品给;品类信息告诉你这是叶菜、根茎还是花菜。很多人一上来就急着建优化模型,结果连“销量是当天卖完的量还是上架量”都没分清,后面全错。

我一般会先做三件事:第一,把单品编码和品类对齐,确认每个单品属于哪个蔬菜大类;第二,把销售日期和批发日期对齐,注意批发价可能是前一天或当天凌晨的;第三,把损耗率单独拉出来,因为损耗直接决定“补货量 = 销量 / (1 - 损耗率)”这个换算关系。如果损耗率是按品类给的,就不要硬套到单品上,否则叶菜和根茎的差异会被抹平。

import pandas as pd # 读取销售流水、批发价、品类信息 sales = pd.read_csv('sales.csv') wholesale = pd.read_csv('wholesale.csv') category = pd.read_csv('category.csv') # 对齐单品编码与品类 sales = sales.merge(category, on='单品编码', how='left') # 按日期和单品聚合销量与均价 daily = sales.groupby(['日期', '单品编码']).agg( 销量=('销量', 'sum'), 均价=('销售单价', 'mean') ).reset_index() # 合并批发价 daily = daily.merge(wholesale, on=['日期', '单品编码'], how='left') print(daily.head())

这段代码的逻辑是先把分散的附件拼成一张“日期-单品-销量-均价-批发价”的主表。参数上注意:销量用sum是因为同一天同一单品可能有多次销售记录;均价用mean是为了后续算毛利。如果批发价有缺失,不要直接删,先看是不是非交易日,用前向填充更稳。

2.2 损耗率怎么进模型

损耗率是这道题最容易被低估的参数。常见做法是:补货量 = 预测销量 / (1 - 损耗率)。但这里有个坑——损耗率如果是按“上架量”算的,那这个公式成立;如果是按“销售量”算的,公式要反过来。我一般会先拿几天数据反推:用实际补货量、实际销量和损耗率对一遍,确认口径。

另外,损耗率不是常数。叶菜夏天损耗能到30%以上,冬天可能只有10%。如果题目给了按品类和月份的平均损耗率,就按品类和月份取;如果只给了品类平均,那就在模型里加一个季节因子,用历史数据拟合。别小看这一步,损耗率差5个点,最优补货量能差出10%以上。

2.3 价格弹性的粗略估计

定价决策离不开价格弹性。题目通常不会直接给弹性系数,需要自己估。最简单的方法:对每个品类,用历史数据做销量对价格的回归,取对数后斜率就是弹性。注意要控制日期固定效应,否则会把季节趋势误当成价格影响。

import numpy as np import statsmodels.api as sm # 取某个品类的数据 cat_data = daily[daily['品类'] == '叶菜'].copy() cat_data['log销量'] = np.log(cat_data['销量'] + 1) cat_data['log价格'] = np.log(cat_data['均价'] + 1) # 加日期虚拟变量控制季节 date_dummies = pd.get_dummies(cat_data['日期'], prefix='d', drop_first=True) X = pd.concat([cat_data[['log价格']], date_dummies], axis=1) X = sm.add_constant(X) y = cat_data['log销量'] model = sm.OLS(y, X).fit() print(model.params['log价格'])

这里log价格的系数就是弹性估计。如果系数是 -1.2,说明价格涨1%,销量降1.2%。参数上注意:加+1是为了避免零销量取对数出错;日期虚拟变量是为了吸收季节和促销影响。如果数据量不够,可以按品类合并估计,不要硬拆到单品。

3. 定价与补货的联合优化:从目标函数到求解器

3.1 目标函数怎么写才不翻车

这道题的目标通常是“利润最大化”,但利润 = 销售收入 - 批发成本 - 损耗成本。损耗成本怎么算?如果损耗的菜没有残值,那就是批发价乘以损耗量。所以目标函数可以写成:

利润 = Σ(售价 × 销量) - Σ(批发价 × 补货量)

其中补货量 = 销量 / (1 - 损耗率)。这样写的好处是把损耗直接折进补货量里,不用单独再减一次。但要注意:如果题目允许打折清仓,那损耗率会变,目标函数要分段。

我一般会先写一个简化版:假设当天必须清完,售价和销量满足线性需求函数,然后求最优售价。再逐步加约束:补货量上限、品类最小陈列量、价格不能低于成本等。别一上来就写全约束,否则求解器容易不收敛。

3.2 用 scipy 做单品类最优定价

先拿一个品类练手。假设需求函数是线性的:销量 = a - b × 售价。用历史数据拟合出 a 和 b,然后代入利润函数求极值。

from scipy.optimize import minimize_scalar # 拟合线性需求:销量 = a - b * 售价 X = cat_data['均价'] y = cat_data['销量'] b, a = np.polyfit(X, y, 1) # 利润函数:售价 * 销量 - 批发价 * 补货量 批发价 = cat_data['批发价'].mean() 损耗率 = 0.2 def profit(price): 销量 = a - b * price if 销量 <= 0: return 1e6 # 惩罚无效解 补货量 = 销量 / (1 - 损耗率) return -(price * 销量 - 批发价 * 补货量) # 取负求最小 res = minimize_scalar(profit, bounds=(批发价, 批发价*3), method='bounded') print('最优售价:', res.x, '最大利润:', -res.fun)

这段代码用minimize_scalar做一维搜索。参数上:bounds设成批发价到三倍批发价,是防止价格低到亏本或高到没人买;损耗率先用品类平均,后面可以换成动态值。注意profit返回负值是因为 scipy 默认求最小。如果销量算出来是负的,直接给个大惩罚值,让求解器避开。

3.3 多品类补货:把线性规划用起来

单品类定价只是热身,真题要求的是多品类联合决策。这时候可以用线性规划或整数规划。决策变量是每个品类每天的补货量和售价,约束包括:总补货量不超过仓库容量、每个品类补货量不低于最小陈列、售价在合理区间。目标还是利润最大化。

from scipy.optimize import linprog # 假设有3个品类,决策变量为补货量 x1,x2,x3 # 目标:最大化利润 -> 最小化负利润 # 利润系数 = 售价*销量 - 批发价*补货量,这里简化为线性 c = [-10, -12, -8] # 负的单位利润 # 约束:总补货量 <= 100 A_ub = [[1, 1, 1]] b_ub = [100] # 每个品类补货量 >= 5 bounds = [(5, None), (5, None), (5, None)] res = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs') print('最优补货量:', res.x)

这里用的是linprog,参数c是负的单位利润,因为要最小化。A_ub和b_ub是总补货量约束,bounds是每个品类的最小补货量。实际题目里,单位利润不是常数,它跟售价和销量有关,所以更准确的做法是用非线性规划或分段线性化。但作为起步,先用线性规划跑通流程,再逐步替换目标函数。

3.4 动态规划处理“今天不卖完明天就烂”

如果题目强调损耗和时段,比如当天没卖完的菜第二天只能打折或扔掉,那就得用动态规划。状态是“当前库存”,决策是“定价和补货”,转移是“明天库存 = 今天补货 - 今天销量”。我一般会按天滚动,用后向归纳求最优策略。

# 简化动态规划:假设需求是随机的,用期望利润 import numpy as np def dp_optimal(T, max_inventory, demand_samples): # V[t][i] 表示第t天库存为i时的最大期望利润 V = np.zeros((T+1, max_inventory+1)) policy = np.zeros((T, max_inventory+1)) for t in range(T-1, -1, -1): for i in range(max_inventory+1): best_profit = -1e9 best_order = 0 for order in range(max_inventory - i + 1): expected = 0 for d in demand_samples: sold = min(i + order, d) revenue = sold * 10 # 假设售价10 cost = order * 5 # 假设批发价5 leftover = i + order - sold expected += revenue - cost - leftover * 2 # 损耗惩罚 expected /= len(demand_samples) if expected + V[t+1][max(0, i+order-sold)] > best_profit: best_profit = expected best_order = order V[t][i] = best_profit policy[t][i] = best_order return V, policy

这段代码是动态规划的骨架。V[t][i]是第 t 天库存 i 时的最大期望利润,policy存最优补货量。参数上:demand_samples是历史需求样本,用来算期望;leftover * 2是损耗惩罚,可以换成实际损耗成本。注意状态空间别开太大,否则跑不动。实际用的时候,可以把品类拆开分别做,再在总库存约束下协调。

4. 避坑与排查:这道题最容易翻车的五个地方

4.1 销量和补货量口径搞反

现象:算出来的补货量比销量还小,利润高得离谱。原因:把“销量”当成“补货量”直接用了,忽略了损耗。解决:先确认损耗率是按上架量还是销售量算的,补货量 = 销量 / (1 - 损耗率) 只在上架量口径下成立。拿几天实际数据反推验证。

4.2 价格弹性估成正数

现象:回归出来价格系数是正的,价格越高销量越高。原因:没控制季节和促销,或者把打折价和原价混在一起。解决:加日期虚拟变量,把打折记录单独标记,或者只用非打折日数据估弹性。如果还是正,检查是不是把“售价”和“销量”的因果搞反了。

4.3 损耗率用成常数

现象:模型在夏天预测的补货量偏低,冬天偏高。原因:损耗率随季节变化,用年平均损耗率会失真。解决:按品类和月份分组算损耗率,或者加季节因子。如果数据不够,至少按季度分。

4.4 优化求解器不收敛

现象:linprog或minimize报错,或者结果全是边界值。原因:约束矛盾,比如最小补货量之和超过仓库容量;或者目标函数非线性太强。解决:先检查约束可行性,把硬约束改成软约束加惩罚项;非线性强就分段线性化,或者用启发式算法先找可行解。

4.5 忽略品类替代

现象:叶菜降价后,根茎销量也降了,模型没捕捉到。原因:只做了单品类优化,没考虑品类之间的替代关系。解决:在需求函数里加交叉价格项,或者用向量自回归估计品类间影响。如果时间紧,至少把替代性强的品类放一起联合优化。

5. 从论文到源码:怎么把结果讲清楚、跑出来

5.1 论文里必须有的三张表

第一张是数据清洗前后的对比表,说明你处理了多少异常值、缺失值。第二张是价格弹性估计表,按品类列出弹性系数和显著性。第三张是最优定价与补货结果表,按天或按品类给出建议售价、补货量和预期利润。这三张表能把你的逻辑链完整呈现,评审一看就知道你不是拍脑袋。

5.2 源码怎么组织才方便复现

我一般会分四个文件:data_preprocess.py负责读数据和清洗,demand_estimate.py负责估弹性和需求函数,optimize.py负责定价和补货求解,main.py串流程。每个文件里把参数写在开头,比如损耗率、价格区间、仓库容量,方便别人改。不要把所有代码塞一个文件,否则调参时容易乱。

# main.py 示例 from data_preprocess import load_and_clean from demand_estimate import estimate_elasticity from optimize import optimize_price_order data = load_and_clean('sales.csv', 'wholesale.csv', 'category.csv') elasticity = estimate_elasticity(data) result = optimize_price_order(data, elasticity, loss_rate=0.2, capacity=100) print(result)

这样组织的好处是每一步可单独测试。参数loss_rate和capacity放在调用处,改起来不用翻代码。如果题目要求输出每天每个品类的决策,就在optimize里循环日期,把结果存成 CSV。

5.3 验证方法:用历史数据回测

别只跑一天就下结论。把历史数据分成训练集和测试集,用训练集估参数,在测试集上模拟定价补货,算实际利润。如果测试集利润比实际低太多,说明模型过拟合。我一般会看两个指标:预测销量和实际销量的 MAPE,以及模拟利润和实际利润的偏差。MAPE 超过30%就得回头检查需求函数。

5.4 一个具体技巧:把损耗率做成动态惩罚项

如果题目允许,可以在目标函数里加一个动态损耗惩罚:损耗率越高,补货量惩罚越大。这样模型会自动偏向少补货,避免烂市。具体做法是在利润函数里减一项lambda * 损耗率 * 补货量,lambda用历史数据调。这个技巧在叶菜类特别管用,因为叶菜损耗波动大,固定损耗率容易翻车。

我做完这道题最大的教训是:别急着上复杂模型,先把数据口径和损耗率搞对,再谈优化。很多队伍卡住不是因为算法不行,而是因为销量和补货量没对齐。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询