☰
天池菜鸟需求预测与分仓规划实战:两阶段建模与源码复现
2026/10/23 20:54:15 网站建设 项目流程

简介:需求预测是供应链管理中的核心环节,其目标并非单纯追求预测精度,而是为后续的库存优化与资源配置提供决策依据。在实际业务中,预测结果往往需要结合分仓规划,综合考虑仓库容量、运输成本和缺货惩罚等约束,才能形成可执行的备货方案。特征工程在预测模型中起着决定性作用,通过历史统计、时间周期、商品属性和交叉特征,可以有效提升模型对复杂销售规律的捕捉能力。以LightGBM为代表的树模型凭借其对离散特征和短时序数据的良好适应性,成为此类场景的常见选择,同时可结合分位数预测映射安全库存,并通过整数规划或启发式算法求解最优分仓方案。这一套从预测到规划的组合方法,不仅适用于天池菜鸟竞赛,也可迁移至电商仓配、新零售补货等工业场景。本文基于第二赛季的完整实战经验,系统拆解赛题理解、特征构建、模型训练、分仓优化和工程实现的关键细节,为供应链算法实践提供一套可复现的参考路径。 天池菜鸟需求预测与分仓规划第二赛季,我从拿题到整理完整跑通的源码,前后花了接近三周时间。这个项目给我的第一感觉是:它不只是一次销量预测比赛,更是一个“预测+分仓”的双任务组合题。核心要解决的是菜鸟仓网里货该备多少、备在哪个仓库、什么时候到的问题,最终提交的输出通常分成两块:每个仓库每个SKU未来N个周期的需求量预测,以及基于预测结果做出来的分仓备货/调拨方案。

源码和说明我已经整理成一套可以直接复跑的工程结构,下面按赛题理解、特征工程、预测建模、分仓规划、工程实现、踩坑复盘六块来讲。如果你是第一次打供应链类比赛,这篇可以帮你少走很多弯路;如果你已经跑通基线,更建议直接看第3章和第4章,里面有不少提分细节是后面反复调参才试出来的。

1. 赛题不只是一次销量预测,而是“预测+分仓”的组合决策

这一节先不急着上代码,而是把题读透。需求预测类比赛很多人一上来就开特征工程,结果做了两周才发现评分函数里还藏着分仓成本,前面的模型结构全得推倒重来,非常浪费时间。

1.1 从订单数据到仓网决策,解题思路先从评分规则倒推

菜鸟仓网的历史订单数据通常是“订单明细+商品主数据+仓库信息”的组合形态。订单明细里有下单时间、SKU编号、仓库编号、数量;商品主数据会有类目、价格、重量等属性;仓库信息则可能包含仓容量、所在城市。第二赛季给出的数据,明显在分仓规划环节加重了权重,也就是说只把需求量预测准还不够,还要把预测出的量合理分配到不同仓库,让整体履约更稳、成本更低。

我的习惯是先别急着建模,先把评分规则拆出来。大多数这类比赛的评分函数不是简单的MAE或RMSE,而是带业务约束的复合指标。比如预测部分按一定精度函数计算误差,分仓部分再按库存满足率、缺货率、库存周转或运输成本去扣分。只有把评分函数拆解成“预测误差项+规划损失项”,才知道该把优化重心放在哪里。

我当时做了一张评分函数拆解表,把每个可能影响分数的地方对应到具体的输出字段。后面所有特征和模型设计,都围绕这张表来做校验。这不只是参赛技巧,实际上是供应链项目落地的常规做法——业务问题不先定义清楚,算法做得再花哨也白搭。

1.2 两阶段方案的优势,以及为什么没有直接做端到端

整个解题路线我最终定为“两阶段”:第一阶段用机器学习模型预测需求,第二阶段把预测结果丢进一个带约束的优化模型做分仓规划。可能有人会问,为什么不训练一个端到端模型,输入历史订单,直接输出分仓方案?

答案很直接:端到端模型的决策过程不透明,很难处理仓库容量、SKU属性、运输成本这类硬约束,而且一旦预测改了一个单位,整个分配结果都要重算。两阶段方案的好处是每一环节都可以单独验证:预测效果好不好看验证集的误差,规划结果合不合理看每一仓的库存是否超容、缺货有没有被压低。

我见过不少同学在第一阶段疯狂调参,到了第二阶段只写一个“就近分配”的规则,结果分数掉得莫名其妙。实际上分仓规划才是这个赛题拉开差距的地方。即使预测误差不完全理想,一个能感知运距成本和仓容限制的规划模型,也能把实际得分救回来不少。这个判断在我后续实验中得到了验证:纯预测模型分数提升有限,但优化模块每加一类约束,排名都有肉眼可见的上涨。

2. 特征工程是上限,先把四类特征打扎实

预测类比赛里流传一句话:“特征决定上限,模型决定下限。”这句话对天池菜鸟这个赛题尤其适用。数据里存在大量周期性、季节性、事件性的信号,如果特征里没有,树模型再强也学不出这些规则。

2.1 原始数据长什么样,先看三张表

我拿到数据后的第一件事,是花一个多小时做数据体检而不是直接跑代码。当时数据大概包含三个维度的表:订单明细表(订单号、SKU、仓库、日期、下单件数)、商品信息表(SKU、类目、价格带、上架时间)、仓库信息表(仓库编号、容量、城市)。有的赛季可能还额外给到促销日历,这属于非常关键的辅助信号。

这三张表要关联起来,才能构造出有业务含义的特征。单独看订单表只能做时间序列统计,连上商品表才能区分爆款和新品,连上仓库表才能知道某个SKU在哪些仓有历史覆盖。关联过程要注意键的唯一性,我曾经因为order表和sku表关联后出现了行数膨胀,导致后续所有特征全部算错,最后花了一整天才排查出来。这里有个经验:每做一步join,都输出一下行数做 sanity check。

2.2 历史统计特征、时间特征、商品属性、交叉特征

特征总体可以分成四族。第一族是历史统计特征:对于每个SKU+仓库组合,计算过去7天、14天、30天、60天的销量总和、均值、中位数、最大值、最近一天的销量。为什么要用中位数而不是纯均值?因为促销或节假日会把均值拉得很高,如果直接用均值做滞后窗口,会导致大促之后几天的预测一直虚高。用中位数或截尾均值会更稳。

第二族是时间特征:月份、星期几、是否月初/月末、是否节假日前后几天、第几周。这类特征对捕捉月度结算压力、周末需求差异很有用。第三族是商品属性特征:类目、价格区间、上架天数、是否新品。价格敏感的SKU在促销窗口内销量会暴涨,这类特征能帮助模型区分“这个SKU天生就是高频爆款”还是“它只是这段时间被促销刺激了”。

第四族是交叉特征,这也是容易被忽略但提分明显的部分。比如SKU类目×仓库城市、星期几×假日标记、最近3周同SKU均值×是否大促。交叉特征的目的不是增加计算量,而是让树模型能直接找到组合条件下的局部规律,比如“华东仓的日化类SKU在周末销量显著上升”这类模式。

2.3 长尾SKU和缺失值,别用 fillna(0) 掩盖问题

这个赛题里SKU分布一定是长尾的,头部少数爆款贡献了大部分销量,尾部大量SKU只有零星记录。刚开始我把缺失销量直接填0,结果发现低活跃SKU的预测几乎全被压成接近0,最终提交后在长尾部分的误差极大,排名直接掉了一档。

后来换了一种处理方式:对于某些SKU在某个仓库完全无历史的情况,不填0,而是用同类目同仓库的平均销量作为基础值填充,同时保留一个 was_missing 标记特征。这样模型既知道“这个组合缺数据”,也知道“大概的量级应该在哪”。这个改动对最终分数的影响非常明显。

缺失值处理不能盲目,要搞清楚为什么缺失:是仓库从未铺过货,还是该SKU铺过但正好那段时间滞销?如果是前者,可以考虑用同仓同品类均值;如果是后者,更适合用时序插值或上期值回填。把缺失原因区分清楚,特征才有业务含义。

2.4 特征筛选,快速留下有效特征

特征造完可能会有几十上百列,我一般不会直接全量丢进模型,而是先用LightGBM跑一个快速版本,用 feature_importance 排序。只保留贡献前80%的特征,并把相关性过高(比如0.95以上)的窗口特征适当删掉一部分。特征太多容易过拟合,尤其在这个数据“SKU多但单条时序短”的场景里,动不动就掉进短序列噪声里。

筛选之后,特征数量控制在30到50个之间即可。后面所有实验都基于这套特征版本跑,避免每调一次参数就重新造一遍特征。同时我会把特征生成逻辑固定成一个函数,后面复现的时候可以直接调用,这也是工程化能给比赛带来的最大好处之一。

3. 预测模型:LightGBM为主、时序模型为辅的加权融合

模型选择上,我最终用了LightGBM作为主力模型。这不是因为它最先进,而是在这个赛题的数据形态下,它是最“性价比”的选择。树模型处理类目特征方便,训练速度快,还能在目标函数里塞自定义权重,适配供应链场景对误差方向的要求。

3.1 为什么选LightGBM,而不是一味堆深度学习

有些同学看到历史订单数据就想着上LSTM、Transformer,但我建议先冷静评估一下数据量。这个赛题是典型的“多实体、短时序”:SKU仓库组合很多,但每个组合的连续历史可能只有几十条。这种条件下,深度学习模型很容易过拟合,而且调参周期非常长。LightGBM天然能融合大量离散特征和统计特征,还能靠正则化把短序列的噪声压住,是这类场景里更稳的选择。

我用的训练目标是最小化绝对误差(MAE),而不是默认的MSE。原因在于销售数据里天然有大量异常峰值,如果让模型去拟合极值,会把大部分正常周期也带偏。MAE对离群点更鲁棒,最终提交的分数也比MSE版本要好。

LightGBM的核心参数我调了三轮:第一轮把 learning_rate 设成0.05,num_leaves 从31调到63,其他默认,先跑通一个基线;第二轮开始压缩 bagging_fraction 和 feature_fraction 到0.8左右,增加随机性,防止单棵树的过拟合;第三轮用 early_stopping 固定在验证集上迭代次数,避免多跑浪费算力。

3.2 多步预测与时间序列验证的正确姿势

预测未来N个周期,很多新手会直接用一个滚动模型一步一动地预测,但在这个赛题里,我采用的是直接多步预测(direct multi-step)策略。也就是说,要预测未来第1天、第2天、第7天,就分别训练三个模型,每个模型以不同的滞后目标作为标签。这样做的原因很简单:递归预测会把前面的误差带到后面的预测里,导致越远的周期越不准;而直接多步预测每个周期都独立学习自己的规律,误差不会累积。

验证方式同样关键。最忌用随机KFold,因为销售数据有强时序性,随机划分会把未来数据混进训练集,造成信息泄漏,验证集上看起来分数极高,一提交就被打回原形。我用的是一套自定义的时间序列滑窗验证:训练集取当前窗口之前的数据,验证集取紧接其后的一段数据,然后逐步向前滑动。

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, valid_idx in tscv.split(X): x_tr, x_va = X.iloc[train_idx], X.iloc[valid_idx] y_tr, y_va = y.iloc[train_idx], y.iloc[valid_idx] # 每个fold都重新训练一次模型

这个滑窗除了验证模型,还可以用来验证提交文件的稳定性。如果每个fold的误差都差不多,说明模型没有偶发过拟合;如果某一折误差特别大,往往意味着那段时期有促销或节假日,需要在特征里继续补充事件信号。

3.3 自定义损失:对缺货和积压设置不同惩罚

这是整个项目最值得提分的地方。供应链场景里,缺货和积压的成本完全不对等:缺货意味着客户没买到,可能直接丢订单;积压只是库存压着,资金占用高但伤害没那么直接。所以我在LightGBM里给每个样本设置了不同的权重:如果实际销量大于预测值(相当于缺货),就给更高权重;如果预测值大于实际销量(相当于积压),权重适当降低。

具体做法是构造一个权重列,按真实值和预测值的相对大小动态调整,在每次训练时作为 sample_weight 传入。不是说所有SKU都用同一套惩罚比例,而是根据SKU的毛利率、是否爆款来精细化调整。爆款SKU缺货代价最高,长尾SKU则以控制积压为主。

import lightgbm as lgb weight = np.where(y_true > y_pred, 1.5, 0.8) # 示意逻辑 train_data = lgb.Dataset(x_train, y_train, weight=weight)

这个自定义损失在验证集上的提升不一定很大,但在最终业务评分上表现非常明显。天池这类比赛很多时候比的就是谁更贴合业务,而不是谁的模型MAE最低。

3.4 融合权重按验证损失反比计算

单靠LightGBM还不够,因为树模型对周期性、节假日这类长周期信号捕捉较弱。我另外训练了一个Prophet模型,专门用来预测SKU和仓库聚合到类目层级的总量趋势,然后把类目趋势按历史份额摊回每个SKU。这样既保留了树模型的强特征学习能力,又补上了时间序列模型对周期和节假日的敏感度。

融合不是简单平均。我计算了每个模型在验证集上的MAE,再按损失倒数归一化得到权重。比如LGBM验证MAE是12,Prophet是16,那LGBM的融合权重就是1/12除以(1/12+1/16),大约0.57,Prophet是0.43。每次调整特征后都重算一次权重,确保融合比例跟随模型表现变化。

4. 分仓规划:把预测结果变成可执行备货方案

进入这个环节,比赛的味道完全不同了。前面预测模型解决的是“备多少”,分仓规划解决的是“放在哪、运多少”。这也是第二赛季最能拉开名次的模块,因为大部分人的规划方案就是简单按仓库历史销量占比拆一下,完全没考虑成本、容量这些约束。

4.1 分仓规划的目标,不只是把货塞进仓库

分仓规划本质上是最小化“运输成本+库存持有成本+缺货惩罚+超容风险”的组合优化问题。每个SKU在不同仓库的单位运输成本不同,每个仓库有容量上限,每个SKU还可能面临最小起发量约束。如果只按就近仓存储,很可能热门仓秒满,冷门仓大量空置,反而导致总成本上升。

所以我做分仓规划的第一步,是建一个成本矩阵。矩阵的行是SKU候选仓,列是目标需求仓,每个单元格是从候选仓调拨到目标仓的单位成本。实际比赛中不一定给你完整运费表,但可以根据仓库城市间距离折算一个相对成本,或者用历史订单里“非本仓发货”的比例作为隐式成本估计。

如果数据里直接给了仓库维度的库存成本、缺货成本,那就更好,可以直接量化目标函数。如果没有,就用默认的对称运输成本加上惩罚项,先跑通一版,再根据验证集反馈去校准系数。

4.2 预测分位数到安全库存的映射技巧

这一步是我在复盘中重点想分享的。预测模型输出的一般是一个期望值,但供应链备货不能只按期望值做,否则会有一半的时间缺货。更稳妥的方式是输出预测分位数,比如P50、P80、P90。P90意味着有90%的概率需求不会超过这个量,适合用于高缺货成本的商品;P50适合低毛利且仓储成本高的商品,避免库存积压。

我用LightGBM的quantile objective分别训练了P50和P90两个版本,然后按SKU的属性决定用哪个分位结果做分仓。热点SKU和单价高的商品用P90,长尾和仓储成本偏高的商品用P50。这个思路让最终提交的备货量不那么“裸奔”,缺货率下降得很快。

分位数映射不要在代码里写死,而是要放到一个配置表里,按SKU类目和仓库属性灵活调整。我甚至尝试过根据每周的预测残差动态调整分位,当上周残差普遍为正时就整体上调,效果也不错。

4.3 用PuLP做最小成本整数规划

分仓规划的求解我用了PuLP,一个轻量级的线性规划库。问题建模方式如下:变量 x[sku][wh] 表示SKU在仓库wh的备货数量,目标是最小化总调拨成本和缺货损失,约束包括每个仓库的容量限制、每个SKU的最小起发量、以及预测需求量的满足比例。

import pulp as pl prob = pl.LpProblem("allocation", pl.LpMinimize) x = {} for sku in sku_list: for wh in warehouse_list: x[(sku, wh)] = pl.LpVariable(f"x_{sku}_{wh}", lowBound=0, cat="Integer") # 目标:运输成本 + 缺货惩罚 cost_terms = [] for (sku, wh) in x: cost_terms.append(unit_trans_cost[(sku, wh)] * x[(sku, wh)]) prob += pl.lpSum(cost_terms) + shortage_penalty # 约束:仓库容量 for wh in warehouse_list: prob += pl.lpSum(x[(sku, wh)] for sku in sku_list) <= capacity[wh] # 约束:满足预测需求的比例不低于某阈值 for sku in sku_list: prob += pl.lpSum(x[(sku, wh)] for wh in warehouse_list) >= min_ratio * forecast[sku] prob.solve()

这个模型为什么比简单规则好?因为它在目标函数里显式地权衡了“多放热门仓”和“控制总成本”之间的矛盾。规则法往往顾此失彼,要么容量溢出,要么缺货惩罚极高。PuLP求解完后,我会把每个仓库的分配量落成提交表,并做一轮容量校验,看看有没有仓库被塞爆。

4.4 求解受限时的启发式兜底

整数规划虽然严谨,但SKU数量和仓库数量一多,求解时间可能比较长,尤其还要在比赛时间内反复跑交叉验证。我实现的兜底方案是贪心算法:按“单位成本增量”给每个SKU的入库方案排序,优先把订单分配到边际成本最低的仓库,直到仓库容量耗尽。

for sku in sku_list: sorted_wh = sorted(warehouse_list, key=lambda w: unit_cost[(sku, w)]) remaining_demand = demand[sku] for wh in sorted_wh: can_put = min(capacity[wh] - used_capacity[wh], remaining_demand) x[(sku, wh)] += can_put used_capacity[wh] += can_put remaining_demand -= can_put if remaining_demand <= 0: break

这个贪心解不算最优,但速度极快,适合用来快速验证特征和预测模块的变化。在最终提交前,我会用PuLP把关键SKU组合算一遍精确解,长尾SKU则留在贪心结果里。开发和提交兼顾,才是实战节奏。

5. 源码结构、依赖与一键复现流程

整理源码和文档这件事,很多参赛者容易忽视,但恰恰是“高分项目”能否被复现的关键。我的工程结构不算复杂,但每一层都尽量保证清晰可追溯。

5.1 工程目录设计

project/ ├── data/ │ ├── raw/ # 原始订单、商品、仓库数据 │ └── processed/ # 清洗和特征工程后的中间表 ├── features/ │ ├── build_features.py # 特征生成主脚本 │ └── feature_config.py # 特征开关和参数配置 ├── models/ │ ├── train_forecast.py # LGBM/Prophet训练与保存 │ ├── infer_forecast.py # 预测推理脚本 │ └── weight_ensemble.py # 模型融合权重计算 ├── optimization/ │ ├── allocate_pulp.py # 整数规划求解 │ └── allocate_greedy.py # 贪心兜底求解 ├── evaluation/ │ └── evaluate_submit.py # 自定义评分函数 ├── output/ │ ├── prediction.csv # 需求预测结果 │ └── submission.csv # 最终提交文件 └── README.md # 复现说明文档

这样拆分的目的是把“数据处理”、“预测”、“优化”、“评估”四个环节解耦。任何一个环节调整,都不需要改动其他模块。特别是比赛期间经常要回退到某个历史版本,清晰的目录结构能省下大量debug时间。

5.2 运行脚本顺序与依赖安装

README里我写了详细的运行顺序。依赖方面只需要 pandas、numpy、scikit-learn、lightgbm、prophet、pulp 这几个常见库,没有特殊环境要求。安装直接用 pip 就行:

pip install pandas numpy scikit-learn lightgbm prophet pulp

执行顺序建议为:先跑特征脚本,再训练模型,接着做推理与融合,最后进入规划求解和评分。每一步都输出校验信息,包括行数变化、缺失值比例、验证损失、分配总件数和仓库容量使用率。有一次我不小心在特征阶段把日期列读成了字符串,训练出来的模型分数几乎等于随机,就是靠这些校验信息才追到了问题源头。

5.3 自建评测函数,模拟比赛打分

自建评测函数极其重要。即使比赛没有公开完整评分公式,也要根据已有信息构造一个近似评估方式。我写了一个评估脚本,传入预测文件和真实标签,计算MAE、缺货率、超容比例,以及一个综合的业务分数。每次提交前先在本地跑一遍,确保没有明显的低级错误,比如仓库编码对不上、SKU数量不匹配、容量超限等。

自评分数和线上分数之间会有差异,但只要趋势一致,就能用来做方案对比。我最常用的方法是“同一天同时提交两版”,用A/B对照的方式观察不同策略在线上分数的变化,然后决定保留哪个逻辑。这比闷头调参数高效许多。

6. 踩坑记录与高分技巧汇总

最后一部分,我把这次比赛里真实踩过的坑和最后验证能稳定提分的技巧整理出来,按“高频问题”和“高分经验”两个维度列了几个速查表,方便你做方案时快速对照。

6.1 信息泄漏:最隐蔽的掉分点

第一版代码我犯了一个典型错误:在构造滞后特征时用了“当前周期的销量”去预测“当前周期的销量”,等于把答案直接喂给了模型。当时验证集分数漂亮得吓人,结果一提交线上,分数惨不忍睹。排查后才发现,滚动窗口的特征函数没有 shift(1),导致特征和标签之间产生了同期相关性。

修复方式很简单,所有滞后窗口在计算时都先按SKU和仓库分组做 shift(1),确保历史特征严格落后于预测目标。不要嫌这个操作麻烦,它直接决定了训练和线上的一致性。另一个容易泄漏的地方是用目标编码时把未来统计量混进去了,这种情况一旦出现,验证分数会异常高,必须保持警惕。

6.2 冷启动SKU与促销干扰

冷启动商品是长尾赛季最头疼的问题。一个刚上架的商品没有历史销量,滞后特征全是0,模型预测也会跟着给0。我的处理方法是提取同品类、同价格带、同仓区的相似SKU历史均值作为冷启动特征。不需要精确匹配,只要类目和价格段一致,就能提供比0强得多的先验值。

促销数据同样会干扰模型。如果不单独构造促销标记,模型会把大促当天的高销量当作常态,导致促销后几天的预测仍在高位。我把大促日期离散成一个“促销前第几天、促销当天、促销后第几天”的特征组,训练时模型就能学到“大促后销量断崖式回落”这个规律,实际预测效果稳定很多。

6.3 参数调优节奏与高分细节速查表

调参节奏上,我的经验是“先宽后窄”。先用默认参数跑通全流程,再逐步调学习率、树深度、特征采样比例。每一轮只调一两个参数,同时跑全套验证,别一次同时改十个超参,否则出了问题根本不知道是哪一步引起的。

下面整理一张我在实际操作中反复对照的速查表,不一定适用所有赛季,但思路是通用的。

优化点建议做法收益逻辑
预测误差先真实模拟线上验证,再做特征筛选避免特征过拟合和时序泄漏
缺货/积压不对等自定义样本权重,缺货权重更高贴合业务评分,减少关键SKU缺货
分仓成本按成本矩阵做整数规划,而非纯就近规则直接降低运输和库存成本项分数
冷启动SKU用同品类同价格带均值填充避免新SKU预测整体为0
促销干扰单独加促销前后窗口特征防止大促后预测虚高
长尾SKU用分位数预测并适当下调备货减少低价值商品库存积压

还有一个关于最后提交的细节:提交前一定要做一次全量校验,检查仓库编码、SKU数量、字段名是否完全匹配。每次线上分数大跳水,一半原因是数据对齐问题,而不是模型效果变差。把校验函数写进评估脚本里,比事后排查要省心得多。

做完这个项目我最大的体会是,这类供应链赛题拼到最后不是模型炫技,而是对业务约束和数据结构理解到不到位。源码我已经整理成了可以本地直接复跑的形式,跑通前建议先把pulp和lightgbm装好,验证顺序最好先跑基线再逐步加特征和规划模块。第三赛季如果规则有变化,我会继续在赛后更新复盘,把新的约束和优化思路再补进来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询