供应链优化实战:需求预测、定价与库存决策模型解析
1. 赛题核心与破题思路从“蔬菜类商品”到“供应链优化”每年九月的那个周末对于全国几十万数学建模爱好者来说都是一场没有硝烟的“头脑风暴”。2023年的C题题目一出来很多同学就有点懵——“蔬菜类商品的自动定价与补货决策”。这听起来不像传统的物理模型或优化问题更像是一个商业运营问题。但恰恰是这种贴近实际应用的题目最能考察我们如何将数学工具转化为解决现实问题的能力。这道题的核心说白了就是给一家生鲜超市当三天“智慧大脑”。你手头有过去四年各种蔬菜的销售流水、损耗记录还有每天的进货成本。你的任务就两个第一根据历史数据给未来一周的每种蔬菜定一个最合理的售价第二告诉超市每天每种菜该进多少货。目标很明确在满足各种约束比如顾客满意度、库存容量的前提下让超市这三天的总利润尽可能高。这背后考的是什么绝不仅仅是套个回归模型预测销量那么简单。它考察的是对一个完整供应链决策闭环的理解需求预测 → 定价策略 → 库存决策 → 利润评估。任何一个环节的模型选择不当都会导致整个链条的崩溃。比如如果你用简单的线性回归预测销量但忽略了蔬菜销量受价格、星期几、节假日影响的非线性关系你的预测就会失准如果你只追求利润最大化把菜价定得老高导致销量锐减、损耗激增最终利润反而可能为负。所以破题的第一步不是急着找代码而是拆解问题建立逻辑框架。我们可以把整个问题分解为几个关键模块数据理解与预处理模块清洗销售数据识别异常值比如促销日的爆量分析销量、价格、损耗率随时间日、周、年和品类的变化规律。需求预测模块这是整个模型的基石。需要预测未来一周在不同定价策略下每种蔬菜的日销量。这涉及到价格弹性分析。定价决策模块在预测的需求基础上以最大化单日或阶段利润为目标求解最优价格。这通常是一个带约束的优化问题。补货决策模块根据预测销量、定价决策、当前库存、损耗率以及库存容量限制决定每天的最佳补货量。这本质上是一个库存控制问题可能用到报童模型或其扩展形式。模拟与评估模块将上述决策代入未来三天的模拟环境中计算总利润、平均满意度等指标评估策略优劣并可能需要进行策略调整。这个框架就像盖房子的蓝图有了它你才知道每一块砖每个模型该放在哪里以及它们之间如何衔接。接下来我们就深入每个模块看看具体怎么“砌砖”。1.1 数据基石从杂乱流水到清晰洞见题目给的数据通常是Excel或CSV格式的销售明细表可能包含商品编码、商品名称、销售日期、销售单价、销售数量、是否打折、损耗数量、批发价成本等字段。第一步不是跑模型而是“读懂”数据。1.1.1 关键预处理步骤规整时间序列确保数据按天连续。对于没有销售记录的天可能是门店休息或该商品缺货要谨慎处理。缺货日不能简单视为销量为0可能需要结合前后数据判断或标记为缺失值。处理异常值与缺失值促销日大幅降价带来的销量暴增是正常现象不应作为异常值剔除但需要在建模时引入“是否促销”作为哑变量。数据错误如销售数量为负、单价明显偏离正常范围如黄瓜单价100元/kg需根据业务逻辑修正或剔除。缺失成本某天某种菜的批发价可能缺失可用前后几天的均值、或同期去年同星期的成本进行插补。特征工程这是提升模型性能的关键。从原始日期中可以提取出大量有价值的信息时序特征年、月、日、一年中的第几天、一周中的第几天星期几。星期几的影响通常非常显著周末买菜人多。滞后特征前1天、前7天上周同一天的销量、价格。这对于捕捉短期趋势和周期性至关重要。滑动统计特征过去7天、30天的平均销量、销量标准差、平均价格等。反映近期销售水平和波动性。事件特征是否为节假日、节气如立冬可能影响某些蔬菜需求、促销日。品类特征蔬菜的类别叶菜、根茎、瓜果等可以编码后作为特征。1.1.2 探索性数据分析画图这是最直观的方法。销量时间序列图观察整体趋势长期是增长还是下降、季节性每年是否有固定高峰低谷、周期性每周波动是否明显。价格-销量散点图初步观察价格弹性。对于每个单品可以画出历史价格和销量的关系看看是否呈现负相关价格越高销量越低。箱线图按星期几分组画销量的箱线图可以清晰看到周内效应。损耗率分析计算每种菜的历史平均损耗率分析损耗率与销量、库存天数的关系。高损耗的菜品在补货时需要更加保守。注意预处理没有“标准答案”每一步操作都要有业务逻辑支撑并且最好在报告中说明你如何处理以及为什么这样处理。这体现了你对问题的理解深度。2. 模型构建预测、定价与补货的三重奏数据准备好后就进入核心的模型构建环节。这三个环节环环相扣需要协同考虑。2.1 需求预测模型不止于回归预测未来一周在不同价格下的销量这是难点。一个经典的思路是构建需求函数。我们假设对于单品i在第t天的销量 ( Q_{it} ) 是其自身价格 ( P_{it} )、其他相关商品价格、以及一系列外部特征 ( X_t )如星期几、节假日的函数。2.1.1 基础模型线性回归与价格弹性最简单的需求函数是线性形式 [ Q_{it} \alpha_i \beta_i P_{it} \gamma^T X_t \epsilon_{it} ] 其中( \beta_i ) 就是价格弹性系数通常为负值。通过历史数据回归得到参数后给定一个未来价格 ( P_{it} )就能预测销量 ( Q_{it} )。但问题来了线性假设可能太强。现实中价格对销量的影响可能是非线性的。降价10%带来的销量增长可能与降价20%带来的增长不成比例。2.1.2 进阶模型更灵活的需求刻画对数线性模型( \ln(Q_{it}) \alpha_i \beta_i \ln(P_{it}) \gamma^T X_t \epsilon_{it} )。此时 ( \beta_i ) 表示需求的价格弹性即价格变化1%导致销量变化百分之几。这在经济学中更常用。机器学习模型当关系复杂时可以考虑树模型如LightGBM、XGBoost或神经网络。它们能自动捕捉非线性关系和特征交互。例如星期几和价格可能存在交互效应周末消费者对价格可能更不敏感。优势预测精度可能更高。劣势模型可解释性差难以直接导出用于优化定价的数学形式。通常需要将其作为“黑箱”预测器嵌入后续的优化循环中进行搜索。2.1.3 考虑品级与损耗题目中蔬菜有不同品级如一级、二级通常品级越高成本和售价也越高但需求可能不同。在预测时可以将同一品种的不同品级视为高度相关但不同的商品分别建模或引入“品级”作为特征。 损耗率会影响可售销量。假设预测出的 ( Q_{it} ) 是理论需求实际补货量需要满足这个需求但过程中会有损耗。一种简化方式是所需进货量 预测销量 / (1 - 平均损耗率)。更精细的做法是建立损耗率预测模型。2.2 定价决策模型在约束下寻找最优解预测模型给出了“如果定价为P预计能卖Q”的关系。定价模型要回答“那么P到底该定多少”2.2.1 单日单产品定价这是一个经典的优化问题。对于第t天单品i决策变量售价 ( P_{it} )。目标函数最大化当日毛利润 ( \pi_{it} (P_{it} - C_{it}) * Q_{it}(P_{it}) )。其中 ( C_{it} ) 是成本( Q_{it}(P_{it}) ) 是由需求预测模型给出的函数。约束条件价格变动范围通常不能超过历史最高最低价太多或题目会给出限制。顾客满意度价格不能过高题目可能要求销量不低于某个值或价格涨幅不超过某个比例。这可以转化为对 ( Q_{it} ) 的约束或对 ( P_{it} ) 的上下限约束。如果需求函数 ( Q(P) ) 是简单的线性或对数线性形式那么利润函数 ( \pi(P) ) 是一个关于P的二次函数线性需求或其它形式可以通过求导等于零来解析求解最优价格。 例如对于线性需求 ( Q a - bP ) (a, b0)利润 ( \pi (P-C)(a-bP) )。令导数 ( d\pi/dP a - 2bP bC 0 )可得最优价格 ( P^* \frac{a}{2b} \frac{C}{2} )。这就是著名的“成本加成定价”在考虑需求弹性后的形式。2.2.2 多产品多日联合定价现实情况更复杂不同蔬菜之间可能存在替代西红柿贵了大家多买黄瓜或互补关系且今天的定价和库存会影响明天的决策。这需要建立动态规划或随机优化模型但考虑到比赛时间通常进行简化按品类分组将替代性强的蔬菜分为一组在组内联合优化。序列决策先独立优化每一天的价格然后根据当天的模拟销售结果如库存结余微调第二天的定价和补货策略。这更像一个模拟-优化的框架。2.3 补货决策模型平衡短缺与过剩定了价预测了销量接下来要知道进多少货。这本质上是一个报童模型问题在随机需求下决策订货量以平衡缺货损失机会成本和库存过剩损失残值损失或损耗成本。2.3.1 经典报童模型假设第t天对单品i的需求 ( D_{it} ) 是一个随机变量服从某个概率分布如正态分布、泊松分布。已知( c )单位进货成本批发价( p )单位售价由定价模型决定( s )单位残值当天未售出剩余库存的折价通常 ( s c )。对于生鲜蔬菜s可能很低甚至为0完全损耗。( g )单位缺货损失不仅损失利润还可能影响顾客满意度可以设为一个惩罚成本。则最优订货量 ( Q^* ) 应满足( F(Q^*) \frac{p - c g}{p - s g} )其中 ( F(\cdot) ) 是需求 ( D ) 的累积分布函数。这个公式的含义是最优库存水平对应的累积概率等于“边际利润”与“边际损失边际利润”之比。2.3.2 结合本题的调整需求分布我们的需求预测模型给出的 ( \hat{Q}{it} ) 可以看作是需求的均值。我们还需要估计需求的不确定性标准差 ( \sigma{it} ) 。可以用历史预测误差的统计量或时间序列模型如ARIMA的预测区间来估计。损耗成本对于蔬菜未售出的残值s可能为负需要处理垃圾的成本。可以将损耗成本直接纳入模型过剩的单位损失 ( c - s )。库存容量约束这是本题的一个重要约束。所有蔬菜的每日总库存量前一天结余当日补货不能超过仓库总容量。这使问题从多个独立的报童问题变成了一个带资源约束的联合订货问题求解难度大增。常用方法先忽略总容量约束用报童公式计算每个单品的最优订货量。如果总库存超限则按照一定的优先级规则如单位体积利润贡献率、损耗率高低削减某些单品的订货量。3. 方案实现与编程要点理论模型需要代码来实现。这里以Python为例阐述关键步骤。3.1 环境与数据准备import pandas as pd import numpy as np from sklearn.linear_model import LinearRegression, Ridge from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor import statsmodels.api as sm from scipy import optimize import matplotlib.pyplot as plt # 1. 读取数据 sales_df pd.read_excel(附件1 销售流水.xlsx) loss_df pd.read_excel(附件2 损耗记录.xlsx) cost_df pd.read_excel(附件3 进货成本.xlsx) # 2. 数据合并与清洗 # 假设数据以日期和商品编码为键 df pd.merge(sales_df, cost_df, on[日期,商品编码], howleft) df pd.merge(df, loss_df, on[日期,商品编码], howleft) df[损耗率] df[损耗数量] / (df[销售数量] df[损耗数量]) df.fillna(0, inplaceTrue) # 谨慎处理缺失值 # 3. 特征工程 df[日期] pd.to_datetime(df[日期]) df[星期几] df[日期].dt.dayofweek df[是否周末] df[星期几].apply(lambda x: 1 if x5 else 0) df[月份] df[日期].dt.month df[年份] df[日期].dt.year # 创建滞后特征 df.sort_values([商品编码,日期], inplaceTrue) for lag in [1, 7]: df[f销量_滞后{lag}天] df.groupby(商品编码)[销售数量].shift(lag)3.2 需求预测模型实现示例我们为每个单品建立一个预测模型。这里展示两种思路思路一基于价格弹性的回归模型def train_demand_model_for_item(item_df): 为单个商品训练需求模型 # 准备特征和目标变量 # 特征自身价格、星期几、月份、滞后销量等 features [销售单价, 星期几, 月份, 销量_滞后1天, 销量_滞后7天] # 可以加入价格的对数或价格的平方项来捕捉非线性 item_df[log_价格] np.log(item_df[销售单价]) features.append(log_价格) X item_df[features].dropna() y item_df.loc[X.index, 销售数量] # 添加截距项 X sm.add_constant(X) # 使用statsmodels进行回归便于查看统计显著性 model sm.OLS(y, X).fit() # print(model.summary()) # 可以查看价格系数的显著性应为负 return model # 对每个商品循环训练 item_models {} for item_id, group in df.groupby(商品编码): try: model train_demand_model_for_item(group.copy()) item_models[item_id] model except Exception as e: print(f商品 {item_id} 建模失败: {e})思路二使用树模型进行预测from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error def train_ml_model_for_item(item_df): features [销售单价, 星期几, 月份, 销量_滞后1天, 销量_滞后7天, 年份] X item_df[features].dropna() y item_df.loc[X.index, 销售数量] if len(X) 20: # 数据量太少退回简单模型 return None X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, shuffleFalse) model GradientBoostingRegressor(n_estimators100, random_state42) model.fit(X_train, y_train) pred_val model.predict(X_val) mse mean_squared_error(y_val, pred_val) # print(fItem {item_id} Val MSE: {mse:.2f}) return model ml_models {} for item_id, group in df.groupby(商品编码): model train_ml_model_for_item(group.copy()) if model: ml_models[item_id] model3.3 定价与补货联合优化框架这是一个简化的模拟优化循环用于未来三天的决策def simulate_one_day(current_date, inventory, models, cost_today, total_capacity): 模拟某一天的决策与销售 current_date: 当前日期datetime对象 inventory: 字典商品编码 - 当前库存量 models: 训练好的需求预测模型字典 cost_today: 字典商品编码 - 当日批发价 total_capacity: 总库存容量 decisions {} # 存储决策结果 daily_profit 0 daily_sales_volume 0 # 步骤1: 为每个商品预测在不同价格下的需求需要先确定价格搜索范围 # 这里简化我们用一个优化函数为每个商品找最优价 for item_id, model in models.items(): current_stock inventory.get(item_id, 0) cost cost_today.get(item_id, cost_today.get(item_id, cost_today.default)) # 假设能获取当日成本 # 定义利润函数用于优化 def profit_func(price): # 使用模型预测在该价格下的销量 # 需要构建特征向量 features pd.DataFrame({ 销售单价: [price], 星期几: [current_date.dayofweek], 月份: [current_date.month], 销量_滞后1天: [last_sales.get(item_id, 0)], # 需要前一天销量 销量_滞后7天: [sales_7d_ago.get(item_id, 0)], # 需要7天前销量 const: [1] }) # 预测销量 predicted_demand model.predict(features)[0] # 销量不能超过库存补货量这里先假设补货量足够后续调整 actual_sales min(predicted_demand, current_stock replenish_qty) # 计算利润 profit (price - cost) * actual_sales # 可以加入对价格过高导致销量过低的惩罚 if predicted_demand some_threshold: profit - penalty return -profit # 因为我们要最小化负利润 # 约束价格在合理范围内 price_bounds (cost * 1.1, cost * 3) # 例如最低加价10%最高3倍 # 使用优化算法寻找最优价格 from scipy.optimize import minimize_scalar res minimize_scalar(profit_func, boundsprice_bounds, methodbounded) optimal_price res.x predicted_demand_at_opt ... # 根据最优价重新预测 decisions[item_id] {optimal_price: optimal_price, predicted_demand: predicted_demand_at_opt} # 步骤2: 基于预测需求和最优价格计算初始补货量报童模型 initial_order {} total_planned_inventory 0 for item_id, dec in decisions.items(): pred_demand dec[predicted_demand] demand_std estimate_demand_std(item_id, current_date) # 需要实现一个函数估计需求标准差 # 简化报童公式计算假设需求服从正态分布 from scipy.stats import norm unit_profit dec[optimal_price] - cost_today[item_id] unit_loss cost_today[item_id] * loss_rate[item_id] # 假设损耗成本 critical_ratio unit_profit / (unit_profit unit_loss) # 防止除零 critical_ratio np.clip(critical_ratio, 0.001, 0.999) z_score norm.ppf(critical_ratio) # 查找分位数 optimal_order_qty pred_demand z_score * demand_std optimal_order_qty max(0, optimal_order_qty) initial_order[item_id] optimal_order_qty total_planned_inventory inventory.get(item_id, 0) optimal_order_qty # 步骤3: 处理总库存容量约束 if total_planned_inventory total_capacity: # 需要削减订单 overage total_planned_inventory - total_capacity # 按优先级排序削减例如按单位利润/单位体积排序低的先削减 item_priority [] for item_id in initial_order: unit_profit decisions[item_id][optimal_price] - cost_today[item_id] volume_per_unit get_volume(item_id) # 需要每个商品的单位体积数据 priority_score unit_profit / volume_per_unit item_priority.append((item_id, priority_score, initial_order[item_id])) # 按优先级升序排序分数低的先被削减 item_priority.sort(keylambda x: x[1]) for item_id, score, order_qty in item_priority: if overage 0: break # 最多削减到订单量为0 cut min(order_qty, overage) initial_order[item_id] - cut overage - cut # 步骤4: 模拟当日销售考虑随机性 actual_sales {} for item_id, dec in decisions.items(): order_qty initial_order[item_id] available_stock inventory.get(item_id, 0) order_qty # 模拟实际需求在预测值附近随机波动 actual_demand np.random.normal(locdec[predicted_demand], scaledemand_std) actual_demand max(0, actual_demand) # 需求非负 # 实际销量不能超过可用库存 sold_qty min(actual_demand, available_stock) actual_sales[item_id] sold_qty # 计算损耗 leftover available_stock - sold_qty # 损耗成本计算... # 更新利润 daily_profit (dec[optimal_price] - cost_today[item_id]) * sold_qty - cost_today[item_id] * leftover * loss_rate[item_id] daily_sales_volume sold_qty # 更新库存剩余部分进入第二天考虑损耗 inventory[item_id] leftover * (1 - loss_rate[item_id]) # 记录当天的销量用于下一天的滞后特征 global last_sales, sales_7d_ago last_sales actual_sales # 更新7天前的销售记录需要维护一个历史字典 return decisions, initial_order, daily_profit, inventory实操心得在实际编程中上述框架需要大量细节填充。例如estimate_demand_std函数如何实现get_volume数据从哪里来比赛数据通常不直接给出需要自己根据经验假设或从其他数据推断如根据商品名称和常识估计。关键是要在论文中明确说明你的假设并分析其合理性。4. 模型检验、灵敏度分析与论文写作模型建好了代码跑通了但工作只完成了一半。如何让你的论文脱颖而出关键在于严谨的检验和深入的分析。4.1 模型检验与评估4.1.1 历史数据回测用历史数据中的一段作为“训练期”紧挨着的一段作为“测试期”。在测试期假装不知道未来的真实销量用你的模型基于训练期数据训练逐日做出定价和补货决策然后与真实历史数据对比。评估指标利润模型模拟的总利润 vs 历史实际总利润需根据你的定价策略重新计算历史利润。预测准确性销量预测的均方根误差RMSE、平均绝对百分比误差MAPE。服务水平缺货率需求大于库存的天数比例、库存周转率。约束满足是否每天都满足库存容量约束4.1.2 对比基准策略设立几个简单的基准策略对比显示你模型的优越性。基准策略1成本加成每日价格 成本 × (1 固定利润率)补货量 前7天平均销量。基准策略2跟随策略价格和补货量与去年同期完全相同。基准策略3简单预测用移动平均预测销量价格固定。 将你的复杂模型与这些简单策略对比从利润、风险等角度说明其提升。4.2 灵敏度分析与策略讨论这是体现思考深度的部分。分析模型对关键参数和假设的敏感程度。价格弹性系数如果实际价格弹性比你估计的更敏感或更不敏感利润会如何变化这说明了什么说明需求预测的准确性至关重要损耗率损耗率上升10%对总利润和最优补货策略的影响有多大这能引出关于冷链管理、库存控制重要性的管理建议。库存容量如果仓库容量减小20%你的模型如何调整策略利润会下降多少这能为门店扩张或仓储管理提供量化依据。需求波动性如果未来需求的不确定性标准差增大报童模型中的最优订货量会如何变化通常会增加安全库存对你的策略有什么启示4.3 论文写作与图表呈现数学建模竞赛“建模”和“求解”各占一半另一半是“表达”。摘要用300-500字精炼地概括问题、你的整体思路、所用模型、求解方法、主要结论和策略建议。这是评委第一眼看到的内容务必字斟句酌。问题重述与分析不要照抄题目要用自己的语言梳理问题的背景、目标、约束和难点展现你的理解。模型假设清晰列出所有重要假设如“假设不同蔬菜间的需求相互独立”、“假设损耗率在短期内稳定”并说明其合理性。符号说明用三线表列出所有主要变量、符号及其含义。模型建立这是核心。按照“总-分”结构先给出整体框架图再分小节详细介绍每个子模型。公式要规范推导要清晰。模型求解说明用了什么算法如梯度下降、遗传算法、什么工具Python的哪个库、关键参数如何设置。结果分析用图表说话利润对比用柱状图价格销量趋势用折线图库存变化用面积图。图表要美观、信息量大并有详细的文字分析。模型评价与推广客观评价模型的优点如贴近实际、考虑全面和缺点如未考虑突发天气、假设较强并提出改进方向。将模型推广到其他商品如水果、肉类或场景如线上生鲜零售。最后再分享一个比赛中的小技巧时间管理。三天时间第一天上午一定要确定整体框架和分工下午完成数据预处理和基础分析。第二天全天攻坚模型核心和编程实现。第三天上午完成灵敏度分析和结果整理下午和晚上集中写作论文和制作图表。编程和写作最好同步进行写论文的同学要尽早介入理解模型逻辑。最后一个通宵往往是常态但清晰的规划和团队协作能让你在最后时刻从容不迫。记住一个逻辑自洽、表述清晰、有亮点的中等模型远胜过一个漏洞百出、表达混乱的复杂模型。祝你在数模的战场上运筹帷幄决胜千里。
