数学建模实战:基于销量预测与报童模型的商品定价与补货决策

📅 发布时间:2026/8/27 1:28:51
数学建模实战:基于销量预测与报童模型的商品定价与补货决策 1. 项目概述从赛题到实战的完整复盘去年国赛C题“蔬菜类商品的自动定价与补货决策”可以说是一道非常典型的、连接商业现实与数理模型的赛题。它没有停留在纯理论推导而是要求我们基于一个超市的实际销售数据去构建一套能够自动运行的决策系统。这恰恰是数学建模竞赛近年来一个很明显的趋势越来越强调解决实际问题的能力而不仅仅是漂亮的公式和复杂的算法。题目给出了过去四年内多个蔬菜品类的日销售数据包括销量、成本、售价、损耗率等核心字段要求我们完成两件事第一建立数学模型实现对未来一周内各蔬菜品类的自动定价第二基于预测的销量和定价制定未来一周的每日补货计划。最终目标很明确——在满足市场需求的前提下最大化商超的整体收益或者更具体地说是最大化“商超收益”这个收益需要综合考虑销售收入、货物成本、以及因未售出而产生的损耗成本。这道题的魅力在于它的开放性和层次感。新手团队可以基于相对简单的统计方法如时间序列预测、回归分析搭建一个基础框架拿到保底分数而追求高水平的团队则可以在预测精度、定价策略的博弈性、以及补货模型的鲁棒性上深入挖掘构建一个融合了机器学习、优化理论甚至一点库存论的综合模型。我去年带队时我们选择了一条兼顾实用性与创新性的路径核心思路是“分而治之协同优化”先利用改进的时序模型如XGBoost融合SARIMA高精度预测销量再构建一个以商超收益最大化为目标的非线性规划模型进行定价最后将预测销量和定价结果输入到一个考虑安全库存和订货成本的动态补货模型中。整个过程我们用Python实现代码模块清晰数据流闭环。今天我就把这套完整的解题思路、模型细节、代码实现以及我们踩过的那些“坑”毫无保留地分享出来希望能为备战未来数模竞赛的同学提供一个扎实的参考框架。2. 核心问题拆解与建模总览面对这样一个综合性的问题直接上手编码很容易陷入混乱。我们的首要任务是将宏大的问题分解成几个逻辑连贯、可独立建模又相互关联的子问题。2.1 问题一销量预测——一切决策的基础定价和补货都严重依赖于对未来需求的判断。因此一个准确、稳健的销量预测模型是整个项目的基石。题目给出的数据是典型的时间序列数据具有明显的季节性年度、月度、周度、趋势性以及可能的节假日效应。2.1.1 预测模型选型与融合策略单纯使用ARIMA或Prophet可能不足以捕捉蔬菜销售中复杂的非线性关系例如促销、天气的突然影响。我们采用了“线性模型树模型”的融合策略SARIMA模型擅长捕捉序列的自相关、季节性和趋势。我们用它来拟合销量序列中稳定的周期成分。XGBoost/LightGBM模型将日期特征年、月、日、星期几、是否为节假日、历史销量滞后项、以及成本价和售价作为特征。这里的关键洞察是价格本身是影响需求的重要因素但在预测阶段我们尚不知道未来价格因此需要使用历史价格或预期价格作为特征。我们采用了滚动预测的方式用上一期的预测价格作为下一期特征输入的近似。2.1.2 预测实操要点与数据预处理注意数据中通常存在缺失值、异常值如促销导致的销量尖峰和记录错误。直接喂给模型效果会很差。我们的预处理流水线包括缺失值处理对于少量缺失采用前后日期均值填充对于连续缺失考虑用同类商品的销售模式进行插补。异常值检测与平滑使用箱线图或3σ原则识别极端值。对于促销造成的合理“峰值”我们予以保留但将其标记为“促销日”特征对于明显错误的记录如销量为0但成本价正常则用移动平均替代。特征工程除了基础的日期特征我们还构造了滑动统计特征过去3天、7天、14天的平均销量、销量标准差。同比/环比特征与去年同期、上周同期的销量比值。价格弹性相关特征历史销量与历史售价的比值、变化率等。# 示例特征工程代码片段 import pandas as pd import numpy as np def create_features(df, lags[1,2,3,7,14]): 为时间序列预测创建滞后特征和滚动统计特征 df: 包含‘sale_quantity’销量的DataFrame df df.copy() # 日期特征 df[year] df[date].dt.year df[month] df[date].dt.month df[day] df[date].dt.day df[dayofweek] df[date].dt.dayofweek df[is_weekend] df[dayofweek].isin([5,6]).astype(int) # 滞后特征 for lag in lags: df[flag_{lag}] df[sale_quantity].shift(lag) # 滚动统计特征 df[rolling_mean_7] df[sale_quantity].shift(1).rolling(window7).mean() df[rolling_std_7] df[sale_quantity].shift(1).rolling(window7).std() # 价格相关特征假设有‘sale_price’列 if sale_price in df.columns: df[price_to_cost_ratio] df[sale_price] / df[cost_price] df[price_change] df[sale_price].pct_change() return df2.2 问题二定价模型——在收益与风险间寻找平衡定价不是孤立的它直接影响销量通过需求函数进而影响收益和损耗。这是一个典型的优化问题。2.2.1 目标函数构建商超收益 销售收入 - 采购成本 - 损耗成本。销售收入 预测销量 * 销售单价采购成本 预测销量 * 单品成本价题目给出损耗成本 (补货量 - 预测销量) * 单品成本价 * 损耗率当补货量大于销量时发生因此目标函数可以形式化为Maximize Profit ∑[ (p_i - c_i) * D_i(p_i) - c_i * θ * max(0, Q_i - D_i(p_i)) ]其中p_i是商品i的售价c_i是其成本价D_i(p_i)是价格p_i对应的预测需求量即问题一的输出Q_i是补货量问题三的决策变量θ是损耗率。2.2.2 需求函数价格-销量关系的估计这是定价模型的核心与难点。我们如何知道价格变动对销量的影响我们采用了两种方法结合历史数据回归针对每个单品用过去一段时间的历史数据拟合销量与价格的关系。常用模型有线性模型D a - b*p或对数线性模型log(D) a - b*log(p)后者更符合经济学中的恒定价格弹性假设。引入价格弹性先验对于数据量少或关系不明显的单品我们根据商品特性必需品、奢侈品设定一个经验性的价格弹性范围辅助模型求解。2.2.3 约束条件与求解定价并非可以天马行空需要受到约束价格上下限约束售价通常不能低于成本价否则亏本也不能高于某个上限如监管要求或市场接受度。我们设定c_i p_i k * c_i其中k根据商品类型设定如叶菜类1.5-2.0根茎类1.8-2.5。市场稳定性约束相邻日期的价格波动不宜过大例如|p_i(t) - p_i(t-1)| / p_i(t-1) 0.1。求解这个带约束的非线性规划问题我们使用了SciPy.optimize库中的minimize函数因为目标是最大化所以需要最小化负收益。# 示例定价优化模型核心代码片段 from scipy.optimize import minimize def profit_function(prices, cost, predicted_demand, replenish_qty, spoilage_rate): 计算给定价格下的总收益负值用于最小化 prices: 各商品的价格数组 revenue predicted_demand * prices cost_of_goods predicted_demand * cost spoilage_cost cost * spoilage_rate * np.maximum(0, replenish_qty - predicted_demand) total_profit np.sum(revenue - cost_of_goods - spoilage_cost) return -total_profit # 返回负值因为minimize是求最小值 # 定义约束条件价格在成本价的1倍到2.5倍之间 bounds [(cost[i], 2.5*cost[i]) for i in range(len(cost))] # 定义初始价格例如成本价加一定利润率 initial_prices cost * 1.5 # 调用优化器 result minimize(profit_function, initial_prices, args(cost, predicted_demand, replenish_qty, spoilage_rate), boundsbounds, methodL-BFGS-B) optimal_prices result.x2.3 问题三补货决策——连接预测与定价的最后一环补货决策依赖于预测销量和定价结果目标是确定每天向每个单品订多少货。2.3.1 补货模型的核心要素目标在满足需求的前提下最小化总成本采购成本持有成本缺货/损耗成本。在本题简化背景下核心是平衡采购过多导致的损耗和采购过少导致的销售机会损失。决策变量未来一周每一天、每一个单品的补货量Q_i(t)。输入来自问题一的未来每日销量预测D_i(t)。来自问题二的最优价格p_i(t)价格会影响最终的实际需求这里我们假设预测需求已隐含价格影响。单品成本价c_i、损耗率θ_i。约束库存平衡约束当日末库存 上日末库存 当日补货量 - 当日预测销量。仓储能力约束总库存量不能超过仓库容量若题目给出。补货量非负约束。2.3.2 模型建立报童模型的扩展这是一个多周期、多产品的库存问题。一个经典的建模思路是将其转化为一系列动态规划问题但计算复杂。我们采用了更实用的方法——改进的报童模型Newsvendor Model。对于每一天每个单品我们将其视为一个独立的报童问题在知道预测需求分布我们由预测模型得到的是一个点估计值D和其预测误差的标准差σ的情况下决定最优补货量Q*使得期望成本最小。 期望成本 过量成本损耗 * P(需求 Q) 缺货成本机会损失 * P(需求 Q) 其中过量成本 c * θ缺货成本 p - c即单件销售的毛利润。假设需求服从以D为均值、σ为标准差的正态分布则最优补货量Q*可以通过计算关键比率Critical Ratio得到CR (缺货成本) / (缺货成本 过量成本) (p-c) / ((p-c) c*θ)然后Q* D z * σ其中z是标准正态分布下累积概率为CR的分位数可用scipy.stats.norm.ppf(CR)求得。这个模型的优势在于计算高效且具有明确的经济学解释毛利率越高、损耗率越低关键比率越大就应该准备更多的库存以应对不确定性。# 示例基于报童模型的补货决策代码 from scipy.stats import norm def calculate_optimal_order(demand_mean, demand_std, cost, price, spoilage_rate): 计算最优补货量 demand_mean: 预测销量均值 demand_std: 预测销量标准差可通过预测模型的误差估计 shortage_cost price - cost # 缺货成本单位利润损失 overage_cost cost * spoilage_rate # 过量成本单位损耗成本 critical_ratio shortage_cost / (shortage_cost overage_cost) # 防止比率超出(0,1)范围 critical_ratio np.clip(critical_ratio, 0.01, 0.99) z_score norm.ppf(critical_ratio) # 查找分位数 optimal_order demand_mean z_score * demand_std # 补货量不能为负且可以加上一个最小起订量约束 optimal_order max(0, optimal_order) return np.round(optimal_order, 2) # 保留两位小数符合实际 # 对每个单品、每一天进行计算 for product in all_products: for day in forecast_days: mean_demand forecast_df.loc[day, product][predicted_sale] std_demand forecast_df.loc[day, product][prediction_std] # 需要从预测模型中获得误差估计 opt_order calculate_optimal_order(mean_demand, std_demand, cost[product], optimal_price[product, day], spoilage_rate[product]) replenish_plan[product, day] opt_order3. 代码架构与核心模块实现一个清晰、模块化的代码架构对于团队协作和模型调试至关重要。我们的项目主要分为以下五个核心模块3.1 数据加载与预处理模块 (data_preprocessor.py)这个模块负责读取原始数据通常是Excel或CSV格式并进行清洗、转换和初步的特征构造。核心功能读取与合并读取多个sheet或文件按时间、商品ID进行对齐合并。异常值处理我们实现了一个基于移动标准差的方法将连续3天以上超出3倍标准差的数据视为异常并用前后窗口的中位数替换。缺失值填补对于成本、售价等关键字段的缺失采用同一商品相邻日期的值进行填充。数据平滑对于销量数据使用滑动平均进行轻度平滑以消除偶然波动突出趋势和季节。输出生成干净的、可供后续模型直接使用的DataFrame。实操心得预处理阶段花的时间通常占整个项目的30%以上但这是值得的。务必保存预处理后的中间数据文件避免每次运行都从头开始节省大量时间。3.2 销量预测模块 (demand_forecaster.py)该模块封装了我们的融合预测模型。核心类/函数HybridForecaster类初始化SARIMA和XGBoost模型。fit方法使用历史数据训练模型。这里我们采用了时序交叉验证来调参避免过拟合。predict方法输入未来日期的特征需要先构造好输出销量点预测和区间预测用于估计标准差σ。evaluate方法在历史数据上计算RMSE、MAE等指标评估模型性能。一个关键技巧对于XGBoost模型我们不是一次性预测未来7天而是采用滚动预测Rolling Forecast。即用截至t日的数据预测t1日然后将t1日的预测值作为已知特征的一部分再加入t1日的其他特征去预测t2日以此类推。这样更符合实际应用场景。# 示例滚动预测的核心循环 def rolling_forecast(model, initial_features, forecast_horizon7): model: 训练好的预测模型如XGBoost initial_features: 包含历史特征和未来日期基础特征如星期几的DataFrame forecast_horizon: 预测步长 predictions [] current_features initial_features.copy() for step in range(forecast_horizon): # 预测下一步 pred model.predict(current_features.iloc[-1:])[0] predictions.append(pred) # 为下一步准备特征将预测值作为滞后特征加入 if step forecast_horizon - 1: # 创建新的一行特征日期递增 new_row current_features.iloc[-1:].copy() new_row[date] pd.Timedelta(days1) # 更新滞后特征例如将最新的预测值赋给lag_1 new_row[lag_1] pred # 更新其他滚动特征需要重新计算这里简化 # ... current_features pd.concat([current_features, new_row], ignore_indexTrue) return np.array(predictions)3.3 定价优化模块 (pricing_optimizer.py)该模块实现第2.2节所述的定价模型。核心类/函数DemandFunctionEstimator类根据历史数据拟合每个单品的需求-价格曲线。PriceOptimizer类__init__方法接收成本、预测需求、损耗率、价格弹性范围等参数。set_constraints方法设置价格上下限、波动率约束。solve方法调用scipy.optimize.minimize求解最优价格并返回结果和优化状态。难点处理对于需求函数拟合效果差的商品我们采用了一个保底策略价格设定为成本价加上一个基于品类平均毛利率的固定加成。确保模型在任何情况下都能输出一个合理的价格。3.4 补货计划模块 (replenishment_planner.py)该模块实现第2.3节所述的报童模型。核心类/函数NewsvendorPlanner类calculate_critical_ratio方法根据价格、成本、损耗率计算关键比率。compute_safety_stock方法根据需求标准差和服务水平由关键比率衍生计算安全库存。generate_plan方法循环所有商品和日期调用calculate_optimal_order函数生成最终的补货计划表。与预测模块的衔接补货模块强烈依赖于预测模块输出的不确定性度量即预测误差的标准差σ。我们通过计算预测模型在验证集上的残差标准差来估计这个值。对于不同的商品这个值是不同的耐储存的商品如土豆预测误差可能较小而不耐储存的叶菜误差可能较大。3.5 主流程与可视化模块 (main_pipeline.py)这是整个项目的调度中心负责按顺序调用各个模块并整合结果。工作流程加载配置参数文件路径、预测天数、模型超参数等。运行data_preprocessor得到干净数据。运行demand_forecaster得到未来7天的销量预测及置信区间。运行pricing_optimizer输入预测销量得到最优价格。运行replenishment_planner输入预测销量、最优价格和预测不确定性得到补货计划。结果汇总与输出将价格表和补货计划表保存为Excel或CSV文件。可视化生成关键图表如各单品历史销量与预测销量的对比趋势图。未来一周定价策略的柱状图。补货量、预测销量与安全库存的关系面积图。可视化不仅用于论文美化更是验证模型合理性的重要手段。例如如果发现某个商品的补货量长期远高于预测销量就需要检查其损耗率设置是否过低或者需求预测是否系统性偏低。4. 模型检验、灵敏度分析与常见问题排查模型建好、代码跑通只是第一步。在数模论文中模型的检验、分析和讨论是拿高分的关键。4.1 模型检验策略历史回测Backtesting将模型应用到过去一段历史数据上。例如用1-3月的数据训练预测4月的销量和定价然后与4月的实际数据对比。计算核心指标预测准确度销量预测的MAPE平均绝对百分比误差、RMSE。收益模拟使用模型生成的定价和补货策略结合实际销量计算模拟收益。与使用简单策略如固定加价率、简单移动平均补货的收益进行对比。这是证明模型有效性的最有力证据。稳定性检验改变训练数据的时间窗口长度例如用过去6个月、12个月、24个月的数据分别训练观察模型预测结果和最优决策是否发生剧烈变化。一个稳健的模型应该对训练数据长度不敏感。4.2 灵敏度分析Sensitivity Analysis这是体现思考深度的环节。我们需要探讨当模型的关键参数或输入假设发生变化时输出结果如总收益、定价、补货量如何变化。4.2.1 对需求预测误差的灵敏度方法人为地将预测销量调高或调低10%、20%重新运行定价和补货模型。分析观察总收益的变化。通常模型对过度预测补货过多导致损耗比预测不足缺货更敏感因为损耗是实实在在的成本而缺货只是机会损失。这可以引出对提高预测精度的价值讨论。4.2.2 对损耗率的灵敏度方法将题目给定的损耗率上下浮动一定比例如±5% ±10%。分析损耗率上升会直接导致报童模型中的过量成本增加从而降低关键比率使得最优补货量Q*减少。在定价端损耗率上升也会促使模型倾向于制定更高的价格来覆盖风险。通过分析可以量化损耗控制对提升收益的重要性。4.2.3 对价格弹性假设的灵敏度方法在拟合需求函数时尝试不同的函数形式线性、指数、对数或者直接改变价格弹性系数。分析价格弹性的大小直接影响定价的激进程度。弹性小必需品提价对销量影响小模型会倾向于定更高的价弹性大可替代品提价会导致销量锐减模型会定更保守的价格。分析不同弹性下的收益变化可以评估市场调研获取准确弹性的价值。4.3 常见问题与排查实录在开发和调试过程中我们遇到了不少典型问题这里分享出来供大家避坑问题1预测模型在验证集上表现很好但滚动预测未来多步时误差急剧放大。原因这通常是误差累积Error Accumulation导致的。在滚动预测中上一步的预测误差会作为输入特征影响下一步的预测导致误差像滚雪球一样越来越大。解决方案使用更稳健的特征减少对滞后一期预测值的依赖增加更多外部特征或周期性特征。采用多输出模型使用可以一次性预测未来多步的模型如DeepAR、Seq2Seq而不是一步步滚动。对于本题我们采用了“分步预测误差校正”策略即每预测3步就用最新的“已知”数据假设我们每天都能获得前一天的销售数据重新训练或调整模型。问题2定价优化模型求解失败或不收敛。原因目标函数或约束可能非凸、不光滑或者初始值设置得太差。排查与解决检查目标函数打印出不同价格下的收益值确保其形状大致合理。对于非线性部分如需求函数确保其导数存在。尝试不同求解器和初始值SciPy的minimize提供了多种方法如‘SLSQP‘, ’trust-constr‘。多试几种。同时尝试几组不同的初始价格如成本价*1.2 *1.5 *1.8。简化问题先去掉波动率约束等复杂约束看能否求解。如果能再逐步加入约束定位问题所在。采用启发式方法作为保底如果优化始终失败可以退而求其次采用成本加成法或基于历史毛利率的定价法。问题3补货量出现剧烈波动今天补货1000明天补货50不符合商业常识。原因预测销量或预测误差标准差本身波动剧烈或者关键比率计算中由于价格或成本的小幅变动导致z_score发生跳变。解决方案平滑输入对预测销量和预测误差进行简单的移动平均平滑再输入补货模型。对补货量施加平滑约束在报童公式计算出的Q*基础上增加一个约束使其与前一日的补货量或库存量的差值不超过一定比例。这可以通过在优化框架内引入额外约束实现或者简单地对结果进行后处理平滑。设置最小起订量MOQ在实际商业中供应商常有最小起订量要求。在模型中我们可以设定Q* max(MOQ, 计算值)。问题4模型总收益在回测中甚至低于简单的基准策略。原因这是最令人沮丧的情况。可能的原因有1模型过度复杂导致过拟合2某个核心假设严重偏离实际如假设需求函数是线性的但实际是非线性的3未考虑重要的现实因素如不同蔬菜之间的替代/互补效应、促销活动的巨大影响。诊断步骤分解收益将总收益拆分为销售收入、成本、损耗成本三部分与基准策略逐项对比。看看是收入低了还是损耗成本高了单品级分析找出是哪些单品拖累了整体收益。针对这些单品单独检查其预测、定价、补货各环节的输出是否合理。检查“黑天鹅”事件回测期是否包含了未在模型中考量的特殊事件如极端天气、突然的公共卫生事件这些事件会导致模型失效。处理办法是在特征中加入“异常事件”标识或者在建模时剔除这些特殊时期的数据。最后我想强调的是数学建模竞赛没有“唯一正确”的答案。评委更看重的是解决问题的逻辑过程、模型的合理性与创新性、以及分析的深度。我们这套“预测-定价-补货”的框架提供了一个坚实、可解释、可扩展的基础。你可以在此基础上尝试引入更高级的模型如LSTM进行预测、用强化学习进行动态定价、考虑多品类联合补货的优化或者对灵敏度分析进行更深入的探讨。希望这份超过5000字的详细复盘能帮助你深入理解这道赛题并在未来的比赛中构建出更出色的解决方案。完整的代码已整理更新注重模块化和注释你可以根据实际数据结构和需求进行调整。记住好的代码和清晰的逻辑同样是获奖的关键。