尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

天池菜鸟需求预测与分仓规划实战:两阶段建模与源码复现

天池菜鸟需求预测与分仓规划实战:两阶段建模与源码复现 简介需求预测是供应链管理中的核心环节其目标并非单纯追求预测精度而是为后续的库存优化与资源配置提供决策依据。在实际业务中预测结果往往需要结合分仓规划综合考虑仓库容量、运输成本和缺货惩罚等约束才能形成可执行的备货方案。特征工程在预测模型中起着决定性作用通过历史统计、时间周期、商品属性和交叉特征可以有效提升模型对复杂销售规律的捕捉能力。以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.05num_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_splits5) 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, weightweight)这个自定义损失在验证集上的提升不一定很大但在最终业务评分上表现非常明显。天池这类比赛很多时候比的就是谁更贴合业务而不是谁的模型MAE最低。3.4 融合权重按验证损失反比计算单靠LightGBM还不够因为树模型对周期性、节假日这类长周期信号捕捉较弱。我另外训练了一个Prophet模型专门用来预测SKU和仓库聚合到类目层级的总量趋势然后把类目趋势按历史份额摊回每个SKU。这样既保留了树模型的强特征学习能力又补上了时间序列模型对周期和节假日的敏感度。融合不是简单平均。我计算了每个模型在验证集上的MAE再按损失倒数归一化得到权重。比如LGBM验证MAE是12Prophet是16那LGBM的融合权重就是1/12除以(1/121/16)大约0.57Prophet是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(fx_{sku}_{wh}, lowBound0, catInteger) # 目标运输成本 缺货惩罚 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, keylambda 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装好验证顺序最好先跑基线再逐步加特征和规划模块。第三赛季如果规则有变化我会继续在赛后更新复盘把新的约束和优化思路再补进来。本文还有配套的精品资源点击获取
返回列表