生鲜补货预测与库存优化:基于报童模型和分位数回归的完整实践

发布时间:2026/9/13 11:51:38
生鲜补货预测与库存优化:基于报童模型和分位数回归的完整实践 简介这是2023年全国大学生数学建模竞赛C题“销售预测与库存管理”的一等奖获奖配套资料包面向参加数模竞赛的高校学生、指导教师以及需要Python数据分析项目实战的开发者。压缩包共38个文件总大小7.93MB涵盖9个Jupyter Notebook分析脚本、3个Python程序、24个Excel数据表及1篇论文定稿PDF其中ipynb实现聚类分析、ARIMA与GRU销量预测、多品类回归建模等关键环节xlsx则存放单品汇总、损耗率、预测结果等中间数据结构清晰便于复盘。目前已有42人学习下载。资料不仅包含完整可运行的赛题代码和最终论文还附带项目详细说明能帮助读者理解问题拆解、数据预处理、模型对比与结果可视化的全流程既适合备赛训练也可作为毕业设计、课程设计或Python数据挖掘项目的参考蓝本。1. 2023年C题的真正难点预测的不是销量而是补货量2023年国赛C题拿到手很多人第一反应是“这不就是个销量预测吗”然后把时间序列模型跑一圈等交卷时才发现第二问的库存优化完全没接上。这道题明面上给的是蔬菜各单品的销售流水实际要交付的是未来一周每个单品的补货量和定价策略。换句话说预测只是中间产物最终考核的是你能不能把预测的不确定性转成可执行的单级库存决策。再有题目里的单品种类极多、历史数据却不长部分单品可能只有几周的销售记录冷启动问题比一般时序预测更突出。我见过不少队伍把精力全押在让MAE再低一点结果论文里的“库存模型”只是用均值预测乘了个安全系数直接被评委看穿。反观能拿一等奖的代码包普遍是同一套结构数据清洗与特征工程、分层预测、用报童模型把分位数预测转成订货量、最后用一份能复现的脚本把全过程串起来。这篇文章就按这条线把从原始流水表到最终补货单的完整链路讲一遍代码部分按能直接跑通的程度给出参数含义也会逐一说明。2. C题的数据先看什么从流水表到可训练样本2.1 原始数据的三个必要检查点拿到数据先别急着建模先做三个检查。第一确认销售流水的时间跨度2023年C题的数据范围大致是2023年7月到2024年6月但不同单品起售时间不一致这意味着有些序列天然就短不能一刀切地截取训练集。第二检查“销量为0”的日期是真实无销售还是缺货前者是正常波动后者会严重污染均值特征——常见做法是看同一天同分类下其他单品的销售情况来推断或者干脆单独做一个缺货标记位。第三检查价格字段C题里价格是会变动的而且部分单品存在“促销价”这个信息在后面做定价策略时非常关键。处理流水表的关键一步是把“宽表”转成“按单品-日期排列的长表”。一个容易忽略的细节是日期索引要补全即使在某个日期该单品没有销售记录也要用0填充否则后续构造滞后特征时索引会对不上。2.2 特征工程把“单品种时序”变成“面板回归”C题的预测目标是一个个单品各自的销量但单独给每个单品建模型非常不现实——历史太短模型学不到东西。多数国赛一等奖方案的做法是改为构造面板数据每一行是一个(单品, 日期)组合特征是分类属性、时间属性、滞后销量、滚动统计量、价格变化率标签是该单品当天的销量。这样所有单品共享同一个回归模型长序列的单品能给短序列的单品提供跨品类的学习信号这也是数据量不够时最有效的做法。滞后特征里最容易踩坑的是“泄漏”如果用t日的价格特征预测t日的销量看起来没问题但如果把t1日的信息混进t日特征测试集效果就会虚高。所以构造特征时全部只用历史窗口的数据代码里统一用shift()错开日期。2.3 代码实现构造基础训练集import pandas as pd import numpy as np df pd.read_csv(sales.csv, parse_dates[date]) # 补齐单品-日期全组合 all_singles df[single_id].unique() date_range pd.date_range(df[date].min(), df[date].max(), freqD) full_index pd.MultiIndex.from_product([all_singles, date_range], names[single_id, date]) df df.set_index([single_id, date]).reindex(full_index).reset_index() df[qty] df[qty].fillna(0) df df.sort_values([single_id, date]).reset_index(dropTrue) for lag in [1, 3, 7]: df[fqty_lag{lag}] df.groupby(single_id)[qty].shift(lag) df[qty_roll7_mean] df.groupby(single_id)[qty].transform( lambda x: x.shift(1).rolling(7).mean()) df[price_change] df.groupby(single_id)[price].pct_change() df[weekday] df[date].dt.weekday df[month] df[date].dt.month这段代码做三件事第一用reindex补全缺失日期保证每个单品每一天都有一行第二用shift把滞后特征和滚动均值错开一天防止用当天信息预测当天第三构造价格变化率和时间属性。注意reindex之后销量为0的行价格字段也需要向前填充或者用分类均值填充具体做法取决于原始数据里价格缺失的原因。对短序列单品滞后3天、7天的值会大量为NaN后面训练树模型时直接保留NaN即可LightGBM原生支持缺失值处理。3. 预测层Base-Pool-Meta三层结构而不是调一个Prophet3.1 为什么要按品类分层建模C题的销售模式有明显的品类差异叶菜类生命周期短、价格波动大根茎类相对稳定食用菌又有其独立的生长周期。把所有单品混在一个模型里模型虽然能共享信息但会吸收太多品类噪音。常见做法是分成三层——Base层对每个单品跑一个简单的统计基线比如上周同期均值作为baselinePool层所有单品的数据放一起训练LightGBM得到主模型Meta层对Base和Pool的结果做加权融合权重在验证集上搜索。这个结构的优势在短序列单品上尤其明显Base层因为历史太短几乎不可用但Pool层能从同一分类下其他单品学到销量分布规律。3.2 相似品信息迁移的做法跨品类共享信息是Pool模型的本能但同一分类下的相似单品仍然存在差异。拿不到相似单品的特征怎么办一个直接从推荐系统搬过来的思路用每个单品的销售向量算相似度矩阵把相似单品的近期销量均值作为新特征拼进去。销售向量可以取最近14天的日销量序列求余弦相似度取Top5相似品构造两个特征相似品近期销量均值和相似品价格均值。这个特征对冷启动单品特别有效——一个新单品没有自己的历史销量但有相似的老品数据作参照。3.3 LightGBM训练脚本与滚动测试集import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit feature_cols [c for c in df.columns if c not in [qty, date, single_id]] X df[feature_cols] y df[qty] tscv TimeSeriesSplit(n_splits3) for train_idx, val_idx in tscv.split(X): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model lgb.LGBMRegressor( objectivequantile, alpha0.7, n_estimators300, learning_rate0.05, num_leaves31, colsample_bytree0.8 ) model.fit(X_train, y_train, eval_set[(X_val, y_val)], callbacks[lgb.early_stopping(50)])这里的objectivequantile和alpha0.7是关键设置。回归模型默认预测条件均值但第四节的库存决策需要的是预测分布的分位数所以直接在这里训一个0.7分位数模型alpha值等于报童模型里的最优服务水平。TimeSeriesSplit保证训练集永远在验证集之前这是时序任务与普通交叉验证最大的区别——随机打散会把未来信息泄漏到训练集里导致评估结果虚高。训练完成后对同一份数据再训一个alpha0.5的模型作为中位数预测两个模型的输出在第四节的补货量计算里会配合使用。4. 决策层用报童模型把预测分布变成补货量4.1 报童模型参数与成本结构预测做完了接下来的问题是给定预测分布补多少货才最优。这里引入报童模型它的核心是权衡两个成本超量成本c_o订多了菜烂在仓库里和缺货成本c_u订少了丢失销售机会。最优订货量出现在超量的边际期望成本等于缺货的边际期望成本这一点解出来最优订货量就是销量分布的c_u/(c_uc_o)分位数。C题里超量成本通常用进货价近似缺货成本用售价减进货价近似。如果题目要求“满足市场需求的前提下使总成本最小”则缺货成本还要加一项商誉损失但这个是软性指标直接按售价减进货价再加一个惩罚系数最稳妥。4.2 分位数预测与最优订货量求解因为第3章的LightGBM已经训好了alpha分位数模型预测得到的就是q_alpha最优订货量直接等于这个值。举个例子进货价3元、售价5元那c_o3, c_u2最优服务水平2/(23)0.4也就是补货量取预测分布的0.4分位数。如果不加这个决策层直接用点预测0.5分位数补货算下来总成本会比最优高出不少这个差值可以写进论文的灵敏度分析里。4.3 代码实现补货量与定价联动# 假设已有预测结果 pred_df包含 single_id, date, q50, q70 # q50 是中位数预测q70 是服务水平 0.7 下的分位数预测 cogs df.groupby(single_id)[price].mean().to_dict() # 进货价 price_map df.groupby(single_id)[price].last().to_dict() order_qty [] for _, row in pred_df.iterrows(): sid row[single_id] price price_map[sid] cost cogs[sid] * 0.6 # 假设进货价是售价的60% cu price - cost co cost service_level cu / (cu co) alpha_col fq{int(service_level * 100)} # 如果没训对应分位数用线性插值近似 qty row[alpha_col] if alpha_col in pred_df.columns else row[q50] order_qty.append(np.ceil(qty)) pred_df[order_qty] order_qty注意这段代码里的线上插值逻辑很粗糙更严谨的做法是先对每个单品确定最优服务水平再针对该水平训练或预测对应分位数。所有数据集的alpha列可以用循环一次性生成但真要写进论文最好对服务水平画一条成本曲线说明为什么这个alpha是最优的这也是评委最爱看的部分。定价联动的逻辑在于如果预测发现某单品未来一周需求高于可供应量可以把售价往上调缺货成本随之上升服务水平上升补货量增加反过来需求疲软时降价促销超量成本相对变高模型自动给出更保守的补货量。5. 提升论文说服力的两个技巧CRPS评分与库存-成本前沿图5.1 用CRPS替代MAE做模型选择MAE只衡量点预测的误差但C题第二问要的是分位数预测质量。两个模型的MAE相同分位数可靠性可以差很多一个过分自信、一个过度保守后者在报童模型里的表现会更好。国赛评阅时评委更认可CRPSContinuous Ranked Probability Score它同时衡量预测分布和真实值之间的差距。def crps(obs, q50, q_lo, q_hi): # 简化版CRPS用分位数近似积分 score np.abs(obs - q50) if obs q_lo: score 0.5 * (q_lo - obs) elif obs q_hi: score 0.5 * (obs - q_hi) return score更规范的做法是用prophet自带的CRPS计算函数或者用scoringrules库但手工实现的这个版本已经能说明问题模型不但要预测得准还要把不确定性范围报出来。如果q_lo和q_hi构成的区间能覆盖住60%的真实销量说明模型校准良好。5.2 画一条缺货成本与库存成本的前沿曲线另一个答辩必杀技是画出“库存成本-缺货成本”的权衡曲线。做法很简单把服务水平从0.1以0.05步长递增到0.95对每一个服务水平计算对应的补货量再回测到验证集上计算该策略下的超量成本和缺货成本。两个成本相加画出一条U形曲线最低点对应的服务水平就是理论最优。levels np.arange(0.1, 0.95, 0.05) total_costs [] for lv in levels: order np.percentile(pred_df[q50], lv * 100) # 简化实际用分位数预测 over np.maximum(order - val_df[qty], 0).mean() * co under np.maximum(val_df[qty] - order, 0).mean() * cu total_costs.append(over under)这张图放到论文里能直观呈现“为什么选择0.65而不是0.5”的决策逻辑也是对第4章报童模型的实际验证。代码逻辑按服务水平循环求总成本再在结果中找最小值点整个过程不需要额外的库matplotlib一行画图即可。6. 提交前要做的三个检查代码、论文、数据文件的对应一等奖代码包最常见的问题是评委根本跑不起来。三个检查点第一代码里的随机种子是否固定LightGBM、数据打乱、KNN相似度计算都要设置seed不然同一份代码两次运行结果不同第二数据路径是否用了相对路径很多队伍在Windows上写死绝对路径评委换台电脑就崩直接给相对路径加个try判断最省事第三论文里的参数和代码里的默认参数是否一致比如论文写的learning_rate0.05代码里却是0.1会被理解成论文和实现脱节。关于代码规范我记得评阅规则里会有“可复现性”一项严格说评委不会真的逐行运行但会在论文中找关键数据和图表对应。所以补货量结果表、成本曲线图、特征重要性图三个输出最好存到CSV和PNG连同代码一起打包形成一个结果可追溯的完整链。最后把预测结果按题目要求的格式转成提交表列名和单位别弄混。这一步做完代码包就算真正能达到“给人就能复现”的一等奖标准了。本文还有配套的精品资源点击获取