
简介JDGOC初赛数据仓储网络智能库存管理是一份面向物流供应链、机器学习与运筹优化学习者的赛题原始数据包围绕“销量预测库存调拨”双阶段问题展开可用于复现京东多级仓配网络的智能库存决策流程。完整数据覆盖京东区域配送中心与前置仓的仓配网络便于理解真实问题规模。压缩包共15个文件含10个CSV数据文件覆盖销量、促销、SKU属性、初始库存、补货及需求分布等、3个Python脚本含仿真模拟与提交示例、1个编译缓存及1个说明文本整包约16.62MB。已有1152人浏览学习。基于该数据读者可先利用历史销售与促销数据构建销量预测模型再针对RDC与FDC之间的库存平衡设计调拨策略预测与调拨结果均可直接对接库存计划适合作为物流管理、供应链优化方向的论文实证或课程项目。1. 仓储网络智能库存管理初赛数据一套数据两条技术线做机器学习的人第一眼看到这份JDGOC初赛数据容易把注意力全放在销量预测上觉得这又是典型的时序回归任务。但真正把inventory_replenishment.csv、simulation.py、submission.py放在一起看之后你会发现这套资源比表面复杂不少——它的完整链路是「预测优化仿真」三段式先用店铺维度的历史销量、促销和商品属性做需求量预测再把预测结果喂给补货策略由策略决定每个仓库补多少货最后用仿真器跑库存成本。也就是说机器学习只是前半程后半程运筹优化才是决定排名的分水岭。这套数据适合两类人想学销量预测实操的和想练库存调拨决策的。一口气跑通两条线对理解真实供应链系统的读写接口很有帮助。2. 数据包拆解从文件清单还原赛题流程2.1 销量预测阶段要喂给模型的数据解压之后先不急着读代码按文件名把数据分分类。销量预测阶段的输入我习惯分成三组历史销量、促销信息、SKU静态属性。sku_sales.csv是核心训练表记录每个SKU在每个仓库、每一天的实际销量。注意它不是纯订单表而是已经按sku_id warehouse_id date聚合好的日粒度数据字段里通常还有售价和会员价之类的价格信息。做特征时注意日期是有gap的有些日期可能缺行缺的不是销量为零而是当天没有记录。读数据的时候要按完整日期索引去重填充不然模型会漏掉一部分「零需求」样本。sku_prom.csv和sku_prom_testing_2018Jan.csv是促销计划初赛数据里给的促销标记比较粗糙一般只有「是否促销」「促销名称」「折扣力度」这几类字段。但注意训练集里的促销字段是真实发生的促销而Testing那个文件是预测期内的计划促销也就是说未来到底哪些SKU会做活动是已知的这是提升预测精度的关键信息。sku_attr.csv和sku_info.csv是商品静态信息比如品类、品牌、价格带、体积重量。sku_quantile.csv有点特殊它存的是每个SKU在某日的销量分位数预测结果如果用它可以做鲁棒优化但初赛阶段很多选手直接把它当监事信号用——我的建议是你可以把它作为对照特征但别直接拿来做预测输出因为它本身就是别人模型的产物直接叠加会让最终结果失真。2.2 库存优化阶段成本、初始库存与补货决策库存阶段的文件是initial_inventory.csv、sku_cost.csv、inventory_replenishment.csv。initial_inventory.csv是每个仓库在初始时刻的库存量字段包含RDC与FDC的映射关系RDC是区域配送中心FDC是前置仓两者在成本结构上有明显差异——RDC的存储成本低FDC离客户近但租金高所以优化目标不会单纯是「多备货」而是「把货放在对的层级」。sku_cost.csv描述每个SKU的采购成本、仓储成本、缺货成本这是优化算法要优化的系数。仓储成本通常按库存持有天数算缺货成本则是「如果客户下单但仓库没货」的惩罚项。这两项互相矛盾备货多则仓储成本高备货少则缺货风险大模型的权重就是在两者之间找平衡点。inventory_replenishment.csv则是往期每天实际补货量记录可以作为策略模仿的学习对象。2.3 评测与提交submission.py、simulation.py 谁先跑我第一次看simulation.py的时候以为它是仿真环境直接跑了官方示例发现它只负责「读提交文件 → 演一遍库存流 → 输出总成本」。submission.py才是选手自己要完善的核心代码它从预测模块拿到未来需求量再根据当前库存决定「从RDC调多少货到FDC」以及「RDC向供应商订多少货」。提交文件格式在sample_submission里。它要求输出一个长表每一行是sku_id warehouse_id date quantity order_type其中order_type要区分是补货单还是调拨单。仿真器只能认这个格式字段名、类型、编码都不能错。常见做法是保持sample_submission的表头把自己的结果填进去不要随意改列名。值得注意forecast_data_0523、inventory_data_jdata_0524这两个文件是赛期不同轮次的数据快照它们的列结构完全一致只是日期窗口不同可以理解成「训练轮次数据」和「测试轮次数据」。跑离线回测时我会用0523的数据做策略参数搜索用0524做最接近实时情况的验证这样不容易在正式提交时因为日期字段错位翻车。3. 销量预测从特征工程到LightGBM训练脚本3.1 需求分布特征与量化边界销量预测这一步做得粗一些也能跑通整体流程但它直接决定了补货策略的质量。预测误差大后面调拨策略再好也白搭。我一般不只预测点估计还会额外预测一个「高需求量分位」——也就是用分位数回归输出sale_p90因为补货决策真正关心的是「安全库存应该备到哪个水平」。sku_quantile.csv里已经给了参考分位数但那是比赛方的基准模型算出来的。自己训练时我建议在LightGBM中设置objectivequantilealpha0.9这样能直接得到90%分位数预测。为什么不用objectiveregression再加一个常数系数因为销量分布明显右偏大量SKU的日常销量集中在低位偶尔出现销量尖峰固定倍数会要么过备货要么欠备货分位数回归可以让模型在不同SKU上学到不同的备货冗余量。3.2 训练代码按店铺细粒度做回归预测下面给一个可以直接落到代码的LightGBM基准脚本用pandas读取文件并合并特征。注意我在这里做了两层聚合SKU粒度特征品类促销力度和仓库粒度特征仓储周转平均水平。合到一起之后模型才能知道「同一个SKU在不同仓库卖得好不好」。import pandas as pd import numpy as np import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit # 读取销量、促销、属性三张表 sales pd.read_csv(sku_sales.csv, parse_dates[date]) prom pd.read_csv(sku_prom.csv, parse_dates[date]) attr pd.read_csv(sku_attr.csv) # 合并促销和属性信息 df sales.merge(prom, on[sku_id, warehouse_id, date], howleft) df df.merge(attr, onsku_id, howleft) # 构造时间特征 df[weekday] df[date].dt.weekday df[day_of_month] df[date].dt.day df[month] df[date].dt.month # 用同一天的前N天销量做滑窗特征 df df.sort_values([sku_id, warehouse_id, date]) df[sales_lag1] df.groupby([sku_id, warehouse_id])[sales].shift(1) df[sales_lag7] df.groupby([sku_id, warehouse_id])[sales].shift(7) df[sales_mean_7d] df.groupby([sku_id, warehouse_id])[sales].transform( lambda x: x.rolling(7, min_periods1).mean() ) # 删除缺少标签的行 train df.dropna(subset[sales]) feature_cols [weekday, day_of_month, month, sales_lag1, sales_lag7, sales_mean_7d, price, promotion_flag, category_level] X train[feature_cols] y train[sales] # 时间序列交叉验证 tscv TimeSeriesSplit(n_splits3) for fold, (tr_idx, va_idx) in enumerate(tscv.split(X)): X_tr, X_va X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va y.iloc[tr_idx], y.iloc[va_idx] model lgb.LGBMRegressor( objectivequantile, alpha0.9, n_estimators500, learning_rate0.05, num_leaves63, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit( X_tr, y_tr, eval_set[(X_va, y_va)], eval_metricquantile, callbacks[lgb.early_stopping(50)] ) pred model.predict(X_va) print(ffold {fold}: pred_mean{pred.mean():.2f})逻辑解释sales_lag1和sales_lag7是两种不同尺度的滞后项sales_mean_7d是近一周移动均值比直接用原始销量做特征抗噪声能力更强。promotion_flag来自促销表如果同一个SKU在测试期有促销这个特征会把预测均值拉高让补货策略多备一些货。参数说明alpha0.9表示训练90%分位数模型而不是均值预测TimeSeriesSplit的n_splits3用在日粒度的数据上相当于最后三段时间段做验证比随机K折更符合供应链场景的时间相关性。early_stopping(50)在验证集分位数损失连续50轮不下降时终止防止过拟合。3.3 分位数预测和点预测实际怎么配合拿到90%分位数预测后不要直接用分位数预测去做仿真那会让补货量整体偏高。我的做法是把点预测送入submission.py的补货主逻辑90%分位数只用来计算安全库存的缓冲项。这样既保证正常情况下的库存周转又能覆盖促销季的需求峰值。有一个常见的误区是看到sku_quantile.csv就直接把它当成答案提交结果仿真成本非常高因为那些数据是基于历史分布产生的并不包含对未来促销的适应。你应该把它当成一个「预训练模型输出」用来检查自己的分位数预测是否合理而不是替代它。同伴操作时还会遇到一个问题有些高频SKU销量几百几千有些长尾SKU一周只卖一两件。原始销量做滞后特征之后长尾SKU的sales_lag1几乎都是0模型很难区分「没卖出去」和「真正没货」。我的解决方式是加一个is_zero_sales的0/1特征让模型先学「有没有货」再学「卖多少」相当于把预测拆成了两步。4. 从预测到库存补货调拨决策与仿真链路4.1 为什么第二阶段要落在调拨而不是直接订货预测做完接下来是补货策略。很多初次上手的人直接写一个「库存低于阈值就订购固定量」的策略完全忽略RDC与FDC的关系结果仿真器给出的成本远高于基准。原因在于仿真器区分补货与调拨两类操作补货是从外部供应商进货到RDC有比较长的提前期调拨是从RDC调到FDC提前期很短但单次运输成本更高。如果基层仓缺货就直接从外部补既不现实也便宜不了多少。这里涉及的目标函数是「总成本 采购成本 仓储成本 缺货成本 运输成本」。调拨决策实际上是一个流量控制问题每个FDC根据预测需求量申请调拨量RDC汇总所有FDC的需求再决定向供应商订多少。这样做的好处是RDC可以减少安全库存冗余由多个FDC共享同一个缓冲池。4.2 一个可运行的ABC补货阈值脚本下面是补货决策的核心逻辑。当预测值大于当前库存加上在途库存时计算缺口量然后按ABC分类决定是否一次调到位的上限。这里我用了一个简单门槛策略适合作为初赛阶段的强基线比直接用全量预测值下单要稳定。import pandas as pd # 读取初始库存和单位成本 inv pd.read_csv(initial_inventory.csv) cost pd.read_csv(sku_cost.csv) # 读取上一步的分位数预测 forecast pd.read_csv(my_forecast.csv, parse_dates[date]) # 合并每个SKU的库存和成本 inv inv.merge(cost, onsku_id, howleft) # ABC分类按库存成本排序前20%为A类、中间30%为B类、其余为C类 inv[cost_rank] inv[unit_storage_cost].rank(pctTrue) inv[abc_class] pd.cut( inv[cost_rank], bins[0, 0.2, 0.5, 1.0], labels[A, B, C] ) # 安全库存系数A类商品多备一些防止断货 safety_factor {A: 1.3, B: 1.2, C: 1.1} # 生成补货计划 records [] for _, row in inv.iterrows(): sku row[sku_id] wh row[warehouse_id] current_stock row[initial_qty] on_order row[in_transit_qty] if in_transit_qty in inv.columns else 0 daily_demand forecast.loc[ (forecast[sku_id] sku) (forecast[warehouse_id] wh), sales_quantile90 ].mean() if pd.isna(daily_demand): daily_demand 0 required_stock daily_demand * 14 * safety_factor[row[abc_class]] gap required_stock - current_stock - on_order if gap 0: records.append({ sku_id: sku, warehouse_id: wh, date: forecast[date].max(), quantity: gap, order_type: 1 }) submission pd.DataFrame(records) submission.to_csv(submission.csv, indexFalse)注意这段代码里required_stock daily_demand * 14 * safety_factor的含义14是补货覆盖天数也就是一次订足未来两周的需求这个数值要根据仿真器的提前期来调safety_factor是安全系数A类高成本高缺货成本的SKU用1.3倍缓冲C类用1.1倍。如果你的forecast[date].max()跨多天你用预测均值乘天数这里均值针对的是未来14天逻辑是自洽的。订单类型order_type1表示调拨单0是供应商补货单具体枚举值要以赛题给出的sample_submission为准。如果你拿不准直接抄sample_submission表格里的类型标注不要自己发明。4.3 与仿真的接口提交文件格式与字段映射仿真器运行命令一般是python simulation.py submission.csv但不同轮的仿真器可能读取对应时间窗的预测数据因此提交前要检查三件事第一提交文件的日期范围是否覆盖仿真器要求的全部预测天数缺失日期会被当成「当天不补货」可能造成缺货惩罚第二sku_id与warehouse_id是否在原数据中存在仿真器对非法ID会直接跳过而且不报错只在最终成本中出现异常缺口第三quantity只能是整数浮点数会被取整到过低的补货量。我自己会在提交前加一段数据校验脚本把所有提交行按sku_id warehouse_id date做唯一性检查因为仿真器对同一SKU同一仓库同一日期出现两行不同操作会做顺序覆盖先行后行的执行顺序直接改变结果。校验脚本输出非法行数量确保为零再提交。# 数据校验重复行、非法ID、负值检查 check pd.read_csv(submission.csv) dup check.duplicated(subset[sku_id, warehouse_id, date]) assert not dup.any(), f存在重复行共{dup.sum()}条 assert (check[quantity] 0).all(), 提交中存在负数补货量字段别名也是常见的隐性坑。sample_submission里面的列名可能叫order_qty而不是quantity也可能叫dc_id而不是warehouse_id。直接复制文件再多轮调整是最稳妥的方案避免因为改名或列顺序错位被仿真器拒读。5. 常见问题排查我踩过的四个坑5.1 日期字段解析错误导致预测偏移现象训练时模型表现正常提交后预测覆盖日期比实际需求日期少一天整体补货量全部滞后。 原因pd.read_csv(..., parse_dates[date])在个别文件的日期列上解析出来的格式不一致有些行变成了datetime有些仍是字符串比较时无法匹配。 解决统一用pd.to_datetime(df[date], format%Y-%m-%d)强制执行格式并且检查df[date].dtype是否为datetime64[ns]。从那以后我每次解析完都会assert一下列类型避免后面出错。5.2 分位数特征使用不当导致策略整体偏向过度补货现象用sku_quantile.csv作为预测值代入库存策略后仿真总成本比基准高30%以上。 原因分位数预测本身就是偏高的需求量它已经是一个带缓冲的订货量再套一层安全库存系数相当于双重冗余把所有SKU的库存成本都推高了。 解决把分位数文件只用在校验预测模型稳定性上正式生成补货单时使用模型自身的90%分位数预测而不是官方提供的分位数输出。如果你想用官方分位数做备货那安全库存系数就要降到1.0甚至0.95留出可用库存空间。5.3 仿真器跑挂不是代码问题而是提交数据里有非法仓库ID现象运行simulation.py没有报错但输出成本异常出现巨大缺货损失。 原因我在合并库存表时使用了无匹配的warehouse_id比如某仓库在initial_inventory.csv里存在但在预报数据文件里已经有不同的仓库编号规则。仿真器找不到对应的库存记录就按「库存0」处理于是大量缺货。 解决对提交文件中的每一行做left join检查确认提交中的sku_id warehouse_id都能在初始库存表中找到。具体做法是submission.merge(inv[[sku_id, warehouse_id]], on..., howleft, indicatorTrue)再检查_merge列是否全为both。5.4 编码和CSV转义问题数据量大时最隐蔽的坑现象sku_prom.csv中部分促销名称包含半角逗号直接pd.read_csv读出来列数错乱某些行偏移到下一列。 原因CSV的字段转义符不一致默认逗号分隔时带英文逗号的文本如果不加引号保护解析器会拆成多个列。 解决逐步累加pd.read_csv(..., escapechar\\)或pd.read_csv(..., enginepython)。但更稳妥的做法是先检查行内字段数量把不满足列数的行单独过滤出来人工看。还有一点部分文件的编码是GBK或GB18030直接用utf-8读会抛UnicodeDecodeError用encodinggb18030或errorsignore都能处理但后者容易吞字符所以在特征阶段就要留意字符串列的完整性。6. 用一个离线回测脚本验证你的策略有没有白调调参再勤不落到仿真器上验证等于白调。离线回测是我提交前的最后一道关用forecast_data_0523这一轮的数据做策略丢进simulation.py跑一轮成本再和基准提交对比。如果成本降幅低于2%那这次的策略调整就不值得提交。回测脚本里我固定了随机种子并且把所有特征标准化流程封装成一个函数保证「离线实验」和「正式提交」的代码路径完全一致避免出现训练环境和评测环境数据口径不一致的翻车。具体做法是这样的def run_backtest(pred_file: str, submit_file: str) - float: 跑一次完整回测返回仿真总成本越小越好 forecast pd.read_csv(pred_file) strategy build_strategy(forecast) strategy.to_csv(submit_file, indexFalse) cost run_simulation(submit_file) return cost # 先跑一次基准策略 cost_baseline run_backtest(forecast_baseline.csv, submit_baseline.csv) # 再跑一次加入促销因子之后的策略 cost_promo run_backtest(forecast_promo.csv, submit_promo.csv) print(fbaseline{cost_baseline:.2f}, promo{cost_promo:.2f}) if cost_promo cost_baseline: print(促销因子有效可以提交) else: print(促销因子无效保留原策略)参数说明run_backtest的关键在于传入的pred_file必须覆盖仿真器要求的完整日期区间不存在「只预测了一半时间窗口」还能通过校验的情况。跑完回测还要看成本拆解仿真器输出的成本明细通常包含缺货成本、仓储成本和调拨运输成本三块例如调拨运输成本过高说明策略在频繁地从RDC往FDC调货仓储成本过高则是安全库存系数设得太大。一个值得记住的习惯是每次调参只动一个变量批量对比时记录在一个train_log.csv里最后回看时能直接定位是哪个参数让成本跳升。我从那以后每个模拟项目都强制走一遍“先跑基准、再改单因子、最后看仿真明细”的流程把玄学成分降到最低。希望帮到你。本文还有配套的精品资源点击获取