高教社杯C题:蔬菜自动定价与补货决策全解析
发布时间:2026/9/18 4:05:50 作者:尧图编辑部 阅读量:1,286

简介一份面向2023年高教社全国大学生数学建模竞赛C题的完整参考论文重点围绕蔬菜类商品自动定价与补货决策问题展开适合参赛团队用于赛题复盘、模型对比与论文框架参考也可作为运筹优化方向毕业设计的写作模板。论文系统覆盖四个子问题基于历史销售数据的品类与单品分布规律挖掘、成本加成定价下的销售量预测、单品补货量与定价策略的组合优化以及后续数据采集建议建模过程中使用了线性回归、相关性分析、时间序列ARIMA、混合整数规划和贪心算法等方法并提供了问题重述、模型假设、符号说明和求解过程能够帮助读者理解从数据处理到策略生成的全流程。包内为单个PDF文件共1.62MB内容结构完整关键词清晰可直接对照阅读。目前已有170人学习下载是优化类数学建模赛题的重要参考素材。1. 2023高教社数学建模国赛C题本质是在给超市做一套数据驱动的蔬菜定价与补货策略2023年高教社杯全国大学生数学建模竞赛C题“蔬菜类商品的自动定价与补货决策”表面上是一道竞赛题实际是零售行业中一个非常典型的运营决策问题给定历史销售流水、批发价格和损耗率如何为每个蔬菜单品找到“明天定什么价、进多少货”的最优解。多数参赛队伍把精力放在回归预测上预测完销量就停下来但题目真正要的是“决策”而不是“预测”。蔬菜的保质期短、品类多、单价波动快隔天卖不掉就要打折出清这就把问题从单一预测问题变成了一个带约束的联合优化问题价格影响需求需求又反过来决定最优补货量和损耗期望。这道题适合正在做零售数据分析、供应链算法或者刚接触运筹优化的从业者它能完整覆盖从数据清洗、需求建模到非线性规划求解的完整链路而且数据集规模适中单机就能跑。2. 2023C题数据长什么样以及题目背后的固定约束2.1 六个蔬菜大类、单品编码和销售流水的事件结构国赛C题给出的数据一般包含销售流水表、批发价格表以及可能存在的损耗率或商品信息表。销售流水是最核心的输入每一行代表某天某个蔬菜单品在某条渠道的一次销售记录字段通常包含销售日期、单品编码、单品名称、销量千克、销售单价元/千克和是否打折标识。批发价格表则按日期和单品编码给出当天的进价需要注意的是很多行在周末或者节日前是缺值的因为批发市场的报价周期和超市销售周期并不完全对齐。背后的业务是典型的“日清日用”模式蔬菜类商品当天进货、当天定价闭店前如果还有库存要么转入第二天继续卖要么打折出清。损耗率不是统一值叶菜类和菌菇类差异极大常见取值范围在5%到30%。竞赛题不要求选手去仿真门店排队但它要求你承认一个关键事实补货量一旦超过真实需求剩余部分就要承受损耗成本这个成本和利润直接挂钩所以在目标函数里必须把损耗写成期望损失项。数据字典参考如下实际比赛文件字段名称略有不同但结构基本一致。数据域字段名类型业务含义销售流水sale_datedatetime销售日期粒度到天销售流水product_codestring单品编码对应SKU销售流水product_namestring单品名称如“云南生菜”销售流水sale_qtyfloat销量单位千克销售流水sale_pricefloat实际成交均价元/千克销售流水is_discountint是否折扣日销售0或1批发行情wholesale_pricefloat当日批发价元/千克商品属性categorystring大类如“花叶类”“茄果类”商品属性loss_ratefloat该品类的平均损耗率2.2 用Pandas拉平数据按天生成单品维度的事实表拿到原始文件后第一步不是建模而是把“流水”压缩成“事实表”。销售流水是一天多行同一个单品同一天可能以原价和折扣价分别成交要合并成一行。合并时有两个细节容易踩坑折扣日要把打折量和原价量分开标注否则定价模型会把折扣日销量当成正常价格下的需求批发价表要按日期做向前填充因为批发市场周末可能不开市但超市还在营业。以下代码把原始流水按单品和日期聚合并关联批发价和损耗率。需要注意日期字段要统一成pd.Timestamp编码要统一成字符串否则两个表关联时静默产生笛卡尔积。import pandas as pd import numpy as np df_sales pd.read_csv(sales_flow.csv, parse_dates[sale_date]) df_wholesale pd.read_csv(wholesale_price.csv, parse_dates[date]) df_product pd.read_csv(product_info.csv) # 单品编码统一为字符串避免整数和字符串匹配失败 df_sales[product_code] df_sales[product_code].astype(str) df_wholesale[product_code] df_wholesale[product_code].astype(str) df_product[product_code] df_product[product_code].astype(str) # 按 日期单品 聚合销量同时保留折扣销量 df_daily ( df_sales.groupby([sale_date, product_code]) .agg( sale_qty(sale_qty, sum), discount_qty(sale_qty, lambda x: sum(x[df_sales.loc[x.index, is_discount] 1])), avg_price(sale_price, mean), ) .reset_index() ) # 批发价按日期向前填充填充前先按单品分组也是安全的 df_daily df_daily.merge( df_wholesale.sort_values(date), left_on[sale_date, product_code], right_on[date, product_code], howleft, ) df_daily[wholesale_price] df_daily.groupby(product_code)[wholesale_price].ffill() # 关联损耗率 df_daily df_daily.merge(df_product[[product_code, category, loss_rate]], onproduct_code, howleft)这段代码有两个逻辑要点。discount_qty用lambda在groupby内部做条件求和前提是x.index仍然指向原始DataFrame的行号这样df_sales.loc[x.index, is_discount]拿到的判断结果和参与求和的数值行能一一对应比先打标签再聚合更省内存。批发价ffill()是按单品分组的直接按全表填充会把上个单品的价格带到下个单品上所以必须先groupby(product_code)再填充。聚合完成后应检查wholesale_price的缺失率如果某单品缺失超过30%说明这个品类的批发价记录本身不完整建模时最好整段剔除。2.3 这道题的三个隐藏约束打折出清、保鲜周期和陈列上限题目原文里没有直说但数据里隐含了一条决策规则当天没有卖完的蔬菜第二天要么原价续卖要么打折出清。竞赛评分点一般落在“单品定价是否合理”和“补货量是否贴近实际需求”上所以不能只算一个静态毛利率要建立动态策略。实际定价时超市蔬菜课长不会给每个单品独立定价而是按品类统一加价率但建模时如果按品类统一就丢掉了单品之间损耗率的差异对最优价格的修正作用。另一个约束是陈列上限和单次下单上限题目可能没有给出明确数字但作为从业者方案应该加入一个库存容量C_i补货量不能超过这个容量。加上这个约束后问题从“无约束利润最大化”变成“带容量约束的定价库存联合决策”求解难度提升一个档次但这也正是竞赛阅卷时分辨普通论文和完整方案的分界线。保鲜周期也类似叶菜类按1天计算根茎类按3天计算。处理方式是给每个单品设置最大可售天数超出后剩余库存损耗率为100%这个参数会在仿真回测里直接决定库存策略的优劣。3. 蔬菜需求函数与价格弹性从历史销量到可计算的决策依据3.1 为什么不能直接用历史平均销量做补货很多队伍的基准模型是取过去7天销量均值乘以一个安全系数1.2作为明天的补货量。这个逻辑在常规快消品上没问题但蔬菜不行。因为蔬菜的销售价格每天波动价格变动直接改变当日需求量菠菜卖4元可能卖出200斤卖6元可能连100斤都卖不掉。如果你用含高价日的平均销量预测明天的补货量而明天恰好降价就会缺货反过来则会造成积压。所以要先把“价格”这个变量显式放进需求模型里建立价格-销量响应函数而不是简单做时间序列外推。这里采用对数线性需求函数ln(q) alpha beta * ln(p) gamma * D_t epsilon。其中beta就是价格弹性是一个负数表示价格上涨1%时销量变化的百分比D_t是日期特征包括星期几、是否节假日、是否折扣日。对数线性形式在零售定价文献里是标配一方面参数有明确经济学解释另一方面做利润优化时可以直接求解析导数或做网格搜索不需要复杂的非线性回归工具。3.2 用Statsmodels拟合单个SKU的价格弹性并处理周末效应对于每个单品单独拟合需求函数。样本量太少的单品销售天数少于20天建议剔除因为回归系数不稳定。下面代码用了statsmodels的OLS同时加入周末哑变量避免把周末的自然需求上升归因到价格上。import statsmodels.api as sm def fit_demand_sku(df_sku): df df_sku.copy() df df[df[avg_price] 0] df[ln_qty] np.log(df[sale_qty]) df[ln_price] np.log(df[avg_price]) df[is_weekend] df[sale_date].dt.weekday.isin([5, 6]).astype(int) X df[[ln_price, is_weekend]].copy() X sm.add_constant(X) y df[ln_qty] model sm.OLS(y, X).fit() return { alpha: model.params[const], beta: model.params[ln_price], gamma: model.params[is_weekend], p_value: model.pvalues[ln_price], n: len(df), aic: model.aic, } # 按单品编码分组拟合 results {} for code, df_sku in df_daily.groupby(product_code): try: results[code] fit_demand_sku(df_sku) except Exception: continue df_elasticity pd.DataFrame(results).T df_elasticity[beta].hist(bins30)beta落在-2.5到-0.3之间是正常区间如果出现正值或者绝对值大于5大概率是样本太短或价格数据有问题比如某单品长期不打折、价格几乎没有波动回归自然无法识别弹性。此时对该单品应放弃回归参数改用品类平均弹性代替。p_value大于0.05也不能说明弹性不存在只能说明这个单品的价格变异不足不足以支持独立估计。另外要注意周末效应必须显式建模蔬菜的周末销量通常比工作日高出30%以上忽略它会导致价格系数被高估。3.3 内生性处理价格不是外生的打折日要特殊对待直接回归会遭遇一个内生性问题超市在库存压力大的时候会更积极地打折促销所以价格低的同时销量高并不完全是价格敏感的体现还有促销事件本身的驱动作用。换言之价格和误差项相关OLS估计的弹性偏大。竞赛里多数队伍不处理这个问题但评分高的论文一般会提到。常见做法有两种第一种是只使用非折扣日的样本来拟合需求函数把打折日样本用于校验第二种是引入工具变量比如用同品类其他单品的平均批发价作为该单品价格的IV因为批发价影响单品定价但不直接影响该单品的日需求。用statsmodels的IV2SLS实现第二种处理关键是找到合法的工具变量。from statsmodels.sandbox.regression.gmm import IV2SLS df_all df_daily.copy() df_all[ln_qty] np.log(df_all[sale_qty]) df_all[ln_price] np.log(df_all[avg_price]) # 工具变量该单品所属品类当日的平均批发价 category_wholesale ( df_all.groupby([sale_date, category])[wholesale_price] .transform(mean) ) df_all[iv_category_ws] np.log(category_wholesale) # 第一阶段价格对工具变量回归 X_stage1 sm.add_constant(df_all[[iv_category_ws]]) stage1 sm.OLS(df_all[ln_price], X_stage1).fit() df_all[price_hat] stage1.fittedvalues # 第二阶段用拟合价格替代原价格 X_stage2 sm.add_constant(df_all[[price_hat, is_weekend]]) stage2 sm.OLS(df_all[ln_qty], X_stage2).fit()第二阶段里price_hat的系数才更接近真实的价格弹性。为什么品类的平均批发价是合理的IV因为批发价格是外部市场决定的不受单个门店某天卖不卖得动的影响单个门店的日销量也无法撼动批发市场报价。而超市定价通常参考进价加成批发价越高售价越高满足“工具变量与内生解释变量相关”的前提。实操中如果品类内单品数量少于5个IV容易变成弱工具变量此时回归结果反而比OLS更糟稳妥做法是直接删除打折日样本做敏感性对照。4. 定价与补货联合决策构造利润函数寻找最优价格和最优订货量4.1 目标函数毛利减损耗不只看单日还要看剩余库存的期望损失蔬菜定价决策不能用“销售毛利销量*(售价-进价)”这么简单的公式因为日末剩余库存不是无成本的。剩余库存要么次日继续卖产生资金占用和品质下降要么直接报废。更合理的做法是把问题拆成两步决策变量是明天的定价p和补货量Q在进货前就把明天的需求分布当作已知函数计算期望利润。期望利润函数写作E[Profit(p, Q)] p * E[min(D(p), Q)] - w * Q - c_loss * E[(Q - D(p))^]其中D(p)是价格p下的随机需求w是批发价c_loss是单位剩余蔬菜的损耗成本表达式中(Q-D)^表示补货量超过需求的部分。如果剩余库存可以次日继续卖c_loss应取持有成本加次日折价损失如果当天报废则c_loss等于进货成本。竞赛题默认损耗率高且保鲜期短所以c_loss取进货成本即可。需求的不确定性如何处理不能只用点预测值。有两个方案如果只做静态规划则把D(p)写成点预测并用一个比例系数补偿方差如果要做完整决策则用历史残差的经验分布构建需求场景。下面给出的是场景化求解的骨架使用历史残差生成500个可能需求最大化平均利润。from scipy.optimize import minimize_scalar def expected_profit_per_unit(p, Q, w, alpha, beta, gamma, weekend, resid, loss_cost): # 预测对数需求 ln_demand alpha beta * np.log(p) gamma * weekend base_demand np.exp(ln_demand) # 用历史残差生成场景 demand_scenarios base_demand * np.exp(resid) sales np.minimum(demand_scenarios, Q) leftover np.maximum(Q - demand_scenarios, 0) profit p * sales.mean() - w * Q - loss_cost * leftover.mean() return profit def optimize_sku(sku_param, weekend0): w sku_param[wholesale_price] resid sku_param[resid_array] # 外层对价格网格搜索 best_profit -np.inf best_p, best_Q None, None for p in np.linspace(w * 1.1, w * 2.0, 30): # 内层对Q做一维搜索 res minimize_scalar( lambda Q: -expected_profit_per_unit( p, Q, w, sku_param[alpha], sku_param[beta], sku_param[gamma], weekend, resid, sku_param[loss_cost] ), bounds(0, sku_param[capacity]), methodbounded, ) if -res.fun best_profit: best_profit -res.fun best_p, best_Q p, res.x return best_p, best_Q, best_profit代码走的路线是先离散价格再连续搜索补货量避免二维非线性规划遇到局部最优。minimize_scalar用bounded方法在[0, capacity]区间内找最优订货量。lambda Q: -expected_profit...加负号是因为scipy的求解器默认做最小化。价格网格的上下界按批发价浮动下界1.1倍进价保证毛利为正上界2倍进价防止价格跑得太远。如果实际定价规则是“不超过进价的150%”就把上界改成w * 1.5。4.2 单品级定价结果长什么样参数敏感度怎么评估对每个单品跑完上述优化后会得到一张决策表单品编码、建议定价、建议补货量、对应期望利润。一个常见结果是损耗率高的叶菜单品最优定价高于常规加价率因为必须用更高毛利覆盖损耗风险而损耗率低的根茎类最优定价更接近进价的1.2倍。下表是一个模拟输出示例展示价格弹性、损耗率和最优定价之间的关系。单品进价(元/kg)弹性beta损耗率最优定价(元/kg)加价率上海青3.2-1.480.184.81.50西兰花5.6-0.870.258.41.50土豆2.4-0.420.063.11.29鲜香菇9.8-0.710.1213.71.40上表反映出的规律是损耗率高的单品加价率不一定最大还要看价格弹性。西兰花损耗高但弹性较小老客户刚需所以可以加价50%上海青弹性大加价超过正常区间会导致销量大幅萎缩损耗反而更高。土豆弹性小、损耗低最优加价率就是全品类最低。因此定价策略的复杂度主要来自弹性和损耗率的二维交互。拟合时如果发现某个单品弹性为绝对值小于0.3说明该单品属于刚需品最优价格大概率贴着价格上限走不必精细搜索直接把价格设为上限通过补货量来控制库存风险即可。4.3 多品类的联动约束和“销量替代效应”的近似处理单独算完每个单品之后还有一个实际问题蔬菜柜台的消费者带有很强的替代性生菜贵了就买油菜菜心贵了就买芥蓝。严格做法是建立交叉弹性矩阵估计每个单品价格对其他单品销量的影响但竞赛数据通常不够支撑这么大矩阵的估计。退而求其次常见做法是把需求函数加一个品类粘性项即品类总预算有限用“品类需求上限”来约束单品预测值的总和。约束条件可以写成同品类下所有单品预测销量之和不超过历史品类的日均销量乘以季节系数S_t。这个约束把独立单品优化变成有约束的资源配置问题。简化实现可以在单品优化后做一个后验修正如果某品类总补货量超过上限则按单品毛利贡献从低到高依次削减补货量削减优先作用于高损耗单品。这里给出的代码段是对补货量做按比例压缩def category_capacity_control(df_plan, category, cap_ratio1.15): mask df_plan[category] category total_qty df_plan.loc[mask, order_qty].sum() hist_avg_qty df_plan.loc[mask, hist_avg_qty].mean() cap hist_avg_qty * cap_ratio if total_qty cap: # 按损耗率加权缩小损耗越高的单品削得越多 df_plan.loc[mask, order_qty] df_plan.loc[mask].apply( lambda r: r[order_qty] * cap / total_qty * (1 / (1 r[loss_rate])) * 0.9, axis1, ) return df_plan压缩系数0.9和(1/(1loss_rate))是根据损耗率加权的近似值不是严格梯度求解但在竞赛场景够用。使用品类容量控制时要注意cap_ratio参数不能设得太低否则会把销量预测强压到历史水平之下失去挖掘潜力的机会。更精细的做法是用线性规划一次性求解全局最优把单品利润函数做二阶近似后放入线性约束但实现复杂度高且容易因为需求估计误差而过度拟合不建议在没有足够数据的情况下直接上。5. 用滚动回测和98%服务水平把定价补货方案做到可落地5.1 滚动回测框架上一步的决策必须落到下一步的库存上做好了定价和补货决策表还要证明它在历史数据上有效。滚动回测的思路是用第t天及之前的数据做参数估计生成第t1天的定价和补货策略然后用真实第t1天的销量验证效果并把真实销量滚动进入下一轮训练集。这样每个决策都是在当时信息集下做出的不会引入前视偏差。代码骨架如下def rolling_backtest(df, lookback21, step1): all_dates sorted(df[sale_date].unique()) results [] inventory {} for i in range(lookback, len(all_dates) - 1, step): train_end all_dates[i] test_date all_dates[i 1] df_train df[df[sale_date] train_end] df_test df[df[sale_date] test_date] # 用前面训练窗口拟合弹性 # 生成明天的定价和补货量 # 处理昨日剩余库存再执行当天的真实销售 ... results.append(backtest_row) return pd.DataFrame(results)回测里必须处理一个细节昨天的剩余库存会自动结转到今天所以今天的需求要先消化昨天的库存再订购新货。这就意味着补货量不是单纯等于预测需求而是max(0, 目标库存 - 昨日剩余)。如果模型没有显式表达这个库存滚动关系回测结果会高估需要的订货量在实际落地时会把冷库堆满。5.2 服务水平约束保证不缺货但别让损耗吃掉利润单纯最大化期望利润会得到一个激进的策略负毛利的单品直接不进货高损耗单品订货量压得很低。这在数学上正确但在经营上不可行因为顾客常年买不到某几种菜会降低对超市生鲜的整体信任。因此要加一个服务水平约束在需求的非极端日缺货概率不能高于某个阈值反过来补货量过高导致损耗率超过历史正常水平也是失败。竞赛评分不会明确列出这个指标但完整的方案应该包含这部分。服务水平约束写成公式是P(D(p) Q) service_level典型取值在85%到95%之间。加入这个约束后最优补货量会大于纯利润优化的结果差额就是为满足顾客留存而付出保险库存成本。执行层面可以用需求分布的90%分位数作为Q的下界再做优化代码里在minimize_scalar的边界参数中直接修改service_level 0.9 safety_qty np.percentile(demand_scenarios, service_level * 100) # 将优化下限改为safe_qty bounds(safety_qty * 0.8, sku_param[capacity])低位设成安全库存的80%留一点优化空间高位仍由陈列上限决定。这样求解出的补货量不会太低同时利润损失被控制在2%到5%以内。这个技巧在实际业务里特别有效因为它直接用需求经验分布的分位数替代了对“安全库存系数”的猜测销售团队更容易接受最终结果。5.3 高效落地技巧定价阶梯表和参数自动更新脚本最后一个环节是把模型输出变成门店可执行的表格。门店店员不可能每天翻优化模型输出值实际管理动作是给每个单品贴一个标签今日定价、今日限量、是否打折。竞赛论文如果只写到“优化结果”四个字不够完整建议输出一个定价阶梯表和更新脚本。定价阶梯表把价格按区间分档每个档位对应一个补货建议例如“定价3.99元/斤时补货15kg定价4.50元/斤时补货11kg”。推荐用Python生成一张pricing_rule.csv每天凌晨跑完数据更新后自动推送到门店系统或企微群。自动化脚本就是把前面所有步骤串起来拉数、清洗、拟合弹性、优化、写表。注意不做全自动改价而是要有一个max_price_change限制相邻两天的价格变动幅度不超过15%防止优化结果在边界上跳来跳去给消费者造成价格不稳定的观感。限制幅度后的次优价格与最优价格的利润差异通常小于1%这个约束对实际运营是净收益。此时整套方案可以收敛为一个可复用的每日决策工具也完整回答了C题想要的“自动定价与补货决策”闭环。本文还有配套的精品资源点击获取