拓冰建站拓冰建站
首页 / 资讯中心 / 正文

数学建模国赛C题:蔬菜定价与补货决策全解析(ARIMA+GRU+MILP)

简介这是一份聚焦2023年全国大学生数学建模竞赛C题“销售预测与库存管理”的完整获奖方案以一等奖论文与可运行Python代码为核心适合数学建模参赛者、供应链或电商方向的数据分析学习者也可作为相关毕业设计与课程设计的参考。整个压缩包共38个文件以xlsx数据表、ipynb分析笔记、py脚本、pdf论文和md说明为主合计7.93MB其中xlsx涵盖原始销售数据、损耗率、聚类与预测结果ipynb展示数据清洗、建模与可视化的完整过程py用于关键求解结构清晰便于按步骤复现。目前已有42人学习下载。资源内容覆盖赛题关键环节从单品/品类销量汇总、箱线图分析、聚类分类到ARIMA与GRU销量预测、混合整数规划求解并附有论文定稿和详细说明文档可帮助读者理解一等奖团队的分析思路、复现代码并迁移到自身项目中是竞赛备赛与算法进阶的实用素材。1. 一份把预测和定价串起来的国赛C题完整代码2023年数学建模国赛C题“蔬菜类商品的自动定价与补货决策”表面上是销量预测实际拉开分差的地方全在数据处理和优化建模。这套一等奖代码从箱线图检查单品异常销量、频数分布估计价格弹性到ARIMA与GRU双模型做月度销量预测最后用混合整数规划反推每个商品未来三个月的定价与补货量整条链路完整且可以直接复现。对备赛的人来说它提供了一条可对照的解题路径对做零售数据分析的工程师真正有价值的是需求预测与定价优化怎么衔接以及损耗率如何进入目标函数。代码里所有中间结果都写成了Excel方便核验每一步数据。建议按“单品流水→品类序列→回归弹性→预测→MILP求解”的顺序去读先跑通notebook再改参数。下面从第一步数据观察开始拆解。2. 数据观察先行箱线图、频数分布与品类合并读完“分频数各品类回归数据”和“按月汇总商品序列.xlsx”这两个中间结果才能理解为什么做第一问时比的不是预测精度而是对数据特性的把握。C题给到的是单品日报表里面既有节假日促销也有断货、调价造成的销量跳变直接拿原始序列训练ARIMA或GRU模型学到的全是噪声。2.1 箱线图里看异常促销和断货噪声的识别“箱型图(商品).ipynb”和“箱线图(品类).ipynb”这两份可视化脚本里核心不是“画图”这个动作而是箱线图给出的判断规则。一个品类下的单品销量分布往往是正偏态大部分时间销量在低位偶尔有几天冲到高位。IQR倍数法能快速标记出那几天的异常点——它们是调价还是促销直接影响后面回归方程能否通过显著性检验。def mark_outlier_by_iqr(df, value_col销量, group_col商品编码): def _flag(group): q1 group[value_col].quantile(0.25) q3 group[value_col].quantile(0.75) iqr q3 - q1 group[异常标记] (group[value_col] q1 - 1.5 * iqr) | (group[value_col] q3 1.5 * iqr) return group return df.groupby(group_col, group_keysFalse).apply(_flag)groupby再apply保证每个单品用自己的四分位数来判断而不是整个品类混在一起算。不同单品的销量级别可以差两个数量级用全局阈值会把叶菜类的高销量当成异常。拿到异常标记后建议把异常点单独建一张表回看对应日期的价格是否发生过变动再决定剔除还是保留调价引起的异常大概率保留断货导致的零销量要剔除。2.2 频数分布与品类合并定价弹性的初步估计“第二问各品类_定价_销量_频数”这张表把离散的定价切成区间统计各区间销量均值。它和“分频数各品类回归数据”配合使用目标是能在建模之前直观回答“这个品类提价5%大概会掉多少销量”。操作上用pd.cut把价格切成6到10档再按品类聚合。df[价格档] pd.cut(df[定价], bins10) freq_table df.groupby([品类, 价格档], observedTrue).agg( 销量均值(销量, mean), 记录数(销量, count) ).reset_index()注意“记录数”这一列很关键某个价格档只有一两条记录时统计量不可信回归时要降权或者剔除。一等奖代码里后面的线性回归是在这张频数表上做的而不是在原始日流水上做因为同价格点对应多个不同的销量值原始数据会被高频价格点主导。“第一问聚类分析.xlsx”则是把品类按销量波动和价格弹性做成特征后聚类的输出聚类结果把品类分成价格敏感和非敏感两类第二问回归的范围也就由这个结果决定。2.3 月度销量汇总从日粒度到预测粒度“按月销售量汇总.ipynb”的价值在于把预测难度降了一档。日销量有零值、断货空档和周末高峰时间序列模型很难抓住趋势月度汇总后的序列通常在30到40个月规律稳定适合ARIMA建模。汇总的代码量很小但有两个易错点排序和空值。def load_monthly(file_path按月汇总商品序列.xlsx): df pd.read_excel(file_path) df[月份] pd.to_datetime(df[月份]) df df.sort_values(月份) return dfpd.to_datetime是为了统一月份格式。Excel里的日期列经常被识别成字符串“2023-03-01”直接排序会按字典序2023-10排在2023-3前面。统一成datetime再排序后面的差分和季节项计算才正确。汇总出的月度序列长度通常在36到42个月这个长度对ARIMA合适对GRU偏少所以后续GRU的窗口要取小一点一般用6个月而不是12个月。“单品合并.ipynb”的作用是在分析前把同一商品在不同门店或规格下的记录合并成一条避免月度汇总时销量被重复放大这一点在跑数据前要确认。文件分析目的关键输出箱型图(商品).ipynb识别单品数据中的异常销量点异常标记表箱线图(品类).ipynb查看品类层级的销量分布差异品类波动范围第二问各品类_定价_销量_频数粗略观察价格与销量的关系各价格档销量均值按月销售量汇总.ipynb转为月度序列供预测使用商品月度序列3. ARIMA与GRU双引擎销量预测模型的选择与实现预测是第三问的前置步骤这套代码里最值得学的地方是同时跑两个模型并做对比而不是只交一份ARIMA结果。ARIMA适合平稳性较好、业务周期固定的月度序列GRU能学到更长时间范围内的非线性模式但也更容易过拟合。3.1 ARIMA建模平稳性检验、定阶与预测先看ADF检验判断是否需要差分。月度序列通常做一阶差分就够d过大会让预测退化成“几乎等于最后一个值”。差分后的序列用AIC自动搜索p和q样本量有限时搜索范围不要太大。from statsmodels.tsa.stattools import adfuller from statsmodels.tsa.arima.model import ARIMA from itertools import product def auto_arima_order(series, max_p3, max_q3): adf_p adfuller(series.dropna())[1] d 1 if adf_p 0.05 else 0 best_aic float(inf) best_order None for p, q in product(range(max_p 1), range(max_q 1)): if p 0 and q 0: continue try: model ARIMA(series, order(p, d, q)).fit() if model.aic best_aic: best_aic, best_order model.aic, (p, d, q) except Exception: pass return best_order, best_aicadfuller返回的第二个元素是p值用它判断是否平稳。很多人会把第一个统计量和p值弄混导致d选错。选好参数后model.forecast(steps3)预测未来三个月statsmodels会自动做差分还原不需要手动逆变换。训练完还要检查残差是否为白噪声。from statsmodels.stats.diagnostic import acorr_ljungbox resid model.resid lb_test acorr_ljungbox(resid, lags[6], return_dfTrue) print(lb_test)lags6是因为月度数据至少要看半年长度的自相关。如果p值小于0.05说明模型剩余自相关明显需要换SARIMA增加seasonal_order(1,1,1,12)。3.2 GRU建模滑窗构造与递归预测GRU比LSTM参数少月度数据量不大时训练更快。常见做法是把过去6个月作为窗口预测下一个月。由于历史数据只有30多个月网络不能太大单层32个隐层单元足够。import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import GRU, Dense from sklearn.preprocessing import MinMaxScaler def build_gru_data(series, window6): values series.values.reshape(-1, 1) scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(values).flatten() X, y [], [] for i in range(len(scaled) - window): X.append(scaled[i:iwindow]) y.append(scaled[iwindow]) X np.array(X).reshape(-1, window, 1) y np.array(y).reshape(-1, 1) return X, y, scaler X, y, scaler build_gru_data(monthly_series) model Sequential([ GRU(32, activationtanh, input_shape(X.shape[1], 1)), Dense(1) ]) model.compile(optimizeradam, lossmse) model.fit(X, y, epochs100, batch_size4, verbose0)MinMaxScaler把销量缩放到[0,1]避免GRU训练时梯度爆炸。注意这里样本量很少不要设置validation_split否则验证集会分不出来想验证模型就手动留最后6个点做预测。预测阶段用递归方式把最后6个月数据送入模型得到一个预测值再把这个值拼进窗口末尾丢掉最前面的值继续预测下一个月。递归预测的误差会累积所以GRU的结果通常只有前1到2个月可靠第三个月只能作为参考。3.3 ARIMA与GRU的适用边界维度ARIMAGRU数据量要求30个点即可起步建议至少50个点否则容易过拟合季节/趋势显式建模SARIMA可加季节项自动学习需要序列中反复出现可解释性有系数能看到滞后项影响黑箱难以解释单个预测多步预测直接输出未来3个月递归预测误差逐步累积适用场景月度序列、销售周期稳定促销、节假日等非线性波动大的场景在这个数据量下最终以ARIMA结果作为基线GRU做交叉验证。如果两个模型结果相差很大优先怀疑GRU窗口设置不合理而不是直接采信复杂模型。4. 由回归到决策定价与补货混合整数规划的建模第三问的目标是给出未来三个月的定价和销量预测让利润最大化。关键一步是把第二问得到的“价格-销量”关系与预测销量、损耗率合并成目标函数再用混合整数规划求解每个商品的定价档位。4.1 销量-价格回归用频数表拟合弹性系数“回归方程展示图.ipynb”和“50个单品对应线性结果.xlsx”里的回归方程形式很统一以价格档位中点为自变量x销量均值为因变量y做一元线性回归。得到的斜率就是该商品的销量-价格弹性。import statsmodels.api as sm fit_df pd.read_excel(50个单品对应线性结果.xlsx) y fit_df[销量均值] X sm.add_constant(fit_df[价格档中值]) ols_model sm.OLS(y, X).fit() print(ols_model.params) print(fR^2{ols_model.rsquared:.3f}, p_value{ols_model.pvalues[价格档中值]:.3f})回归结果中只有斜率p值小于0.05的商品才建议参与调价p值不显著说明价格和销量的关系在统计上不成立强行优化只会得到极端价格。R方不需要特别高生鲜数据里价格对销量的解释度本来就有限但“平均损耗率按类.xlsx”一定要在回归阶段就参与计算用销量乘以(1-损耗率)得到可销售量否则后面MILP算出的利润会明显高估。4.2 混合整数规划建模利润最大化与0-1定价档“第三问求解混合规划.py”的核心是混合整数规划。这里不是直接求解连续价格而是把每个单品的候选定价离散成几个档位用0-1变量选一个。目标函数是总毛利即收入减去进货成本再减去损耗成本。from mip import Model, xsum, maximize, BINARY # price_options: 每个商品的可选定价列表 # predicted_qty: 预测的月销量 # unit_cost: 进货成本 # loss_rate: 品类损耗率 m Model(定价与补货混合规划) x {} for i in item_list: for j, price in enumerate(price_options[i]): x[i, j] m.add_var(var_typeBINARY, namefx_{i}_{j}) # 每个商品恰好选择一个价格档 for i in item_list: m xsum(x[i, j] for j in range(len(price_options[i]))) 1 profit_expr 0 for i in item_list: for j, price in enumerate(price_options[i]): profit_from_i (price - unit_cost[i]) * (1 - loss_rate[i]) * predicted_qty[i] profit_expr profit_from_i * x[i, j] m.objective maximize(profit_expr) m.optimize(max_seconds120) print(objective value:, m.objective_value)目标函数中(price - unit_cost)是单件毛利(1 - loss_rate)是扣掉损耗后的可销售比例predicted_qty来自ARIMA或GRU的预测。如果商品换了新价格档predicted_qty还要乘一个由回归方程计算的弹性调整系数也就是将回归斜率和截距折算成“相对原价的变化率”再作用到预测销量上这样才能把第二问和第三问真正串成一个优化问题。第二问规划求解用的是同一个0-1变量框架只是只有一个决策周期第三问有了预测销量后把同样的模型扩展到连续三个月并允许不同月份选择不同价格档。4.3 补齐约束货架容量、价格上下限与服务率纯利润最大化会把所有商品都推高到最高价但超市要考虑消费者接受度、货架容量和品类完整性。常见做法是加三组约束价格范围约束price_options由代码预先筛好不在集合里的价格不予考虑。货架容量约束所有被选中商品的预测需求量总和不超过库容即给每个商品一个虚拟占用系数shelf_space[i]约束xsum(shelf_space[i] * predicted_qty[i] * x[i, j]) CAPACITY。最低单品满足度避免求解器为了利润最大直接压低某个长尾商品的价格可以约束至少80%的商品必须选择非最低价档。约束越多求解器越慢。实际可以把max_seconds设成120秒超时后接受当前最优整数解。CBC求解器在候选档位不多时几十秒内收敛数据量再大一点可以换成HiGHS接口在mip库中基本兼容。参数含义建议初值windowGRU滑窗长度6月max_p, max_qARIMA阶数上限3price_options单品候选价格档数量由回归方程取5到10档loss_rate品类平均损耗率直接读Excel不手工指定max_seconds求解器超时120秒5. 复现这些代码时最常见的四个坑这几年带学生复现类似建模代码重复出现的问题基本都集中在读取、差分还原和MILP的零解上。如果你的结果和一等奖论文对不上优先检查下面的细节。5.1 读Excel的中文列名与编码问题项目里的中间结果都是xlsx用pandas读没问题但如果把Excel另存为csv用read_csv读中文列名时需要注意编码Windows下通常是gbkLinux下是utf-8。更稳的做法是指定engineopenpyxl读xlsx避免底层xlrd版本不一致导致读不出sheet。5.2 ARIMA预测结果恒等于序列最后一个值这是典型误用当AIC选出的d过大比如d2序列被过度差分模型会预测未来值几乎等于当前值。解决办法是限制d的取值在0到1之间并先用差分后序列的ADF p值做判断p值小于0.05就不要再差分。5.3 GRU训练集混入未来数据很多复现者在构建滑动窗口时没有按时间切分训练集和测试集而是全量归一化后直接切窗口。严格的时间序列评估是先划分train和test再对train做fit_transform对test做transform。如果复现时为了简化跳过了这一步至少要让窗口内的历史数据全部早于预测点。手动构造窗口时如果误开了shuffleTrue会把未来数据随机混进训练验证集loss看起来下降很快真正预测时却全崩。5.4 MILP求解结果全为0求解器没有报错但objective为0最常见的原因是predicted_qty对应列缺失或者数据列在Excel里被存成了字符串缺失值与数值相乘后整个表达式变成NaN。排查时在构造目标函数之前打印几行中间数据print(item_list[:5]) for i in item_list[:3]: print(price_options[i], predicted_qty[i], unit_cost[i], loss_rate[i])把数据读取和模型求解拆成两个函数中间落一个debug文件每次只存三五行数据。另外还要看约束是否过强比如price_options列表为空时“每个商品恰好选一个价格档”直接导致模型无解。异常现象常见原因修复建议预测全平d阶数太高限制d 1GRU训练loss低但预测差未按时间切分或误开shuffle关闭shuffle按时间递归预测MILP目标为0数据列缺失或字符串类型分解打印中间结果回归斜率不显著用原始日流水做回归改用频数表均值6. 把预测-定价代码改造进生产库存系统的工程细节如果只是交一份竞赛论文上面的流程已经够了要平移到真实电商的补货系统还需要补几块内容。零售库存管理通常要求预测每天刷新一次补货优化每周跑一次。6.1 从notebook到定时任务先把ARIMA和GRU的notebook改造成两个脚本train_daily.py每天读最近12个月销量增量训练模型输出“未来7天销量预测表”optimize_weekly.py读预测结果、损耗率和供应商价格跑MILP输出建议补货量。生产环境常用Airflow定时触发结果写入数据库而不是Excel。Excel适合竞赛复盘不适合线上服务。6.2 预测与库存策略的接口设计补货模块拿到预测值后要计算目标库存水平目标库存 预测周期需求 安全库存。安全库存常用下面的近似公式计算SS z * sqrt(L * sigma_d^2 mu_d^2 * sigma_L^2)其中z是服务水平对应的常量95%服务水平取1.65L是补货前置期的平均周数sigma_d是周需求标准差mu_d是周平均需求sigma_L是前置期标准差。这里的模型参数不再是比赛里拍脑袋定的容量上限而是由服务水平自动算出的目标库存上限。sigma_d可以用历史12周实际销量减预测值后的残差标准差来估计。预测值由ARIMA或GRU给出残差再反馈给训练脚本做模型监控偏差连续超过20%就触发重新训练。每周跑完优化后还要把实际销售与预测值做对比偏差报表回传给计划员。这套代码在原生状态下跑起来并不优雅但把链路全部看完后再回头去改工程化每个模块都有明确的优化目标预测模块负责精度优化模块负责利润安全库存负责兜底。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门