简介:本资源是2023年全国大学生数学建模竞赛C题《蔬菜类商品自动定价与补货决策优化》的完整参赛成果,面向计算机、统计、运筹与管理科学等专业的本科生及毕业设计需求者,聚焦生鲜零售场景下的动态定价、损耗建模与多目标补货决策问题。压缩包共46个文件(11.9MB),含24个核心数据表(如单品销售量、平均损耗率、ARIMA与GRU预测结果等)、9个Jupyter Notebook(覆盖聚类分析、箱线图可视化、回归建模、混合整数规划求解及深度学习预测全流程)、3个Python主程序(含BONMIN调用脚本)及1篇定稿论文PDF,结构清晰、模块解耦,支持开箱即跑。已有103人学习下载,资源附带详细README说明,所有代码经实测可运行,涵盖数据清洗、特征工程、时序预测(ARIMA/GRU)、回归建模、多目标优化建模与结果可视化全链路,特别适合毕设选题、课设开发与建模能力进阶训练。
1. 项目概述:从数学建模到商业决策的实战跨越
去年带队参加全国大学生数学建模竞赛,我们组选了C题,题目是关于蔬菜类商品的自动定价与补货决策优化。这题目一出来,当时队里几个同学眼睛都亮了,觉得这玩意儿离生活近,有搞头。但真上手才发现,从“有搞头”到“搞出来”,中间隔着十万八千行代码和无数个掉头发的夜晚。这不仅仅是一道数学题,它本质上是一个融合了数据分析、运筹优化和商业逻辑的综合性系统工程。今天,我就把我们从审题、建模、求解到代码实现的完整心路历程和实战细节掰开揉碎了讲,希望能给未来参赛的学弟学妹,或者对数据驱动决策感兴趣的朋友们,提供一个实实在在的参考模板。
这个项目的核心目标很明确:给你一家生鲜超市过去一段时间内,六种单品蔬菜的销售流水、损耗记录以及批发价格数据,让你设计一套算法模型,既能自动给出未来一周每一天的最优定价,又能智能决策每天该补多少货。听起来是不是很像电商后台那个“智能补货与定价系统”?没错,这就是一个高度简化的商业智能雏形。它考验的绝不仅仅是你的数学公式写得漂不漂亮,更是你如何将凌乱的现实数据抽象成可计算的模型,如何平衡收入、成本与损耗之间的矛盾,以及如何将复杂的优化问题落地成可执行的代码。整个过程,我们踩了不少坑,也总结了不少“教科书上不会写”的实战经验,接下来我就分模块详细道来。
2. 核心问题拆解与建模总览
面对“定价”与“补货”这两个纠缠在一起的核心决策,直接上手建模很容易思路混乱。我们的第一步是进行彻底的问题解耦与定义,这是后续所有工作的基石。
2.1 决策变量与目标函数定义
首先,我们必须用数学语言清晰地定义我们要“优化”什么。题目要求兼顾商超的“收益最大化”和“蔬菜销售量的分布尽可能均匀”,这本身就是一个多目标优化问题。在实际处理中,我们将其转化为一个带约束的单目标优化问题,这是工程上更可行的思路。
决策变量:
- 定价变量:设未来第 ( t ) 天(( t=1,2,...,7 )),第 ( i ) 种蔬菜(( i=1,2,...,6 ))的销售单价为 ( p_{i,t} )。
- 补货变量:设未来第 ( t ) 天,第 ( i ) 种蔬菜的补货量为 ( q_{i,t} )(注意,补货在当天早上完成,用于满足当天的销售)。
目标函数: 我们的核心目标是最大化未来一周的总利润。总利润 = 总收入 - 总成本。其中:
- 总收入:未来七天所有蔬菜的销售额总和,即 ( \sum_{t=1}^{7}\sum_{i=1}^{6} (p_{i,t} \times d_{i,t}) )。这里的 ( d_{i,t} ) 是第 ( i ) 种蔬菜在第 ( t ) 天的预测需求量,它是价格 ( p_{i,t} ) 的函数,这是模型的关键。
- 总成本:主要包括两部分:
- 进货成本:( \sum_{t=1}^{7}\sum_{i=1}^{6} (c_{i,t} \times q_{i,t}) ),其中 ( c_{i,t} ) 是第 ( t ) 天第 ( i ) 种蔬菜的批发价(题目给出)。
- 损耗成本:当天未售出且腐败的蔬菜带来的损失。这是最容易出错的地方。损耗不是简单的“进货-销售”,因为蔬菜有保质期。我们引入了库存状态变量( I_{i,t} ) 来表示第 ( t ) 天营业结束后第 ( i ) 种蔬菜的库存量。其更新公式为:( I_{i,t} = I_{i,t-1} + q_{i,t} - d_{i,t} )。那么,第 ( t ) 天结束时,那些已经存放超过保质期(比如,假设蔬菜保质期为3天)的库存部分就会腐烂。我们需要追踪每一批进货的“年龄”,计算准确的损耗量,再乘以成本价或残值(我们按成本价计算损失),计入总成本。
因此,最终的目标函数是最大化:总利润 = 总收入 - (进货成本 + 损耗成本)。
注意:这里有一个巨大的坑。很多初版模型会忽略库存的“时效性”,简单地用
期末库存 * 成本价来计算损耗,这严重高估了损耗成本,因为只有过期的部分才计损。我们采用了“先进先出”(FIFO)的库存流转假设,并设计了库存年龄追踪矩阵,才实现了精确的损耗计算。
2.2 核心约束条件分析
光有目标不行,决策必须在不违反现实规则的前提下进行,这些规则就是约束条件。
- 需求预测约束(价格-需求关系):这是模型的灵魂。需求量 ( d_{i,t} ) 不能随意设定,它必须通过一个需求预测模型与售价 ( p_{i,t} ) 关联起来。我们使用了弹性需求模型:( d_{i,t} = D_{i,t} \times (p_{i,t} / P_{i,base})^{-\epsilon_i} )。
- ( D_{i,t} ):第 ( i ) 种蔬菜在第 ( t ) 天的基准需求量(即在不调价情况下的预测值)。
- ( P_{i,base} ):第 ( i ) 种蔬菜的基准价格(我们取历史平均售价或上一期价格)。
- ( \epsilon_i ):第 ( i ) 种蔬菜的需求价格弹性系数(>0)。弹性系数需要通过历史数据拟合得到。这步的准确性直接决定了整个优化结果是否可靠。
- 库存动态平衡约束:如前所述,( I_{i,t} = I_{i,t-1} + q_{i,t} - d_{i,t} ),且 ( I_{i,t} \ge 0 )。
- 补货量上下限约束:每天补货量有最小和最大限制,即 ( q_{i,min} \le q_{i,t} \le q_{i,max} )。这可能是由于仓储空间、供应商能力或管理规则决定的。
- 售价浮动范围约束:售价不能无限调整,通常限制在基准价格上下一定百分比内,例如 ( 0.7 \times P_{i,base} \le p_{i,t} \le 1.3 \times P_{i,base} )。
- 非负与逻辑约束:所有决策变量(价格、补货量、需求量)必须非负。需求量不能超过当日可用库存(期初库存+补货量),即 ( d_{i,t} \le I_{i,t-1} + q_{i,t} )。
- 销售均匀性惩罚项(可选):为了满足“销售量分布均匀”的次要目标,我们在目标函数中增加了一个惩罚项,例如,惩罚每天销售量与平均销售量的偏差平方和。这样,当利润相差不大时,模型会更倾向于选择销售更平稳的方案。
通过以上定义,我们将一个模糊的商业问题,转化为了一个具有清晰决策变量、目标函数和约束条件的非线性规划(NLP)问题。因为需求函数是关于价格的非线性函数(幂函数)。
3. 数据预处理与需求预测模型构建
巧妇难为无米之炊,再漂亮的模型也需要扎实的数据支撑。题目提供的数据通常包含销售流水、损耗记录和批发价三个表,原始数据往往存在噪音、缺失和 inconsistencies。
3.1 多源数据清洗与融合
- 销售流水表:包含日期、单品编码、销量、售价。清洗重点:
- 异常值处理:检查是否存在销量极大但售价极低(可能是促销录入错误),或售价远高于正常范围的数据。我们采用“3σ原则”结合业务常识进行筛选或修正。
- 缺失值处理:对于某天完全没有销售记录的蔬菜,不能简单填0。这可能意味着缺货。我们需要结合当天的损耗记录和后续日期的销售情况来判断,如果是缺货,则该日需求未被满足,在预测基准需求时应谨慎处理。
- 损耗登记表:包含日期、单品编码、损耗量。这是计算实际售出量的关键。实际售出量 = 销量 + 损耗量。清洗时需注意损耗量是否为负(理论上不应出现),以及是否与销售流水表中的数据在日期和单品上能正确关联。
- 批发价格表:包含日期、单品编码、批发价。注意批发价可能有波动,需要按日期准确匹配到每天的补货成本计算中。
清洗后,我们将三张表基于“日期”和“单品编码”进行关联,生成一张包含每日、每单品“销量”、“损耗量”、“售价”、“批发价”的宽表。这是后续所有分析的基础。
3.2 基准需求 ( D_{i,t} ) 预测
基准需求是指在价格不变的情况下,未来的销量会是多少。我们采用了时间序列分解+回归的组合方法。
- 趋势与季节分解:使用STL(Seasonal and Trend decomposition using Loess)或简单的移动平均法,剔除数据中的长期趋势和周期性(周季节性)成分。生鲜蔬菜的销售通常有很强的周内效应,比如周末销量高,周一销量低。
- 特征工程:除了时间趋势,我们还加入了是否周末、是否节假日、节假日前第几天等作为特征。
- 模型选择与训练:对于6个单品,我们分别训练了6个模型。尝试了LightGBM和Prophet两种。
- LightGBM:将分解后的趋势项、季节项以及人工特征作为输入,预测未来7天的需求。优点是能捕捉复杂非线性关系,且训练速度快。
- Prophet:Facebook开源的时序预测工具,对季节性和节假日效应处理非常友好,几乎开箱即用。实操心得:对于数学建模竞赛,Prophet是更稳妥快捷的选择,因为它减少了特征工程的负担,且结果可解释性强。我们最终主要使用Prophet进行基准预测,并用LightGBM的结果作为交叉验证和补充。
3.3 需求价格弹性 ( \epsilon_i ) 估计
这是连接价格与需求的桥梁。我们使用历史数据,对每个单品 ( i ),拟合对数线性回归模型: [ \ln(d_{i,t}) = \alpha_i + \epsilon_i \cdot \ln(p_{i,t}) + \gamma \cdot X_t + \epsilon_{i,t} ] 其中,( X_t ) 是控制变量,如星期几哑变量、节假日哑变量等,用于控制除价格外其他影响需求的因素。拟合得到的 ( \epsilon_i ) 系数即为需求价格弹性。
注意事项:这里存在严重的内生性问题。价格和销量是同时决定的,可能存在反向因果关系(销量低导致降价,而非降价导致销量高)。在严谨的学术研究中,需要使用工具变量法等解决。但在竞赛有限的时间和数据下,我们做了简化处理:1)使用滞后一期的价格作为工具变量的代理;2)在模型解读时明确指出这一局限性。这体现了建模中“近似艺术”的一面。
4. 优化模型求解与算法实现
至此,我们有了所有模型要素:目标函数、约束条件、预测出的 ( D_{i,t} ) 和估计出的 ( \epsilon_i )。问题归结为求解一个大规模的非线性规划问题(6个单品 * 7天 = 42个价格变量 + 42个补货变量)。
4.1 求解器选择与问题转化
我们选择了Pyomo作为建模语言,调用IPOPT求解器进行求解。Pyomo允许我们用接近数学公式的方式描述优化问题,非常直观。
import pyomo.environ as pyo # 创建模型 model = pyo.ConcreteModel() # 定义集合:单品(I)和天数(T) model.I = pyo.Set(initialize=items_list) model.T = pyo.Set(initialize=days_list) # 定义变量:价格(p),补货量(q),需求量(d) model.p = pyo.Var(model.I, model.T, bounds=(p_min, p_max)) model.q = pyo.Var(model.I, model.T, bounds=(q_min, q_max)) model.d = pyo.Var(model.I, model.T, bounds=(0, None)) # 定义目标函数:最大化总利润 def profit_rule(model): revenue = sum(model.p[i,t] * model.d[i,t] for i in model.I for t in model.T) cost_purchase = sum(wholesale_price[i,t] * model.q[i,t] for i in model.I for t in model.T) cost_waste = calculate_waste_cost(model) # 这是一个自定义函数,基于FIFO计算损耗 return revenue - cost_purchase - cost_waste model.profit = pyo.Objective(rule=profit_rule, sense=pyo.maximize) # 定义需求函数约束 def demand_relation_rule(model, i, t): # D_base[i,t] 是预测的基准需求, epsilon[i] 是弹性系数, p_base[i] 是基准价格 return model.d[i,t] == D_base[i,t] * (model.p[i,t] / p_base[i]) ** (-epsilon[i]) model.demand_rel = pyo.Constraint(model.I, model.T, rule=demand_relation_rule) # 定义库存平衡约束 def inventory_balance_rule(model, i, t): if t == 1: return model.I[i,t] == initial_inventory[i] + model.q[i,t] - model.d[i,t] else: return model.I[i,t] == model.I[i, t-1] + model.q[i,t] - model.d[i,t] model.inv_balance = pyo.Constraint(model.I, model.T, rule=inventory_balance_rule) # ... 其他约束(如需求不超过可用库存等) # 求解 solver = pyo.SolverFactory('ipopt') results = solver.solve(model, tee=True) # tee=True 可以打印求解过程为什么选择IPOPT?因为它是一款强大的开源非线性规划求解器,特别适合处理我们这种具有光滑非线性目标函数和约束的问题。相比遗传算法、模拟退火等启发式算法,IPOPT能更快、更稳定地找到高质量的局部最优解(对于凸问题,就是全局最优)。
4.2 库存与损耗计算的实现细节
这是代码中最繁琐但最关键的部分。我们实现了一个InventoryTracker类来模拟FIFO库存流转。
class InventoryTracker: def __init__(self, shelf_life=3): self.shelf_life = shelf_life # 保质期,假设为3天 # 用一个字典记录库存,键为“批次年龄”(0表示当天进的货),值为数量 # 例如:{0: 50, 1: 30, 2: 20} 表示当天新进50,前一天剩30,前两天剩20 self.inventory_age_map = {age: 0 for age in range(shelf_life)} def add_inventory(self, quantity): """早上补货""" self.inventory_age_map[0] += quantity def sell(self, demand): """销售,按FIFO从最旧的库存开始扣减""" sold = 0 for age in sorted(self.inventory_age_map.keys(), reverse=True): # 从最旧的(年龄最大的)开始卖 if sold >= demand: break can_sell = min(self.inventory_age_map[age], demand - sold) self.inventory_age_map[age] -= can_sell sold += can_sell return sold # 返回实际销量(可能小于需求,如果库存不足) def end_of_day(self): """日结操作:库存年龄增加1天,过期的丢弃并计算损耗""" waste = self.inventory_age_map.get(self.shelf_life - 1, 0) # 年龄为 shelf_life-1 的库存明天就过期,今天结束时丢弃 # 库存年龄滚动 for age in range(self.shelf_life - 1, 0, -1): self.inventory_age_map[age] = self.inventory_age_map[age - 1] self.inventory_age_map[0] = 0 # 新进货的年龄0位置已由add_inventory更新,这里重置为0等待明天的补货 return waste # 在优化模型中,calculate_waste_cost 函数会为每个单品维护一个InventoryTracker对象, # 并根据求解器给出的补货量(q)和需求量(d)序列,模拟7天的库存变化,累计损耗成本。这个模拟器确保了损耗计算的精确性,使得优化模型的目标函数能真实反映库存策略的成本影响。
5. 结果分析与策略解读
求解器跑完后,我们会得到未来7天所有单品的最优价格和补货量序列。但这仅仅是数字,更重要的是解读其背后的商业逻辑。
5.1 定价策略分析
模型给出的定价通常呈现以下规律:
- 高弹性商品:如常见叶菜(生菜、菠菜),需求对价格敏感(弹性系数大)。模型倾向于在需求高峰日(如周末)小幅降价以显著提振销量,从而增加总收入;在需求低谷日则维持或略提价,因为降价带来的销量增长不足以弥补单价损失。
- 低弹性商品:如调味类蔬菜(姜、蒜),需求相对刚性。模型策略更倾向于稳定或提高价格,因为降价对销量刺激作用有限,提价对销量影响也不大,提价能直接提高利润。
- 临近保质期:对于库存中临近过期的批次,模型会在其过期前一日通过显著降价(“每日清货”策略)来刺激消费,减少损耗损失。这体现在价格序列中偶尔出现的“深V”低点。
5.2 补货策略分析
补货量与定价紧密联动:
- “以销定产”模式:补货量紧密跟随预测的需求量。在预计大卖的日子(配合定价策略)前,补货量会显著上升。
- 库存缓冲:考虑到预测误差和需求的随机性,最优补货量通常会略高于预测需求,形成一个安全库存。但安全库存的水平受到仓储成本(隐含在库存上限约束中)和损耗风险的制约。
- “小批量、多频次”倾向:在模型约束允许的范围内(如果补货下限不为0),为了减少因预测不准和蔬菜腐败带来的风险,模型倾向于每天补货,而不是一次性补足多天的量。这符合生鲜零售的“高周转”特性。
我们将这些策略总结成一份给“商超经理”的可执行决策表,包含未来七天每天每种蔬菜的建议售价、建议补货量、预计销量和预计期末库存,并附上关键风险提示(如“周四生姜建议售价较高,需关注顾客接受度”)。
6. 模型稳健性检验与敏感性分析
一个模型好不好,不仅要看它在“理想路径”上的表现,更要看它在“意外情况”下的鲁棒性。我们设计了以下检验:
- 历史数据回测:将模型应用到历史数据的最后一周(假设我们不知道那一周的真实数据),用模型给出的决策模拟当周运营,计算模拟利润,并与当周实际利润对比。这能检验模型在“过去”的表现。
- 预测误差冲击测试:人为地将基准需求预测 ( D_{i,t} ) 上下浮动10%、20%,重新运行优化模型,观察最优决策和预期利润的变化。如果利润波动剧烈,说明模型对预测精度依赖过高,决策风险大。我们可能需要调整模型,例如在目标函数中增加对库存波动的惩罚,使策略更保守。
- 参数敏感性分析:
- 弹性系数 ( \epsilon_i ):这是最关键的参数。我们测试了将弹性系数增大或减小一个标准误,观察定价策略的变化。结果发现,高弹性商品的定价策略对弹性值非常敏感,这提醒我们在实际应用中必须投入资源精准估计弹性。
- 批发价波动:模拟批发价突然上涨10%的情景。模型会做出两种反应:一是提高售价转嫁成本(对低弹性商品),二是减少补货量并更激进地促销以减少库存持有风险(对高弹性商品)。
- 极端场景模拟:模拟“周末突然下雨导致客流减少20%”的情景。我们将周末的基准需求预测值调低20%,重新优化。观察模型是否会动态调整周五的补货计划(减少)和周末的定价策略(进一步降价清库存)。
通过这些分析,我们不仅验证了模型的有效性,更重要的是评估了其决策的风险边界,为模型的实际应用提供了重要的安全垫。
7. 参赛全流程复盘与避坑指南
回顾整个参赛过程,从拿到题目到提交论文代码,有几个关键节点最容易出问题,这里集中分享我们的“血泪教训”。
7.1 时间管理与任务分工
数学建模竞赛是团队战,72小时高强度作战,时间管理就是生命线。
- 第一天上午(3-4小时):必须完成全体成员对题目的深度理解与讨论,形成统一的建模思路框架。切忌各自为战。我们当时就花了半天争论该用随机规划还是确定性模型,浪费了宝贵时间。
- 第一天下午到晚上:数据清洗和探索性分析(EDA)必须完成。同时,负责建模的同学应开始构建核心模型的数学公式。并行工作是关键。
- 第二天全天:是核心攻坚期。算法实现、模型求解、初步结果分析必须在这一天完成。哪怕结果不完美,也要有一个能跑通的版本。
- 第三天上午:模型优化、敏感性分析、结果可视化。下午到晚上:论文写作与整合。千万不要把论文留到最后半天写,那会是一场灾难。写作应从第一天就开始记录思路,第二天填充结果,第三天整合润色。
- 分工建议:一人主攻建模与算法(数学好、编程强),一人主攻数据与可视化(心细、会用绘图工具),一人主攻论文写作与统筹(逻辑清晰、文笔好)。但三人必须对所有环节都有所了解,能互相备份。
7.2 论文写作的核心得分点
评委看论文,就像看一个产品的说明书和设计报告。
- 摘要:重中之重!要用300-500字清晰说明“针对什么问题、建立了什么模型、用了什么方法、得到了什么结果、有何特色”。我们采用了“问题-模型-方法-结果-创新点”的五段式结构,几乎把全文精华浓缩于此。
- 模型假设:必须清晰、合理。例如,“假设蔬菜需求符合弹性需求模型”、“假设库存流转遵循FIFO原则”、“忽略运输成本和其他运营费用”。好的假设既能简化问题,又体现了你对现实的理解。
- 模型建立:公式要规范、编号要连续、符号说明要完整。最好能用一个框图(我们在论文中画了一个“模型框架图”)来展示变量之间的关系和模型流程。
- 模型求解:说明你用的算法、软件、以及为什么选它。给出关键代码片段(如核心的优化模型定义),但不要贴全部代码。
- 结果分析:不要只摆数字。要用图表(如未来七天价格与补货量的变化趋势图)直观展示,并结合业务逻辑进行解读。灵敏度分析部分往往是拉开差距的地方,要展示你思考的深度。
- 模型评价与推广:客观评价模型的优点(如综合考虑定价与补货联动)和缺点(如未考虑竞争对手反应、需求预测存在误差),并提出可能的改进方向(如引入随机规划处理需求不确定性)。
7.3 代码与数据处理的实战技巧
- 版本控制:从第一天起就用Git。每天结束时提交一次,标注清楚更新内容。这能在代码混乱或误删时救命。
- 模块化编程:将数据清洗、预测模型、优化模型、结果分析分别写成独立的函数或脚本。例如
data_clean.py,demand_forecast.py,optimization_model.py,analysis_plot.py。这样调试起来非常方便。 - 中间结果缓存:预测模型训练、参数估计都很耗时。使用
pickle或joblib库把训练好的模型、估计好的弹性系数保存下来,避免每次运行都要重新训练。 - 可视化先行:在建模初期,就多画图。画销售时间序列图、价格与销量的散点图、库存变化图。图形能帮你快速发现数据规律和模型异常,比盯着数字高效得多。
- 求解失败怎么办:IPOPT有时会因初始值不好而求解失败。我们的经验是:给决策变量(价格、补货量)一个合理的初始值(如价格设为历史均价,补货量设为历史平均销量),能极大提高求解成功率。在Pyomo中,可以用
model.p[i,t] = initial_value来设置初始值。
参加一次数学建模竞赛,其价值远超一份获奖证书。它逼着你把一个模糊的实际问题,用数学和代码的力量,变成一个清晰、可量化、可执行的解决方案。这个过程里锻炼的系统思维、数据处理、模型构建和团队协作能力,才是未来无论做科研还是进工业界都无比宝贵的财富。我们这套针对蔬菜定价补货的模型框架,其核心思想——数据预测驱动、在约束下优化全局目标——完全可以迁移到其他零售品类、库存管理甚至广告投放等场景。关键不在于记住了多少公式,而在于掌握了这种“定义问题-抽象建模-求解验证”的思维模式。最后,送上一句我们队当时的座右铭:“模型是简化的现实,但好的简化比复杂的现实更有力量。”与诸位共勉。
本文还有配套的精品资源,点击获取