
简介一份来自JDGOC仓储网络智能库存管理赛题的初赛原始数据包面向物流供应链、机器学习与运筹优化方向的参赛者、研究者及高年级学生。赛题以京东全国8个区域配送中心、近500个仓库的211时效配送为背景需要先完成区域销量预测再通过调拨决策控制RDC与FDC之间的库存平衡适合据此复现两阶段赛题流程或开展学术实验。资源共15个文件以10个csv数据表为核心涵盖销量、库存、补货、商品属性与促销等字段另含3个Python脚本及pyc与txt说明可作为建模仿真与提交参考流程压缩包整体约16.62MB。已有1151人学习浏览数据体量适中、变量覆盖较全。利用这份数据可直接支撑从需求特征提取、预测模型构建到调拨仿真验证的完整链路适合撰写论文或作为实战项目训练帮助读者深入理解供应链场景中的数据建模与优化方法。1. JDGOC初赛数据到底考什么不是预测是补货决策不少第一次拿到JDGOC初赛数据仓储网络智能库存管理赛题包的同学第一件事就是把销量预测模型调到最准然后在第一次提交里翻车预测精度升了分数反而跌了。原因不难解释——这道题要的不是“猜中明天卖多少”而是在一张跨仓库、多SKU、按天滚动的仓储网络上把补货量、补货时机和库存水位一起定下来。数据里同时埋了需求、损耗和在途三条信号任何一条漏掉都会在回测里加倍还给你。这篇笔记面向两类人想冲成绩的参赛者以及想拿这类数据练手库存优化的工程师。下文所有思路都按“拆数据、建特征、做决策、验回测”的顺序展开。2. 拆开JDGOC初赛数据字段映射、时间口径与基线分数有些选手习惯直接df.head()看两眼就开始调模型这是初赛最容易翻车的第一站。仓储网络智能库存管理的数据通常是“仓库 × SKU × 日”的面板但落到文件里可能是出库流水、库存快照、调拨记录混在一起需要先把它拼成一张干净的日粒度基表。2.1 字段映射与数据体检把中文列名换成通用命名拿到赛题包后我一般会先建一个字段映射把可能的中文名或带前缀的列名统一成通用命名。这样做的好处是后面的特征工程和模型代码不用反复改列名提交时也不会因为列名不一致报错。import pandas as pd # 字段名以官方赛题包为准这里用通用命名做一次映射 df pd.read_csv(initial_data.csv, parse_dates[date]) df df.rename(columns{ 仓库: warehouse_id, SKU: sku_id, 日期: date, 销量: demand, 库存: stock_qty, 在途: in_transit, 损耗: loss, }) print(df.dtypes) print(df.isna().mean()) print(df.groupby([warehouse_id, sku_id]).size().describe())这里有个细节demand、loss是流量型stock_qty、in_transit是存量型。流量型字段在聚合时用sum存量型字段用last混在一起都求平均是最常见的浪费数据方式。输出的df.isna().mean()可以快速定位哪些列缺失比例高如果某个仓库SKU组合的记录数远小于整体中位数说明这个面板的观测窗口短后面做滚动特征时要给它单独的窗口参数。再看缺数缺失值不要无脑fillna(0)。一条“当天没有出库记录”和“当天没有上传数据”是两回事前者置0合理后者置0会把周均线拉低。先看原始文件里有没有上传标记列没有的话按面板内“最近一次库存快照”回填库存比直接填0更贴近业务。提示节假日不营业导致的日期缺失不是缺数硬补0会让需求序列出现大量伪低谷。2.2 面板对齐与存量/流量分离按天聚合出基表如果原始数据是流水表同一仓库、同一SKU、同一天会有多行出库或入库记录。直接拿非聚合数据去算滚动均值会重复计算流量。我一般会先按“仓库-SKU-天”做一次分组聚合生成一张干净的日粒度基表。daily ( df.groupby([warehouse_id, sku_id, date], as_indexFalse) .agg( demand(demand, sum), loss(loss, sum), stock_qty(stock_qty, last), in_transit(in_transit, last), ) ) daily daily.sort_values([warehouse_id, sku_id, date]).reset_index(dropTrue) daily[panel_key] ( daily[warehouse_id].astype(str) _ daily[sku_id].astype(str) ) # 看一个panel内日期是否连续date_diff 大于1说明有缺口 daily[date_diff] daily.groupby(panel_key)[date].diff().dt.days print(daily[date_diff].value_counts(dropnaFalse))聚合时把demand和loss按天求和是因为一天内多笔出库应当累加stock_qty和in_transit取当天最后一次快照因为库存是时点值不能把上午的库存加下午的库存当作当天库存用。date_diff的价值在于暴露缺口如果大量组合的间隔是1天说明面板完整如果出现很多间隔为7天或30天说明数据里混着周报或月报粒度必须单独处理否则滚动窗口会跨过空档把窗口内天数算错。对齐基表之后建议顺手做一次重复键检查dup daily.duplicated(subset[warehouse_id, sku_id, date]).sum() print(dup)如果dup大于0说明前面的聚合粒度不对还有遗漏的维度没纳入 groupby。常见遗漏维度包括存储类型、批次、库区。聚合时要不要保留这些维度取决于补货决策是按SKU维度还是按批次维度做前者常见于需求预测后者常见于FEFO保质期管理。多花两分钟做这个检查能避免后面所有特征合并时出现行数爆炸。2.3 建naive基线提交链路先跑通特征工程和模型是后面的事第一步应该先做一个最笨的预测把提交链路跑通。没有这个提交基线后面加任何特征都看不到相对增益很容易陷入“加了特征分数没动不知道是代码问题还是特征无效”的状态。# 基线用每个仓库SKU最近7天销量均值预测未来14天 # 预测周期以赛题说明为准这里用14天演示 base_date daily[date].max() test_dates pd.date_range(startbase_date pd.Timedelta(days1), periods14) pred_rows [] for key, panel in daily.groupby(panel_key): recent panel.sort_values(date).tail(7) mean_demand recent[demand].mean() wh_id panel[warehouse_id].iloc[0] sku panel[sku_id].iloc[0] for d in test_dates: pred_rows.append({ warehouse_id: wh_id, sku_id: sku, date: d, pred_demand: round(mean_demand, 2), }) pred_df pd.DataFrame(pred_rows)这里tail(7)的意思是只看最近7天的销量均分给未来每一天。比赛评分通常只看提交表的数值所以预测值保留两位小数就够了不需要输出“是否缺货”之类的额外列。基线的精度大概率很低但它能验证三件事日期范围是否正确、提交表是否覆盖全部仓库SKU组合、官方评测能不能正常跑通。如果数据存在星期规律比如周末销量是平时两倍把“最近7天均值”改成“上周同一天”会更好。基线不是用来拿分的是用来给后面的所有改动做对照组后续每个特征版本的分数都要先和这个基线比比不过直接放弃。我习惯把pred_df存成带日期版本的文件名比如baseline_20250101.csv后面每个特征版本单独保存。比赛后期要回滚到某个参数组合时这种习惯能救你一命。3. 把需求、损耗、在途卷成一张特征表滚动窗口与跨仓特征很多做惯电商销售预测的人拿到仓储数据后只看销量序列这是最大盲区。仓储网络智能库存管理的补货量由三股力量决定需求、损耗、在途。只预测需求忽略损耗和在途补货决策会系统性偏离。3.1 三类信号与特征表的设计逻辑需求信号决定“要卖多少”损耗信号决定“还要额外赔掉多少”在途信号决定“已经订了多少、还没到货”。一个简单的例子某SKU日均卖10件补货前置期3天安全库存7天目标库存应该是100件左右但如果已经有40件在途库存里还有30件那这次只需要补30件而不是100件。如果不减在途库存会越补越高最后在回测里表现为库存积压分被扣。特征工程的目标就是把这三种信号的历史模式转成表格里的列。时间序列模型可以自己找滞后模式但对这类“仓库 × SKU × 日”的高基数面板数据树模型加手工特征往往更稳。有人会问直接用LSTM或GNN建模时序不行吗可以但在高基数面板上跑GNN的成本很高而且初赛阶段模型迭代要快。树模型加手工特征能在半天内完成一轮“特征-训练-回测”循环这对参赛者的价值比模型上限大得多。先把树模型跑通再考虑深度学习。3.2 滚动窗口特征shift(1) 防泄漏滚动窗口特征是最常用的时序特征但很多人踩过一个坑用了当天销量算均值去预测当天销量造成验证集分数虚高。所有特征里凡是涉及“今天已知信息”的都要先想清楚模型预测的到底是哪一天。def build_features(daily, lead_time3): features daily.copy() features features.sort_values([warehouse_id, sku_id, date]) # 预测T日需求时特征只能用T-1日及更早的信息滚动特征统一shift(1) for w in [7, 14, 28]: features[fdemand_ma{w}] ( features.groupby(panel_key)[demand] .transform(lambda s: s.rolling(w, min_periods1).mean().shift(1)) ) features[fdemand_std{w}] ( features.groupby(panel_key)[demand] .transform(lambda s: s.rolling(w, min_periods1).std().shift(1)) ) # 存量类特征同样只用过去值如果评测允许用当日快照自行改用当天值 for key in [stock_qty, in_transit, loss]: features[f{key}_lag7] ( features.groupby(panel_key)[key] .transform(lambda s: s.shift(7)) ) features[f{key}_ma7] ( features.groupby(panel_key)[key] .transform(lambda s: s.rolling(7, min_periods1).mean().shift(1)) ) return features参数的选择上窗口 w 取 7、14、28分别对应周、双周和月维度。日粒度数据里7天能捕捉周内规律如果数据粒度为周窗口就要改成 4、8、13。shift(1)是防泄漏的关键预测 T 日时特征只能用到 T-1 日及之前的信息T 日当天的需求自然也不能用因为评测不会把当天答案提前给你。min_periods1让面板最前面几行也有值避免因为窗口不足而把整行删掉但副作用是前几行的均值被早期不完整窗口拉偏。训练时建议把每个面板前28天单独标记为“冷启动”或者直接不参与训练因为冷启动期的特征分布和稳定期不一样。滚动窗口之外还要补三列基础日历特征星期几、是否工作日、距离最近节假日的天数。仓储数据里周一和周五的销量形态经常完全不同如果赛题数据跨了春节或双十一节假日前后需求会出现明显的脉冲。这类特征直接用日期列生成不影响面板结构加进去通常能提升2-3个百分点。这里补充一个选型理由为什么用rolling展开多窗口而不是直接shift(1)到shift(28)。滞后列很多时维度会迅速膨胀而且相邻滞后期之间的相关性极高树模型学起来既慢又容易过拟合。滚动均值相当于给滞后列做了一次降维把“最近若干天平均水平”留给模型做基准。如果需要捕捉趋势可以再加一列demand_ma7 - demand_ma28表示短期相对长期的变化方向。3.3 跨仓特征让仓库网络关系进入模型仓储网络的“网络”二字体现在同一SKU在不同仓库之间有互补或竞争关系。某仓库缺货时邻仓可能还有货可以调拨某仓库整体周转率极低说明库容被滞销品占住。这类信息不从单个SKU序列里来要从仓库级或网络级的聚合里来。wh_feats ( features.groupby([date, warehouse_id]) .agg( wh_demand(demand, sum), wh_stock(stock_qty, sum), wh_loss(loss, sum), ) .reset_index() ) features features.merge(wh_feats, on[date, warehouse_id], howleft) features[sku_share_of_wh_demand] ( features[demand] / features[wh_demand].clip(lower1e-6) ) features[wh_turnover] ( features[wh_demand] / features[wh_stock].clip(lower1e-6) )sku_share_of_wh_demand的意义是衡量这个SKU在仓库里算不算重点品占比高的SKU一旦缺货整个仓库的履约都会被拖累安全库存要相对调高。wh_turnover是仓库整体周转率数值高说明仓内商品动销快补货前置期可能需要预留更多余量数值低说明库存积压整个仓库的补货策略要偏保守。跨仓特征还可以继续做同一SKU在不同仓库的需求比值尤其适合判断“需求是否在往仓库间迁移”邻仓当日库存均值可以先算成表再做shift(1)避免用未来值。再往下可以给每个SKU按仓库计算最近30天需求变异系数然后分三档稳定品、波动品、长尾品。稳定品用统一安全库存波动品单独调大safety_days长尾品可以直接用“0补货加阈值触发”。分档操作能让后面的网格搜索空间小很多分数涨得也直接。这些特征不用一次全加第一版先加仓库总需求和周转率跑通后再逐个加。4. 建模路径与避坑清单先预测后补货别把两个模型焊死直接拿“补货量”当标签建模是这个方向最容易走进的死胡同。补货量不是客观事实而是策略输出同样一段需求历史目标库存设高一点补货就多设低一点补货就少。模型去拟合这种由规则生成的目标等于在拟合自己的策略噪声。4.1 为什么预测模型和补货策略必须分开正确做法是拆成两段先用模型预测未来需求再用一个确定性策略根据预测值计算补货量。拆开之后预测误差和策略参数可以单独调出问题也能定位是模型不准还是参数不对。很多参赛队试过直接用LSTM多步预测销量再接补货规则效果并不比树模型加规则好原因在于需求序列的分布有大量零值和小值深度学习对这种长尾很不友好。补货阶段的成本结构是不对称的缺货一件的损失往往远大于多存一件的持有成本。回归模型训练时把缺货和高估看成等权误差所以就算RMSE很低缺货率也可能很高。把损失不对称性放到补货策略里处理用安全库存天数去吸收预测残差比强行改模型的损失函数更容易控制和解释。4.2 LightGBM需求预测的最小配置树模型在异构特征、缺失值和高基数类别上比传统时序模型稳训练速度也够快。选LightGBM而不是XGBoost主要是因为它处理高基数面板数据时内存占用更低训练时间短方便做多组参数实验。先给一组能跑通的最小配置import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit feat_cols [c for c in features.columns if c not in {warehouse_id, sku_id, date, panel_key, demand, stock_qty, in_transit, loss, date_diff}] split_day features[date].max() - pd.Timedelta(days14) trn features[features[date] split_day].copy() val features[features[date] split_day].copy() model lgb.LGBMRegressor( n_estimators300, learning_rate0.05, num_leaves31, min_child_samples20, lambda_l10.1, lambda_l20.5, subsample0.8, colsample_bytree0.8, random_state71, ) model.fit( trn[feat_cols], trn[demand], eval_set[(val[feat_cols], val[demand])], callbacks[lgb.early_stopping(50), lgb.log_evaluation(50)], ) val[pred_demand] model.predict(val[feat_cols])random_state71固定下来是为了让每组实验可复现比赛提交结果和后面调参都要基于同一套随机种子。n_estimators300配合early_stopping(50)实际训练里树数常在100-200就停止没必要提前设很大的数字。lambda_l10.1、lambda_l20.5是本方向常见的偏好因为需求特征里有很多互相关的高斯型滚动均值正则适当给大一点能防过拟合但不要一上来就调先用默认配置跑通。分割必须按时间切不能K折随机切。用TimeSeriesSplit或手动按日期切都行随机切会把未来信息提前暴露给训练集造成验证分数虚高。对长尾SKU如果零销量占比超过70%可以单独建一个“是否断货”的分类器再做条件回归比硬用一个回归模型处理零膨胀数据更容易提升。预测值出来后不要直接丢进补货函数。需求不可能为负如果模型在低销量SKU上输出了负值先剪到0对于连续28天零需求的SKU可以直接把预测置0减少无意义补货对于需求极低的SKU树模型很难输出精确的小数可以考虑把回归目标改成“0/1断货分类 正数回归”的两阶段效果通常比硬调阈值好。4.3 补货量计算目标库存、在途与安全库存预测值出来后补货量用一句公式就能算目标库存减去现有库存减去在途。目标库存等于预测需求乘以前置期加安全天数。公式写成函数方便后面做网格搜索。def reorder_qty(pred_demand, stock_qty, in_transit, lead_time3, safety_days7, min_order0): target_stock pred_demand * (lead_time safety_days) qty target_stock - stock_qty - in_transit return max(min_order, qty)lead_time是补货下单到入库的天数这个数在赛题数据里通常能用“下单日期和入库日期的时间差”统计出来如果统计不出来就按1、2、4、7几个值做网格搜索。safety_days是安全库存覆盖天数它吸收预测误差和需求波动数值越大库存越高缺货越少但持有成本越高。min_order是起订量约束在没有约束时设0即可有最小起订量时返回前要做向上取整取整逻辑单独写在函数外不要污染核心公式。补货计算有个常见错误把损耗单独再减一次。损耗已经体现在历史库存下降和历史需求里预测需求如果已经包含损耗补货公式里就不需要额外减如果预测目标是纯销量损耗才需要另算。到底属于哪种取决于你当初怎么定义训练标签。4.4 避坑清单5条真实踩坑记录第一条滚动窗口泄漏。现象验证集分数极高线下测试几乎完美提交后大跌。原因特征计算时用了当天或未来的demand、stock_qty比如rolling(7).mean()没加shift(1)模型在做“用结果预测结果”。解决所有滚动和滞后特征统一shift(1)预测前强制把测试期日期右边截断再生成特征。第二条用RMSE当唯一指标。现象整体RMSE降了缺货率和库存积压没改善分数原地不动。原因RMSE对大销量SKU敏感会忽略大量小SKU的断货。解决增加加权训练或对demand取log1p后训练同时跑补货模拟直接看罚分。第三条把仓库ID、SKU ID直接喂给树模型。现象模型分数尚可但特征重要性里ID类排名很靠前无法解释。原因ID是编号不是数值树模型切分出的“仓库大于5”没有业务意义。解决用warehouse_id做目标编码或统计编码例如每个仓库的日均销量、每周波动率或者干脆删掉ID列让模型学统计特征。第四条在途字段理解错位。现象补货量持续偏高库存一路堆积。原因把“在途”和“计划采购”混为一谈或者在途已经是包括本次补货的累计值又被扣了一遍。解决先看一段时间每条记录的在途与库存变化确认它是“已下单未到货”还是“当天新下单量”如果是后者补货计算时不能直接减要自己另建“过去lead_time天内的下单量”作为在途近似。第五条补货模型和预测模型时间窗口错位。现象预测的是未来7天总需求补货却按日需求乘天计算数值差一个量级。解决定义清楚预测目标的时间跨度pred_demand如果是未来7天合计公式里就不能再乘lead_time safety_days要先除以7转成日需求再做乘法。5. 用成本罚分做库存闭环回测验证安全库存天数预测准只能算“猜得准”库存管理看的是长期运营成本。我习惯在做完模型和补货公式后把历史数据回放一遍跑一个简化的库存闭环每天先入库再出库和损耗最后盘库存并决定补不补。同一段验证历史对lead_time和safety_days做网格搜索看每组参数的缺货成本、持有成本和下单成本。罚分公式可以写得很简单重点是把三个分量放到同一单位# total_cost 缺货数量 * 缺货单价 平均库存 * 持有费率 * 期天数 下单次数 * 固定成本 total_cost stockout_qty * stockout_unit_cost \ avg_inventory * holding_rate * horizon_days \ order_cnt * fixed_order_cost做这个回测时最值得看的不是总成本最低的那一组而是总成本随safety_days变化的曲线曲线在某个点之前下降快、之后变平说明这个点就是预测能力的边界如果曲线一路下降不回头说明预测误差太大安全库存再怎么调都压不住缺货问题在模型而不是策略。我个人每轮迭代都会固定随机种子、把特征表版本和回测参数存成文件再跑下一版。这样每次调试翻车时都能知道是哪次改动引入的问题不用从头重猜。预测做准只是下限安全库存、前置期和补货节奏这些参数才决定成绩上限。希望这套流程能帮你在初赛阶段少走几步弯路直接把手里的数据用满。本文还有配套的精品资源点击获取