简介仓储网络智能库存管理初赛数据包JDGOC聚焦真实赛题提供京东场景下多区域配送中心与仓库的初赛原始数据及配套代码面向物流供应链、机器学习与运筹优化方向的学习者和参赛者可用于研究销量预测、库存控制与调拨决策问题。压缩包共有十五个文件含十个CSV数据表库存、销量、促销、SKU属性、需求分布等辅以三个Python脚本、一个说明文本和一个缓存模块整体大小十六点六二MB目录结构清晰便于按模块复用。已有1152人浏览学习。数据包含初始库存、补货记录、分位数、商品预测等字段脚本覆盖从销量预测到RDC/FDC库存平衡调拨的完整建模流程同时附带提交样例与模块清单能帮助读者快速还原赛题环境、开展多轮建模仿真也适合作为论文实验数据或教学案例。1. JDGOC初赛数据一张库存快照背后要做的事拿到 JDGOC 初赛数据多数人第一反应是先跑一版出库量预测看误差能压到多低。但「仓储网络智能库存管理」这个题目的重点从来不是预测指标本身而是预测结果能不能变成每个仓库、每个 SKU 每天的补货量建议——多补一天是资金占用少补一天是缺货惩罚两条路的代价完全不对称。这篇笔记按我自己做这类赛题的顺序展开先把数据字段和口径盘清楚再做需求预测然后把预测接进补货策略最后用滚动回测把整条链路跑通并记录几个反复出现的坑。适合刚拿到初赛数据、想快速建立可提交基线的开发者也适合想把「预测 补货」串成闭环的从业者参考。2. 先把数据盘明白字段口径、时间连续性与仓库差异2.1 初赛数据的常见字段与业务含义仓储库存初赛数据大多以「仓库 × SKU × 日期」为粒度组织每天一行快照。常见字段和它们的业务含义如下字段业务含义建模时注意date库存快照日期先检查是否连续跨月跳天会直接影响窗口特征warehouse_id仓库编号不同仓的补货周期和需求规律差异很大sku_id商品编号注意新品和长期滞销品的区分opening_stock当日期初库存出现负数是数据口径问题要先排查inbound_quantity当日入库量不同仓含不含调拨入库口径可能不一致outbound_quantity当日出库量缺货时段出库量会失真不能直接当需求stockout_flag当日是否缺货决定要不要做需求还原是整个建模的关键开关如果数据里还有 lead_time、pipeline_stock、holding_cost、stockout_cost 这些字段恭喜这属于比较完整的赛题设计前置期和成本直接决定了补货策略的边界。先拿一小段数据确认字段类型和粒度别急着建模。import pandas as pd sample pd.DataFrame([ {date: 2024-01-01, warehouse_id: WH_A, sku_id: SKU001, opening_stock: 120, inbound_quantity: 300, outbound_quantity: 80, stockout_flag: 0}, {date: 2024-01-01, warehouse_id: WH_B, sku_id: SKU001, opening_stock: 0, inbound_quantity: 0, outbound_quantity: 0, stockout_flag: 1}, ]) print(sample)这段代码只是为了确认字段分布。注意 WH_B 这行库存为 0、出库为 0、缺货标记为 1说明「出库量为 0」并不代表「没有需求」而是「有需求但发不出货」。后面所有特征工程和样本权重都要围绕这个认知展开。2.2 数据质量检查空值、负库存与时间断层初赛数据通常是脚本导出的很少有完全干净的版本。我一般先跑一轮质量检查把可见的问题记下来而不是直接进模型否则后面所有分析都会建立在一个黑匣子上。import pandas as pd df pd.read_csv(inventory_data.csv, parse_dates[date]) print(df.isna().mean()) print(负期初库存条数:, (df[opening_stock] 0).sum()) key [warehouse_id, sku_id, date] dup_mask df.duplicated(subsetkey, keepFalse) print(重复键条数:, dup_mask.sum()) print(df[dup_mask].sort_values(key).head(10))这段代码做了三件事看空值占比、看负库存数量、看主键是否重复。空值集中在成本字段还可以接受集中在出库量或库存字段就必须处理负库存通常表示期初欠货不是数据错误重复键则说明同一天有多条流水需要用 groupby 聚合或者找数据提供方确认。再看时间连续性。库存管理最怕快照断天如果某仓某 SKU 连续缺失一周窗口特征会算出错误的高波动。g df.groupby([warehouse_id, sku_id])[date] diff_days g.diff().dt.days gap_mask (diff_days 1) diff_days.notna() print(有日期断层的记录数:, gap_mask.sum())日期断层可能是节假日不发货也可能是缺货期间没有快照。要在特征里显式加一个「距上次记录间隔」的列让模型自己学这个信号而不是默默用插值填平。2.3 出库量与缺货标记先还原真实需求再动手这是仓储库存赛题里最容易踩坑的一步。缺货时段出库量为 0但需求可能很大直接拿出库量做预测目标模型学到的是一条被截断的分布预测结果系统性偏小。我的做法是用历史同期中位数做近似还原。import numpy as np df[true_demand_est] df[outbound_quantity].copy() mask df[stockout_flag] 1 median_ref df.groupby([warehouse_id, sku_id])[outbound_quantity].transform( lambda s: s.rolling(28, min_periods1, centerTrue).median() ) df.loc[mask, true_demand_est] np.maximum( df.loc[mask, outbound_quantity], median_ref[mask] )逻辑是对缺货日取该仓库该 SKU 前后 28 天的出库中位数作为需求下界与真实出库量取较大值。这样至少不会把缺货日当成零需求日。需要提醒的是这只是近似还原不代表真实未来需求初赛评估如果按实际需求算这个估计值只能帮模型学分布不能直接当答案。提示缺货天数占整体比例超过两三成时需求还原的影响会非常大建议把缺货样本单独统计出来做一份分布对比再决定训练样本权重。3. 需求预测从滑动窗口特征到 LightGBM 基线3.1 为什么先做预测而不是直接调库存参数很多人拿到这类赛题会直接写补货函数琢磨安全库存系数。但补货策略的每个参数都依赖需求分布订货点要由「前置期内需求均值 安全库存」决定安全库存要由需求标准差决定。需求本身不稳定时直接调参等于盲调。所以我的固定顺序是先做需求预测再把预测结果转成补货决策。预测目标建议用第 2 章的 true_demand_est而不是原始出库量。预测步长则看赛题要求常见是未来 7 天或 14 天。步长决定特征窗口长度预测 7 天滑动窗口至少取 28 天才有足够信息预测 14 天还要额外构造趋势特征否则后半段预测会退化到用均值。另一个容易忽略的点是同一份数据里不同仓库和 SKU 的需求模式差异巨大。有的 SKU 是平稳日用品有的带明显周效应有的是断断续续的长尾品。统一建一个模型可以快速出基线但分仓分 SKU 建模往往能再压几个点误差。3.2 特征工程代码滑动窗口、星期几与滞后量仓储需求预测的常用特征就三类历史出库的滑动统计、日历特征、时滞特征。先把它们构造出来。import pandas as pd import numpy as np def build_features(df): g df.sort_values(date).groupby([warehouse_id, sku_id], group_keysFalse) for w in [7, 14, 28]: df[fout_qty_mean_{w}] g[true_demand_est].transform( lambda s: s.shift(1).rolling(w, min_periods1).mean() ) df[fout_qty_std_{w}] g[true_demand_est].transform( lambda s: s.shift(1).rolling(w, min_periods1).std().fillna(0) ) df[dayofweek] df[date].dt.dayofweek df[is_weekend] (df[dayofweek] 5).astype(int) df[lag_1] g[true_demand_est].shift(1) df[lag_7] g[true_demand_est].shift(7) return df这段代码的关键是 shift(1)当前预测日的特征只能使用前一天及更早的信息否则验证指标会虚高。min_periods1 是为了处理序列前 28 天窗口不足的情况早期特征会偏小但不能直接丢弃否则损失太多训练样本。星期几特征对周效应明显的 SKU 帮助很大。lag_1 和 lag_7 捕捉短期相关性和周同期对比。如果数据跨度长还可以加月份或季度哑变量但要防止过拟合。3.3 超参数与验证切分避免预测吃进未来信息特征构造完接下来是训练和验证。这里最忌讳随机切分时间序列必须按时间切前段训练、后段验证。from lightgbm import LGBMRegressor train df[df[date] split_date] valid df[(df[date] split_date) (df[date] valid_end)] features [c for c in df.columns if c.startswith(out_qty_) or c in [dayofweek, is_weekend, lag_1, lag_7]] model LGBMRegressor( n_estimators600, learning_rate0.05, num_leaves31, min_child_samples20, random_state42, verbose-1, ) model.fit(train[features].fillna(0), train[true_demand_est]) valid[pred] model.predict(valid[features].fillna(0))参数上num_leaves31 对中等规模数据是安全的起点太大很容易在长尾 SKU 上过拟合min_child_samples20 防止小样本节点学出极端值。n_estimators 配合 learning_rate0.05 属于常规组合如果训练集很大可以提高到 1000 棵树但一定要配合早停否则验证集误差会先降后升。注意这里 split_date 的选择要留足时间窗。如果预测目标是未来 7 天验证集至少留 14 天才能看到完整的需求波动而不只是某个片段。4. 把预测接进补货策略安全库存、订货点与回测评估4.1 常见补货策略 (R, S) 与 (s, Q) 怎么选需求预测本身不是最终交付物补货建议才是。常用的两种策略(R, S) 策略每隔 R 天盘点一次补到目标库存 S。适合日粒度快照数据因为初赛数据本身就是按天记录的天然匹配每日盘点的假设。(s, Q) 策略库存降到订货点 s 时补固定数量 Q。适合订单驱动、连续性更强的场景但在数据只有每日快照时很难精确判断触发点。我一般选 (R, S)设 R1 天也就是每天根据预测结果给一次补货建议。这样实现简单也方便做回测模拟今天下的订单lead_time 天后到货。选型理由还有一层初赛评分通常不关心你用的是哪种策略只看最终库存成本、缺货率这类指标。策略结构越简单越容易定位是预测的锅还是参数的问题。4.2 补货计算代码与三个必调参数核心逻辑是目标库存 前置期内预测需求的均值 安全库存 盘点周期内需求补货量 目标库存 - 现有库存 - 在途库存。from scipy.stats import norm def build_plan(df, lead_time3, review_period1, target_service0.95): z norm.ppf(target_service) plan [] for (wh, sku), g in df.groupby([warehouse_id, sku_id]): mean_d g[pred_demand].mean() std_d g[pred_demand].std() horizon lead_time review_period safety_stock z * std_d * np.sqrt(horizon) order_point mean_d * horizon safety_stock target_s order_point mean_d * review_period on_hand g[opening_stock].iloc[-1] pipeline g[pipeline_stock].iloc[-1] order_qty max(0, target_s - on_hand - pipeline) plan.append({warehouse_id: wh, sku_id: sku, order_qty: int(order_qty), safety_stock: round(safety_stock, 2), order_point: round(order_point, 2)}) return pd.DataFrame(plan)代码逻辑分四步按仓库和 SKU 分组用预测日均值和标准差计算安全库存由安全库存推导订货点和目标库存扣掉在手库存和在途库存得到补货量。注意如果 opening_stock 为负order_qty 会自动变大去覆盖欠货这是预期行为不要强行截断。三个必调参数参数含义调低的影响调高的影响lead_time补货前置期更激进库存压力小但容易缺货更保守缺货少但库存积压review_period盘点周期补货更频繁成本上升补货更粗缺货风险上升target_service目标服务水平安全库存下降总成本可能更低安全库存上升缺货成本下降初赛如果给了 holding_cost 和 stockout_cost最好的做法是把成本比换算成服务水平而不是凭感觉定 0.95。缺货成本是持有成本的 3 倍时最优服务水平大约在 0.9 附近5 倍以上才值得考虑 0.95。4.3 用滚动回测验证整条链路而不是只看预测误差预测误差好看不能代表补货方案好因为缺货惩罚和库存持有成本是不对称的。我的习惯是写一个简单的库存演化模拟器把预测接进去跑一遍。def simulate(inventory, demands, arrivals, holding_cost, stockout_cost): total_holding 0.0 total_stockout 0.0 for t in range(len(demands)): inventory arrivals[t] shortage max(0, demands[t] - inventory) inventory max(0, inventory - demands[t]) total_holding inventory * holding_cost total_stockout shortage * stockout_cost return total_holding total_stockoutarrivals 代表补货订单在 lead_time 天后到货的量。回测时要用「当时预测得到的补货量」生成 arrival 序列不能用事后真实值去规划否则会严重高估方案表现。这一步模拟的是完整决策链路预测 → 补货 → 到货 → 满足需求 → 计成本。注意如果初赛只要求预测未来需求不要求给补货量那么滚动回测的核心价值是帮你判断「预测结果能不能支撑补货决策」——这个判断直接在方案里写清楚比只报一个 WAPE 更有说服力。5. 仓储网络智能库存管理的常见问题与避坑记录这一章是我每次做库存类赛题都会重点检查的地方。说句血泪经验初赛翻车十次有八次不是模型不够强而是数据口径和特征泄漏在捣乱。5.1 预测泄漏特征里混进未来值训练误差好看、初赛翻车现象验证集上指标非常漂亮滚动回测却一塌糊涂补货量明显滞后于实际需求。原因构造滑动窗口时忘记 shift(1)把当天出库量同时用进了特征和标签。模型等于「抄答案」训练误差当然低一旦预测未来就原形毕露。还有一种更隐蔽的泄漏用整个时间段的均值做标准化把未来信息揉进了历史数字里。解决统一规则——任何特征只要能追溯到当天及未来的数据一律禁止进模型。写代码时先 shift 再 rolling并且单独把特征构造和标签构造拆成两个函数方便复查。5.2 出库量为 0 不等于没有需求缺货期需求被系统性低估现象模型预测的低需求 SKU 全部集中在曾经缺货的仓库但回测里这些 SKU 的缺货率依然很高。原因直接用 outbound_quantity 做训练目标缺货日的 0 被当成真实需求模型学到的均值偏小补货量自然不足。解决用第 2 章的近似还原方法把缺货日的目标值替换成同期中位数同时在特征里加一个「过去 7 天缺货天数」的计数让模型知道这个 SKU 最近的供应状态不稳定。这两步一起做缺货 SKU 的预测量会明显抬升。5.3 仓库口径不一致入库量里藏了多少跨仓调拨现象同一个 SKU 在 A 仓的入库均值是 B 仓的三倍但两者的出库量差不多。补货策略在 A 仓囤了大量库存。原因有的仓库入库量包含跨仓调拨单有的只统计采购入库。训练时没有区分模型以为 A 仓需求更高或者把调拨当成稳定的补货来源。解决用库存恒等式做校验——期初库存 入库量 - 出库量 应该等于次日期初库存差值大的仓库样本单独打印出来看口径。df[expected_closing] df[opening_stock] df[inbound_quantity] - df[outbound_quantity] next_opening df.sort_values(date).groupby([warehouse_id, sku_id])[opening_stock].shift(-1) df[gap] df[expected_closing] - next_opening print(df[df[gap].abs() 10].groupby(warehouse_id)[gap].mean())如果 gap 集中在少数仓说明这些仓的数据里混进了其他类型流水。稳妥的做法是把这些仓拆出来单独建模或者丢弃掉不可靠的入库特征只保留出库和行为特征。5.4 新品 SKU 没有历史数据特征矩阵全空怎么办现象某些 SKU 在数据集中只出现几次滑动窗口特征全是 NaNLightGBM 填 0 后把它们全部预测成低需求补货量为 0然后一直缺货。原因长尾新品没有足够的窗口计算均值fillna(0) 让模型把它们当成滞销品。解决用同类 SKU 的仓库均值做填充同时加一列 is_new 标记让模型知道该样本置信度低。具体做法是计算同仓库所有 SKU 在同时间窗口的出库均值填充到新品特征里并把 is_new 设为 1。模型会为新品的预测值学习一个收缩系数而不是直接输出全仓均值。自检项快速判断方法特征是否泄漏检查所有窗口特征是否先 shift(1)目标是否失真缺货日占比是否偏高出库量均值是否异常低仓库口径是否一致用库存恒等式计算 gap按仓库分组查看新品是否被忽略查看 is_new 样本的预测均值是否低于同类老品6. 进阶技巧用分仓分 SKU 的 z 值搜索减少缺货成本6.1 为什么统一安全系数会翻车前面补货代码里用了一个固定的 z 值这是初赛基线最安全的写法但也是成本黑洞。高波动 SKU 需要更高的安全库存来防缺货低波动 SKU 用同样的 z 值会白白积压库存。我见过有人把 0.95 服务水平套在所有 SKU 上结果是波动大的长尾品照样缺货稳销品库存却高得离谱。正确做法是按仓库和 SKU 的波动率分组分别搜索 z 值。6.2 一段可复用的 z 值搜索脚本常见做法是先用预测序列算每个 SKU 的需求标准差再按标准差分桶每个桶内搜索 z。for bucket in [1, 2, 3]: subset skus[skus[std_bucket] bucket] best_cost float(inf) best_z 1.65 for z in [1.28, 1.65, 1.96, 2.33]: cost run_backtest(subset, z) if cost best_cost: best_cost cost best_z z print(bucket, bucket, best_z, best_z)分三桶是常见做法低波动、中波动、高波动。每个桶内的 z 值搜索范围从 1.28 到 2.33对应服务水平约 0.9 到 0.99。搜索的目标是总成本不是缺货率别把缺货率压到 0那是用库存成本换来的。最后说一个我自己的教训第一次做这类赛题时我把所有 SKU 的安全系数统一设成 1.96想着「缺货越少越好」结果高波动仓的库存周转率被扣得惨不忍睹。后来改成按波动率分桶搜索 z 值同样数据下总成本降了将近两成。这个赛季我会先把回测脚本写死再决定要不要上更复杂的模型——补货参数的收益往往比换模型大。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?