
1. 项目概述当数学建模遇上菜市场看到“蔬菜类商品的自动定价与补货决策”这个题目很多参加过数学建模比赛的同学可能会心一笑这不就是典型的国赛C题风格吗没错这正是2023年大学生数学建模竞赛C题的原题。我之所以想以“自娱自乐版”的形式重新拆解它是因为我发现很多同学在初次面对这类问题时容易陷入两个极端要么被庞大的数据和复杂的背景吓住不知从何下手要么一头扎进模型构建的细节里忽略了问题本身的商业逻辑和可解释性。这道题的精妙之处在于它把一个看似高大上的“智能决策”问题锚定在了我们每个人日常生活中最熟悉的场景——买菜上。这不仅仅是几个数学公式的堆砌更是对供应链管理、市场经济学和数据分析能力的一次综合考验。简单来说题目要求我们扮演一个“智能店长”的角色。给你一家生鲜超市连续30天、涉及6个蔬菜品类的详细销售数据包括每天的销售量、销售单价、成本单价和损耗量。你的核心任务就两个第一建立模型实现未来一周第31-37天每个单品每日的自动定价第二根据预测的销量和定价制定未来一周的每日补货计划。最终目标很明确在满足市场需求的前提下实现超市利润的最大化同时还要兼顾定价的合理性与稳定性不能今天白菜卖一块明天卖十块把顾客吓跑。这背后涉及的需求预测、价格弹性分析、库存优化和动态规划正是现代零售业智能化的核心。无论你是数学建模的新手想找切入点还是相关专业的学生希望将理论应用于实践这个“自娱自乐”的深度拆解都能带你绕过弯路直击要害。2. 解题核心思路与模型框架设计面对这样一个多目标、多约束的决策问题最忌讳的就是一上来就埋头写代码、调参数。我们先得把问题的“骨架”搭好理解各个部分是如何咬合在一起的。整个决策流程可以看作一个经典的“预测-优化”闭环。2.1 问题分解与流程设计首先我们必须认识到定价和补货不是两个孤立的问题而是一个紧密耦合的决策系统。定价会影响销量销量决定了需要补多少货而补货量又关联着库存成本和损耗风险。一个合理的解题流程应该遵循以下逻辑链条数据理解与预处理这是所有模型的地基。我们需要深入分析给出的销售数据识别销售趋势工作日/周末效应、周期性、价格与销量的关系需求弹性、损耗模式与销量或库存的关系。需求预测模型这是最关键的一步也是后续所有决策的基础。我们需要预测未来一周第31-37天每个蔬菜单品在不同可能价格下的日需求量。注意不是预测一个固定的销量而是预测一个关于价格的函数关系即需求曲线。定价决策模型在预测的需求曲线基础上结合成本、库存和利润目标建立定价模型求解出能使每日利润最大化或综合目标最优的价格。补货决策模型根据定价模型确定的最优价格和对应的预测销量再考虑现有库存、在途库存、安全库存和损耗率计算出每日的最优补货量目标是平衡缺货损失和库存持有含损耗成本。模型集成与验证将以上模块整合形成一个完整的决策系统并利用历史数据如前20天训练后10天验证进行回测评估模型效果。这个流程的核心思想是“分而治之”但模块之间通过数据和目标函数相互联系。例如定价模型的目标函数利润依赖于需求预测的结果而补货模型又依赖于定价模型输出的价格和预测销量。2.2 模型选型背后的“为什么”明确了流程接下来要为每个环节选择合适的“工具”模型。这里的选择没有绝对的对错但有优劣之分关键看是否贴合数据特性和问题要求。对于需求预测由于数据量较小仅30天且蔬菜销售具有明显的时序特征趋势、周期和外部影响价格我推荐采用“集成”思路而非单一复杂模型。为什么不是单纯的ARIMA或LSTM纯时间序列模型如ARIMA难以直接引入价格这个关键解释变量。而LSTM等深度学习模型对于30条这样的“小样本”数据极易过拟合训练不稳定可解释性也差。我的推荐方案构建一个以线性回归为基础融合时序特征和价格弹性的混合模型。具体来说可以将“星期几”Weekday转化为哑变量捕捉周末效应引入“滞后项”如前一天的销量捕捉短期惯性最关键的是加入“价格”以及“价格与星期几的交互项”来量化需求弹性。公式雏形大致为预测销量 β0 β1*星期几哑变量 β2*昨日销量 β3*价格 β4*价格*周末哑变量 误差项。这个模型结构简单可解释性强能清晰看出价格变化对销量的影响在不同日子是否不同。对于定价决策这本质上是一个优化问题。目标函数是每日利润利润 (定价 - 成本价) * 预测销量(定价) - 损耗成本。约束条件包括定价通常有上下限比如不能低于成本不能高得离谱以及可能的价格平滑性约束相邻日价格变动不超过一定比例。为什么用优化因为我们需要在连续的定价空间中搜索那个让利润最大的点。预测模型给出了销量 f(价格)的关系将其代入利润公式就得到了一个关于价格的一元函数或多元函数如果考虑多品类联动求其最大值即可。实操方法对于单个单品单日由于f(价格)通常是非线性的需求随价格上涨而下降我们可以采用网格搜索或梯度下降等数值优化方法。如果问题规模大可考虑使用SciPy.optimize库。一个重要的技巧是将价格离散化例如以0.1元为步长在[成本价1.1 成本价3.0]区间内搜索既能简化计算也符合实际定价精度。对于补货决策这属于库存管理问题经典模型是报童模型或其扩展。其核心是权衡缺货成本机会损失和过剩成本损耗和资金占用。报童模型思想最优补货量应使得最后一单位产品被售出的概率边际收益等于其被损耗的概率边际成本。在我们的场景中需求是随机的由预测模型给出一个分布如均值和方差成本包括进货成本、潜在的损耗成本和库存持有成本。具体计算一个实用的方法是首先通过需求预测得到未来每日需求的概率分布例如假设服从以预测值为均值、以预测误差标准差为方差的正态分布。然后计算临界比临界比 (售价 - 进价) / (售价 - 进价 进价 * 损耗率 单位库存持有成本)。这个比值代表了产品售出的相对收益。接着找到需求分布的分位数使得需求小于等于该值的概率等于临界比这个分位数就是理论上的最优补货量。最后还需考虑现有库存和最小陈列量等现实约束进行调整。注意在实际编程中预测、定价、补货三个模块需要迭代或联立求解。一种简化但有效的策略是顺序决策先基于历史平均价格预测需求然后优化定价再用优化后的价格代入预测模型得到更精确的需求最后计算补货量。虽然这不是全局最优解但计算高效且易于实现对于竞赛和实际应用初期常常够用。3. 核心模块的细节实现与代码剖析思路清晰后我们进入实战环节。我会使用Python结合pandas,numpy,statsmodels,scipy等库一步步实现上述框架。假设我们的数据已经加载进一个名为df的DataFrame中包含date日期,category品类,sales_volume销量,selling_price售价,cost_price成本价,waste损耗量等字段。3.1 数据预处理与特征工程这是所有模型效果的基石耗时但至关重要。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler # 1. 数据清洗 # 检查缺失值 print(df.isnull().sum()) # 处理异常值例如销量或价格为负或极大值 df df[(df[sales_volume] 0) (df[selling_price] df[cost_price])] # 2. 特征工程 df[date] pd.to_datetime(df[date]) df[day_of_week] df[date].dt.dayofweek # 周一0, 周日6 df[is_weekend] df[day_of_week].apply(lambda x: 1 if x 5 else 0) # 周末标识 df[lag1_sales] df.groupby(category)[sales_volume].shift(1) # 昨日销量 df[price_ratio] df[selling_price] / df[cost_price] # 加价率重要特征 df[waste_rate] df[waste] / (df[sales_volume] df[waste] 1e-5) # 损耗率 # 3. 为每个品类单独处理这里以‘菠菜’为例 spinach_df df[df[category] 菠菜].copy().reset_index(dropTrue) # 4. 划分训练集和验证集例如前24天训练后6天验证 train_df spinach_df.iloc[:24] val_df spinach_df.iloc[24:30]实操心得lag1_sales滞后一期销量是一个威力强大的特征它能捕捉销售的惯性。price_ratio加价率比绝对价格更能反映定价策略。计算waste_rate时分母加上一个极小值1e-5是为了防止除零错误。务必按品类分组进行shift操作否则会混入其他品类的信息造成数据泄露。3.2 需求预测模型的构建与训练我们使用statsmodels库来构建一个可解释的线性模型。import statsmodels.api as sm # 准备训练数据 # 选择特征周末标识、昨日销量、价格、价格与周末的交互项 X_train train_df[[is_weekend, lag1_sales, selling_price]].copy() X_train[price_weekend_interaction] X_train[selling_price] * X_train[is_weekend] # 添加常数项截距 X_train sm.add_constant(X_train) y_train train_df[sales_volume] # 训练OLS模型 model sm.OLS(y_train, X_train.astype(float)).fit() print(model.summary()) # 解读关键输出 # 1. R-squared: 模型解释力。0.7通常不错但时序数据要求可放宽。 # 2. P|t|: 特征显著性。通常0.05认为该特征有效。 # 3. coef: 系数。例如‘selling_price’系数为负说明价格上升销量下降符合常识。 # ‘price_weekend_interaction’的系数若显著说明周末的价格弹性与平日不同。注意事项statsmodels的OLS对缺失值敏感确保X_train和y_train中没有NaN。如果lag1_sales第一行是NaN需要在特征工程后或建模前.dropna()。模型摘要中的Durbin-Watson统计量可以检验残差自相关如果接近2则较好远离2说明可能遗漏了重要的时序特征如前日误差此时可考虑加入残差滞后项或使用更专业的时序模型。3.3 定价优化函数的实现基于训练好的需求预测模型我们定义利润函数并进行优化。from scipy.optimize import minimize_scalar def predict_demand(price, is_weekend, lag_sales, model): 根据价格、周末标志和昨日销量预测需求 # 构建预测所需的特征向量 X_pred pd.DataFrame({ const: [1], is_weekend: [is_weekend], lag1_sales: [lag_sales], selling_price: [price], price_weekend_interaction: [price * is_weekend] }) # 确保列顺序与训练时一致 X_pred X_pred[model.params.index] return model.predict(X_pred)[0] def profit_function(price, cost, is_weekend, lag_sales, model, waste_rate0.05): 计算给定价格下的预期利润 predicted_demand predict_demand(price, is_weekend, lag_sales, model) # 简单假设损耗与销量成正比 expected_waste predicted_demand * waste_rate expected_sales predicted_demand - expected_waste profit (price - cost) * expected_sales - cost * expected_waste return -profit # 取负因为我们要最小化负利润即最大化利润 # 对某一天进行定价优化例如预测的第31天是周三is_weekend0 lag_sales取第30天实际销量 cost_price spinach_df[cost_price].iloc[-1] # 假设成本价近期稳定 last_sales spinach_df[sales_volume].iloc[-1] # 第30天销量 # 定义价格搜索边界例如成本价的1.1倍到3倍 price_bounds (cost_price * 1.1, cost_price * 3.0) # 使用有界优化算法寻找最优价格 result minimize_scalar( profit_function, boundsprice_bounds, args(cost_price, 0, last_sales, model, 0.05), methodbounded ) optimal_price result.x max_profit -result.fun print(f最优定价{optimal_price:.2f}元预期利润{max_profit:.2f}元)踩坑提醒minimize_scalar默认最小化目标函数所以我们的profit_function返回的是负利润。args参数用于传递除了待优化变量price之外的其他固定参数。务必设置合理的价格边界防止优化器跑到不现实的区域如低于成本价。对于更复杂的、需要平滑性约束如相邻日价格变动不超过10%的情况需要使用scipy.optimize.minimize并定义约束条件。3.4 补货量的计算基于最优价格和预测需求计算补货量。def calculate_order_quantity(optimal_price, predicted_demand_mean, cost, selling_price, waste_rate0.05, service_level0.95): 计算补货量。 predicted_demand_mean: 预测的需求均值 service_level: 服务水平不缺货概率 # 1. 估算需求标准差这里简化处理可用历史预测误差的标准差 # 假设我们通过验证集计算了预测误差的标准差 demand_std demand_std 10 # 示例值需根据实际预测误差计算 # 2. 计算临界比 (Critical Ratio) # 单位过剩成本主要是损耗成本和资金占用简化用进价*损耗率代表 overage_cost cost * waste_rate # 单位缺货成本错过销售的利润损失 underage_cost selling_price - cost critical_ratio underage_cost / (underage_cost overage_cost) # 3. 计算安全库存和补货点 (基于报童模型思想) from scipy.stats import norm # 安全库存因子 (Z-score) z norm.ppf(critical_ratio) # 根据临界比查正态分布分位数 # 最优补货量基础量 安全库存 order_quantity predicted_demand_mean z * demand_std # 4. 考虑实际约束最小陈列量、库存上限、取整等 min_display 5 max_inventory 200 # 假设期初库存为0则补货量就是订单量 order_quantity max(min_display, order_quantity) # 不低于最小陈列量 order_quantity min(order_quantity, max_inventory) # 不高于库存上限 order_quantity np.round(order_quantity) # 取整 return order_quantity # 使用示例 pred_demand predict_demand(optimal_price, 0, last_sales, model) order_qty calculate_order_quantity(optimal_price, pred_demand, cost_price, optimal_price) print(f预测需求{pred_demand:.1f}建议补货量{order_qty:.0f})核心要点norm.ppf是正态分布的百分位点函数逆CDF。critical_ratio是模型的核心它量化了“多进一单位货”的边际收益与风险的平衡。demand_std需求标准差的估计至关重要如果历史预测误差大就需要更高的安全库存更大的z * demand_std。实际中overage_cost和underage_cost可以定义得更精细例如包含库存持有成本、顾客流失的长期损失等。4. 系统集成、验证与结果分析将以上模块串联形成从第31天到第37天的滚动预测与决策循环。4.1 滚动预测决策流程def rolling_decision_pipeline(df, model, start_forecast_day31, horizon7): 滚动进行未来7天的定价与补货决策。 df: 包含历史数据的DataFrame model: 训练好的需求预测模型 decisions [] current_df df.copy() for day_offset in range(horizon): forecast_date start_forecast_day day_offset # 判断是周几 # 这里需要根据实际日期推算假设第30天是已知的最后一天 is_weekend 1 if ((df[day_of_week].iloc[-1] day_offset 1) % 7) 5 else 0 # 获取“昨日”数据对于第一天预测昨日是历史最后一天后续是模拟的上一天 if day_offset 0: last_day_sales current_df[sales_volume].iloc[-1] last_day_price current_df[selling_price].iloc[-1] current_inventory 0 # 假设每天清货期初库存为0简化问题 else: # 这里需要从之前的决策模拟结果中获取为简化我们用一个占位逻辑 # 实际中你需要维护一个模拟的库存和销售状态 last_day_sales decisions[-1][predicted_sales] last_day_price decisions[-1][optimal_price] current_inventory max(0, decisions[-1][order_quantity] - decisions[-1][predicted_sales]) # 1. 定价优化 cost df[cost_price].mean() # 假设成本不变 res minimize_scalar( profit_function, bounds(cost*1.1, cost*3), args(cost, is_weekend, last_day_sales, model, 0.05), methodbounded ) optimal_price res.x # 2. 需求预测 pred_demand predict_demand(optimal_price, is_weekend, last_day_sales, model) # 3. 计算补货量考虑当前库存 needed_demand max(0, pred_demand - current_inventory) order_qty calculate_order_quantity(optimal_price, needed_demand, cost, optimal_price) # 4. 记录决策 decision { date: forecast_date, is_weekend: is_weekend, optimal_price: round(optimal_price, 2), predicted_demand: round(pred_demand, 1), current_inventory: round(current_inventory, 1), order_quantity: round(order_qty), expected_profit: round(-res.fun, 2) } decisions.append(decision) # 模拟更新状态用于下一天决策。此处简化实际需模拟销售过程。 # simulated_sales min(pred_demand, current_inventory order_qty) * (1 - waste_rate) return pd.DataFrame(decisions) # 运行决策管道 future_decisions rolling_decision_pipeline(spinach_df, model) print(future_decisions)这个流程模拟了现实中的每日决策基于最新状态昨日销量、当前库存决定今天卖什么价、进多少货。这里的巨大简化是假设每日库存清零实际比赛中可能需要考虑库存结转、多日保鲜期等复杂约束。4.2 模型验证与效果评估我们不能只“自嗨”地预测未来必须用历史数据验证模型的有效性。常用方法是时间序列交叉验证或留出法。# 使用验证集评估需求预测精度 def evaluate_model(model, val_df): predictions [] actuals [] # 模拟在验证集上进行滚动一步预测 for i in range(len(val_df)): if i 0: # 预测第一天使用训练集最后一天的数据作为滞后项 lag_sales train_df[sales_volume].iloc[-1] else: lag_sales val_df[sales_volume].iloc[i-1] row val_df.iloc[i] price row[selling_price] is_weekend row[is_weekend] pred predict_demand(price, is_weekend, lag_sales, model) actual row[sales_volume] predictions.append(pred) actuals.append(actual) # 计算评估指标 from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error mae mean_absolute_error(actuals, predictions) mape mean_absolute_percentage_error(actuals, predictions) print(f验证集评估MAE{mae:.2f}, MAPE{mape:.2%}) # 可视化对比 import matplotlib.pyplot as plt plt.figure(figsize(10,6)) plt.plot(val_df[date], actuals, labelActual Sales, markero) plt.plot(val_df[date], predictions, labelPredicted Sales, markers) plt.xlabel(Date) plt.ylabel(Sales Volume) plt.title(Demand Prediction vs Actual on Validation Set) plt.legend() plt.grid(True) plt.show() return mae, mape mae, mape evaluate_model(model, val_df)评估标准MAE平均绝对误差反映绝对误差大小MAPE平均绝对百分比误差反映相对误差更适合不同量纲的比较。对于蔬菜销售预测MAPE在15%-25%之间通常是可以接受的具体取决于品类和数据的平稳性。如果误差过大需要回到特征工程或模型选择步骤进行调整。4.3 结果分析与策略解读运行完模型后我们得到了一份未来7天的决策表。分析这份表格能获得远超题目要求的商业洞察日期是否周末最优定价(元)预测需求(kg)补货量(kg)预期利润(元)31否3.5085.290152.332否3.4587.592156.833否3.4886.191154.134是3.80102.3108195.435是3.85100.5106193.736否3.5283.788149.937否3.4986.891155.2从表中我们可以读出什么周末效应明显周末第34、35天的最优定价显著高于平日同时预测需求也大幅增加。这说明模型成功捕捉到了“周末需求旺盛价格承受能力更强”的市场规律。补货量也相应提高以应对增长的需求。价格平滑性工作日之间的价格波动很小3.45-3.52元这符合“价格不宜频繁剧烈变动”的商业常识。我们的模型虽然没有显式加入平滑约束但由于需求曲线和成本结构相对稳定自然产生了平滑的结果。如果出现跳跃可以考虑在优化目标中加入(price_t - price_{t-1})^2这样的惩罚项。利润导向预期利润在周末达到峰值。整个决策系统的核心驱动力正是利润最大化。补货策略补货量略高于预测需求这包含了基于critical_ratio计算的安全库存以应对需求的不确定性。模型的可解释性优势在此凸显我们可以打开“黑箱”告诉店长“你看因为周末客流大大家对价格不那么敏感价格弹性系数小所以我们可以适当提价0.3元这样总收入增加更多利润也更高。”这样的解释比单纯丢出一个数字更有说服力。5. 常见问题、优化方向与参赛建议在实际动手和比赛过程中你一定会遇到各种各样的问题。这里我总结了一些典型问题和进阶思路。5.1 实操中遇到的典型问题与排查问题需求预测模型R-squared很低比如0.3预测不准。排查首先检查数据是否有未处理的异常值或缺失值其次检查特征是否有效。lag1_sales的系数是否显著价格系数是否为负如果不显著说明这些特征与销量关系不强。解决增加特征尝试加入“节假日”标志、气温如果有、促销活动等信息。加入价格的二次项(price^2)或与其他特征的交互项。处理非线性如果怀疑价格与销量是非线性关系如价格很低时弹性小很高时弹性大可以尝试对价格取对数或使用分段线性回归。更换模型可以尝试决策树回归如RandomForestRegressor或梯度提升树如XGBoost它们能自动处理非线性关系。但务必注意防止过拟合并做好特征重要性分析。问题定价优化结果总是跑到边界上比如总是给出最高允许价。排查这通常意味着你的需求预测模型对价格不敏感价格系数绝对值太小或者利润函数中售价的边际收益始终为正。解决重新审视需求预测模型。确保价格变量是显著的并且其系数大小合理可以通过计算点弹性来验证弹性 价格系数 * (平均价格/平均销量)。如果弹性绝对值太小如-0.2说明模型认为降价也拉不动销量那自然就会定高价。可能需要收集更多数据或引入更能反映价格敏感度的特征。问题补货量计算出来是负数或极大值。排查检查critical_ratio的计算公式。确保underage_cost售出收益和overage_cost过剩成本定义正确且不为零。检查demand_std的估计是否合理不能为0或NaN。解决给成本项加上小的平滑项。对demand_std设定一个下限如平均需求的10%。对最终补货量施加硬约束最小陈列量、最大仓储能力。问题滚动预测中误差会逐日累积放大。排查这是因为你使用昨天的“预测销量”作为今天预测的lag1_sales特征输入。一旦第一天预测有偏差这个偏差会带入第二天形成恶性循环。解决重要技巧在滚动预测时对于lag1_sales不要使用上一期的预测值而应该使用一个经过修正的估计值。例如可以使用(上一期预测值 上一期实际值) / 2的混合值或者更复杂地使用一个基于预测误差的校正模型。在比赛中如果允许可以假设我们能获取“昨日实际销量”这能极大提升多步预测的稳定性。5.2 模型的优化与扩展方向上述方案是一个稳健的起点。要追求更高分数或更贴近实用可以考虑以下扩展多品类联合定价与补货题目中有6个品类。它们之间可能存在替代菠菜贵了顾客买生菜或互补买西红柿的人常买鸡蛋关系。可以在需求预测模型中引入其他品类的价格作为特征或者在优化目标中考虑整体利润最大化而非单品独立优化。这会将问题升级为多元回归和高维优化问题。引入动态成本与损耗模型我们的模型假设成本价固定损耗率恒定。现实中批发价可能波动损耗与库存时间和销量密切相关。可以建立更精细的损耗函数如waste f(库存量, 时间)并将成本作为随时间变化的参数。考虑库存结转与保质期这是国赛题中常见的深化点。蔬菜有保质期如3天当天的补货如果没卖完可以进入第二天库存但价值可能折损新鲜度下降。这就需要引入状态变量每日库存水平和动态规划如马尔可夫决策过程来求解多期最优决策难度会大大增加但也是拉开差距的关键。需求预测的不确定性处理我们之前用demand_std和安全库存来处理不确定性。更高级的方法是进行概率预测如使用分位数回归、贝叶斯方法直接给出未来需求的概率分布然后基于这个分布进行随机优化得到鲁棒性更强的决策。5.3 给数学建模参赛者的建议论文重于代码评委首先看的是你的论文。思路清晰、图表美观、论述严谨的论文即使模型简单也能获得好成绩。务必花时间写好问题分析、模型假设、符号说明部分。可视化是利器将数据趋势、价格-销量散点图、预测对比图、决策结果图清晰地展示出来。一图胜千言。灵敏度分析必不可少在模型中改变关键参数如成本上浮10%、需求波动增加观察定价和补货决策如何变化。这能体现你对模型稳健性的思考是论文的加分项。不要盲目追求复杂模型先用简单、可解释的模型如线性回归、报童模型搭建完整框架确保每个环节都跑通并能产生合理的结果。如果有时间再用复杂模型如XGBoost、神经网络、遗传算法优化进行改进和对比并在论文中说明改进的效果。分工合作团队中最好有人负责建模与编程有人负责论文写作与可视化有人负责整体思路把控与校验。定期同步进度避免最后时刻整合出现问题。回过头看“蔬菜定价与补货”这个题目就像是一个微缩版的商业智能系统。它考验的不仅仅是你对回归、优化、库存模型等知识的掌握更是你将实际问题抽象化、数学化并最终用计算工具解决的综合能力。这个过程里最大的收获不是那个最优解的数字而是那种抽丝剥茧、层层递进解决问题的思维模式。当你下次走进超市看着货架上琳琅满目的蔬菜和价签你脑海里浮现的或许不再只是今晚吃什么而是背后那一套精密的、动态的、追求平衡的决策系统在默默运转。