首页 / 资讯中心 / 文章详情

天池赛题解析:需求预测与分仓规划实战

天池赛题解析:需求预测与分仓规划实战 ★ FEATURED ARTICLE
简介一份天池大数据竞赛‘菜鸟-需求预测与分仓规划’赛题的完整配套代码包面向参赛者及供应链预测、仓储优化方向的学习者。内容围绕需求预测与分仓规划两条主线覆盖数据清洗、缺失值填充、特征工程、分仓独立建模以及XGBoost、GBDT、随机森林、SVR等回归模型的训练与融合同时包含ARIMA时序预测、成本计算与规则/集成对比脚本可帮助快速跑通从数据准备到结果评估的完整流程。包体共68个文件以Python脚本为主58个py承担数据预处理、特征构造、模型训练与混合集成等环节另有6个SQL文件用于生成训练/测试特征2个CSV存放预测结果或中间数据1个R脚本实现ARIMA建模1个Markdown文档说明目录结构与用法。压缩包仅133KB体量不大但模块完整目录按训练/验证、分仓特征、模型融合等划分清晰便于对照学习。目前CSDN已有2331人浏览学习可直接运行并二次改进既可用作赛题复现的基线代码也能作为需求预测与分仓规划项目的工程参考适合补充实战经验、梳理系统实现细节。1. 天池大数据竞赛里的菜鸟赛题需求预测与分仓规划在解决什么天池大数据竞赛的这道菜鸟-需求预测与分仓规划赛题表面上是让参赛者预测未来几周每个仓库的订单量实际上考的是供应链里的“仓配网络”这一整套决策链路。它给出的场景是近场电商的仓网结构货品从总仓调拨到各个区域仓再从区域仓发往消费者。你预测不准库存储备就失真——少备了断货罚款多备了压仓费、周转变慢两边都是真金白银的损失。这道题适合三类人正在做物流数据分析和供应链计划的数据工程师、想练时序建模与运筹优化落地能力的算法工程师以及准备用竞赛项目充实简历的应届生。一个反直觉的结论是预测精度提升 1% 带来的收益往往不如你把安全库存系数调对一次这就是分仓规划的价值所在。2. 先拆赛题再看数据把「仓-SKU-时间」三维对齐再动手赛题的数据通常落在四张表上订单明细表、商品信息表、仓库信息表、用户浏览/加购流量表。新手最容易犯的第一个错是抱着订单明细表直接开始做时间序列预测。这不对。订单明细是一行一个订单项必须先聚合到「仓 SKU 天」这个维度才有资格谈预测。这一章先把仓网结构和数据聚合讲透后面建模才不会翻车。2.1 仓网结构与履约成本的隐藏关系近场电商的仓网一般分两层中心仓和区域仓。中心仓负责囤全量货品区域仓从中心仓进货再覆盖各自周边的配送范围。分仓规划要回答的问题就是每种 SKU 在每个区域仓放多少货既能满足本地需求又不至于堆到库容上限。这两个目标天然冲突所以赛题不会只考你预测准不准而是考你有没有能力在“预测结果”之上做一道运筹优化的应用题。区域仓之间也存在调拨成本。比如 A 仓爆单但 B 仓积压最优解可能是从 B 仓调货到 A 仓而不是让 A 仓临时向中心仓紧急补货因为调拨成本往往低于紧急配送成本。这就在模型里引入了 SKU、仓、时间的二维矩阵决策。把所有约束写进模型之后预测误差会直接传导到补货量上所以分仓规划对预测误差的容忍度很低——尤其怕那种“平时很准、大促全歪”的误差。2.2 用 SQL 聚合订单明细成「仓-品-日」宽表不管后续你用 LightGBM 还是 Prophet第一步都得先把订单明细变成一份干净的日度宽表。常见做法是写一段 GROUP BY 的 SQL把订单表按 warehouse_id、sku_id 和日期聚合同时保留订单数和用户数两个辅助维度后面做特征时会用到。-- 最终产出wh_sku_daily 宽表一行 一个仓 一个SKU 一天 CREATE TABLE wh_sku_daily AS SELECT o.warehouse_id, o.sku_id, TO_CHAR(o.order_date, YYYY-MM-DD) AS dt, SUM(o.order_qty) AS qty, -- 当日总销量 COUNT(DISTINCT o.order_id) AS order_cnt, -- 当日订单数 COUNT(DISTINCT o.user_id) AS user_cnt -- 当日购买用户数 FROM order_detail_tbl o WHERE o.order_date DATE 2023-01-01 AND o.order_date DATE 2024-06-30 GROUP BY o.warehouse_id, o.sku_id, TO_CHAR(o.order_date, YYYY-MM-DD);这段 SQL 的逻辑不复杂但有三个地方值得解释。第一用 TO_CHAR 把时间戳归一到日期粒度后续做滞后特征时不会因为小时颗粒度产生脏数据。第二COUNT(DISTINCT order_id) 和 COUNT(DISTINCT user_id) 不是冗余它们能反映同一 SKU 的购买是集中在少数大单还是分散在大量散单这对预测形态很重要。第三WHERE 条件里的日期范围建议比训练集再放宽 30 天因为边界日期的滞后特征会缺失多留一点余量可以避免特征工程时丢数据。2.3 数据质量探查覆盖率、长尾分布与流量表的价值聚合完成后先别急着建模花一个小时探查这份宽表的数据质量。重点看三个指标第一仓库对 SKU 的覆盖率即每个仓实际上覆盖了多少个 SKU覆盖率低于 30% 的仓基本是新品仓或临时仓建模时要单独处理第二销量分布的幂律特征大部分 SKU 的日均销量可能只有个位数少数爆款占了八成销量第三零销量天数占比零销量过多的 SKU 会让时序模型学出一堆零预测但分仓规划又恰恰不能忽略它们。-- 仓库覆盖率与销量分布 SELECT warehouse_id, COUNT(DISTINCT sku_id) AS sku_cnt, ROUND(AVG(qty), 2) AS avg_daily_qty, ROUND(STDDEV(qty), 2) AS std_daily_qty, ROUND(SUM(CASE WHEN qty 0 THEN 1 ELSE 0 END) * 1.0 / COUNT(*), 4) AS zero_ratio FROM wh_sku_daily GROUP BY warehouse_id ORDER BY avg_daily_qty DESC;流量表的价值也在此刻体现出来。用户浏览加购数据的时间粒度更细而且往往比实际下单提前 1 到 3 天把它按仓和 SKU 聚合成“前 7 天浏览量”“前 3 天加购量”这样的特征可以显著提升大促前夕的预测效果。也就是说建宽表时不要只盯着订单表把流量表也聚合进来后面做特征工程会省一半功夫。3. 需求预测输入准备特征工程与四类候选模型宽表就绪后进入预测建模阶段。竞赛型需求预测的历史数据通常有 12 个月以上目标一般是一到两周的日度预测。时序模型的选型不外乎四类统计模型Prophet / ARIMA、树模型LightGBM / XGBoost、深度学习模型DeepAR / TFT、以及简单的均值基准。我一般不推荐一上来就上 DeepAR不是因为精度不够而是因为调试成本太高对样本量也有要求。树模型加一套扎实的特征工程在这个量级的数据上更容易达到可复现的效果。3.1 先做特征工程滞后特征、滚动窗口与日历特征是底线直接拿原始销量序列喂给树模型是行不通的。树模型不像时序模型自带序列结构它需要你主动构造“过去的信息”。最低配置是三组特征滞后特征lag、滚动窗口统计roll_mean / roll_std、日历特征星期、月、月初、大促前标记。注意一个关键细节所有窗口统计都要基于 shift(1) 之后的数据否则你用了当天数据去预测当天这是特征泄漏会让验证集指标好看得离谱上线直接报废。# 特征工程滞后特征 滚动窗口 日历特征 def build_ts_features(df, lags(7, 14, 21, 28), windows(7, 14)): df df.sort_values([warehouse_id, sku_id, dt]).reset_index(dropTrue) # 按 SKU 仓分组确保序列不回串 grp df.groupby([warehouse_id, sku_id])[qty] # 滞后特征过去第 7/14/21/28 天的销量 for lag in lags: df[flag_{lag}] grp.shift(lag) # 滚动窗口均值与标准差用 shift(1) 排除当天数据避免泄漏 for win in windows: df[froll_mean_{win}] grp.transform( lambda x: x.shift(1).rolling(win, min_periodswin // 2).mean() ) df[froll_std_{win}] grp.transform( lambda x: x.shift(1).rolling(win, min_periodswin // 2).std() ) # 日历特征 dt_series pd.to_datetime(df[dt]) df[weekday] dt_series.dt.weekday df[month] dt_series.dt.month df[is_month_start] dt_series.dt.is_month_start.astype(int) # 流量特征前 7 天加购量/浏览量若数据表里没有可跳过 if cart_cnt in df.columns: df[cart_7d] ( df.groupby([warehouse_id, sku_id])[cart_cnt] .transform(lambda x: x.shift(1).rolling(7, min_periods3).sum()) ) return df参数说明lags 选 7、14、21、28 是为了覆盖周周期性14 天窗口对应两个完整周min_periodswin // 2 是为了在序列头部样本不足时允许用部分窗口计算避免前期数据全部变成 NaN。roll_std 很多人不愿意加怕引入噪声但在大促场景下它恰恰能反映需求的波动性对分仓规划里的安全库存计算非常有用。3.2 用 LightGBM 训练预测模型参数与回看窗口设置特征工程做完后用 LightGBM 做基线模型是最稳的路径。训练时有两件事必须做对。第一按时间切分验证集而不是随机切分因为时序数据一旦随机切分就会把未来的信息泄露给训练集。第二验证集的长度必须和预测目标一致如果任务是预测未来 14 天验证集就取最后 14 天而不是只取最后一天。import lightgbm as lgb feature_cols [c for c in df.columns if c.startswith((lag_, roll_, weekday, month, is_, cart_))] cutoff 2024-05-17 # 训练截止日 horizon 14 # 预测未来 14 天 train df[df[dt] cutoff] valid df[(df[dt] cutoff) (df[dt] cutoff pd.Timedelta(dayshorizon))] dtr lgb.Dataset(train[feature_cols], labeltrain[qty]) dva lgb.Dataset(valid[feature_cols], labelvalid[qty]) params { objective: regression, metric: mae, learning_rate: 0.05, num_leaves: 63, min_data_in_leaf: 20, feature_fraction: 0.8, bagging_fraction: 0.8, verbosity: -1, seed: 42, } model lgb.train( params, dtr, num_boost_round1000, valid_sets[dva], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)], )这段代码里最有讲究的三个参数是 num_leaves、min_data_in_leaf 和 learning_rate。num_leaves 设 63 是因为数据量还没大到需要几百片叶子的程度设太大容易让树拟合到个别 SKU 的噪声min_data_in_leaf 设 20 是对抗长尾 SKU 的有效手段避免某些销量常年为 0 的序列被过度拟合learning_rate 设 0.05 是精度和训练时间的折中竞赛型数据 1000 轮以内基本能收敛。早停轮次设 50 的话实际训练轮数往往在 300 到 600 之间。3.3 模型评测用 SMAPE 而不是 MAE误差分布要盯两个点需求预测的评测指标天池这类竞赛常用 SMAPE它是对称的平均绝对百分比误差公式是SMAPE 100% × (2 × |y - y_hat|) / (|y| |y_hat|)。import numpy as np def smape(y_true, y_pred): return 100.0 * np.mean( 2 * np.abs(y_true - y_pred) / (np.abs(y_true) np.abs(y_pred) 1e-8) )SMAPE 和 MAE 最大的区别在于按相对误差计算所以同样是错 10 件销量 100 的 SKU 比销量 5 的 SKU 权重低得多。但这也带来一个问题模型为了压低 SMAPE会倾向于在低销量 SKU 上预测偏保守因为反正绝对值误差很小。看评测时我习惯同时看两个分布指标预测误差的标准差评估波动风险和零销量 SKU 的预测命中率评估冷启动。只盯一个整体均值大促期间的表现往往会被平时兜底。模型选型层面常见候选方案的对比可以看这张表模型精度训练速度冷启动可解释性适用场景LightGBM高快中高大规模短中期预测基础必备XGBoost高较快中高与 LightGBM 效果接近可做融合Prophet中快低高无特征工程的快速基线DeepAR/TFT较高慢高低时序样本充足的长序列预测Prophet 也不是不能用但它的优势只在纯序列数据上很难把流量特征、大促标记这些外部变量用足。树模型才是这个赛题的主战场。4. 分仓规划建模把预测误差写进约束用线性规划求补货量预测做完了接下来的问题才是赛题的真正核心怎么把预测结果转化为每个仓每一天的补货指令。补货决策不是简单地“预测 100 件就补 100 件”因为要考虑安全库存、仓容上限、跨仓调拨成本、缺货惩罚这四组约束稍有冲突就会算出不可执行的方案。这里最常见的解法是把问题建模成一个混合整数线性规划用 PuLP 或 OR-Tools 求解。4.1 从预测到补货安全库存与缺货成本怎么定预测模型输出的是一个均值。但实际需求围绕均值波动你需要一个安全库存来缓冲这种波动。安全库存的常见做法是用预测标准差乘以一个服务水平系数 Z。服务水平设 90% 时 Z 取 1.65设 95% 时取 1.96。也就是说预测均值 100、标准差 15 时安全库存为 1.65 × 15 ≈ 25 件所以这天的合理库存水位至少是 125 件。安全库存系数不是越高越好。缺货成本和服务水平之间的平衡点要按实际业务来定。我的做法是先定三个参数缺货惩罚一般参考单件毛利加紧急配送成本、仓储持有成本按库存资金占用计算、调拨成本跨仓转运的单件运费。这三个参数决定了目标函数里每一项的权重数值设错模型解出来的方案就会出现“囤了一堆货但是断货依然发生”的奇怪结果。4.2 用 PuLP 建一个可复跑的分仓规划模型这一节给一个可直接复跑的线性规划模型。决策变量是每个仓每个 SKU 每天的补货量目标是让缺货惩罚、仓储成本和调拨成本的总和最小。约束包括库存流转恒等式、仓容量上限、以及服务水平要求。import pulp # 输入参数 W warehouse_list # 区域仓列表 S sku_list # SKU 列表 T 14 # 规划周期天数 forecast {} # forecast[w][s][t] 预测销量 init_inv {} # init_inv[w][s] 期初库存 capacity {} # capacity[w] 仓库存容量上限 shortage_penalty 15.0 # 缺货惩罚元/件 storage_cost 0.02 # 仓储成本元/件/天 transfer_cost 3.5 # 调拨成本元/件 prob pulp.LpProblem(wh_plan, pulp.LpMinimize) # 决策变量补货量、缺货量、库存量 x pulp.LpVariable.dicts(order, (W, S, range(T)), 0, None, pulp.LpInteger) s pulp.LpVariable.dicts(short, (W, S, range(T)), 0, None, pulp.LpInteger) inv pulp.LpVariable.dicts(inv, (W, S, range(T 1)), 0, None, pulp.LpContinuous) # 目标函数缺货成本 仓储成本 调拨成本 prob ( pulp.lpSum(shortage_penalty * s[w][s_][t] for w in W for s_ in S for t in range(T)) pulp.lpSum(storage_cost * inv[w][s_][t] for w in W for s_ in S for t in range(T)) pulp.lpSum(transfer_cost * x[w][s_][t] for w in W for s_ in S for t in range(T)) ) # 库存流转约束库存 上期库存 补货 - 预测销量 缺货量 for w in W: for s_ in S: inv[w][s_][0] init_inv[w][s_] for t in range(T): prob inv[w][s_][t 1] ( inv[w][s_][t] x[w][s_][t] - forecast[w][s_][t] s[w][s_][t] ) # 仓容量约束任意一天的库存总量不得超过仓容量 for w in W: for t in range(T 1): prob pulp.lpSum(inv[w][s_][t] for s_ in S) capacity[w] # 求解 prob.solve(pulp.PULP_CBC_MSG0) print(f求解状态: {pulp.LpStatus[prob.status]})这个模型里最核心的是库存流转约束它给出了库存的递推关系今天的库存加上今天到的补货减去今天的预测需求加上允许的缺货量等于明天的库存。缺货量设成整数变量是因为缺货后这部分需求并没有消失而是转移到下一期或者在账面上记录为欠货允许模型在缺货惩罚较高时通过它来吸收预测偏差。仓容量约束则是防止模型把某个 SKU 的货全部堆到一个仓里。目标函数三个系数的作用参数取值说明shortage_penalty15 元/件缺货惩罚应约等于单件毛利 紧急配送成本storage_cost0.02 元/件/天持有成本按库存资金占用年化折算transfer_cost3.5 元/件调拨成本含跨仓装箱和运输费用注意 shortage_penalty 和 storage_cost 的关系。如果缺货惩罚设得太高模型会疯狂堆库存仓储成本随之飙升设得太低模型又宁愿缺货也不备货服务率达不到要求。这是调参时最耗时间的一组矛盾。4.3 求解结果怎么回写与验证求解完成后需要把 x[w][s_][t] 中非零的记录导出成一份补货计划表格式一般是一行一条“仓 SKU 日期 补货量”。写完补货计划表之后一定要做一次仿真回验用这个补货计划去模拟未来 14 天的库存变化逐日滚动计算缺货量和库存水位而不是只盯着目标函数值大小。回验时重点看两个指标整体缺货比例不能超过 5%爆仓比例不能超过 1%。如果爆仓率过高说明仓容量约束太紧可以考虑把部分 SKU 的补货周期拉长如果缺货率过高则需要提高安全库存覆盖天数。这一步跑通后分仓规划模型才算真正可用而不只是一堆求解成功的状态码。5. 避坑需求预测与分仓规划里的 5 个真实翻车案例这里把我在类似赛题和实际供应链数据里踩过的五次最深坑整理出来按“现象 → 原因 → 解决”的格式写方便你对照自查。5.1 回看窗口整体偏移验证集指标虚高现象模型在验证集上 SMAPE 只有 18%看起来已经达标但一提交线上评测分数掉到 29%排名直接垫底。原因训练集和验证集的切分方式看似按时间切了但特征里的滞后项和滚动窗口项用的是日期序列索引而不是 shift 之后再对齐。结果验证集首日特征里混进了当天的销量形成了泄漏。黑匣子一样的树模型对这类泄漏非常敏感验证集指标虚高完全正常。解决统一用 shift(1) 之后的数据构造所有序列特征并且把验证集整体往后挪一天再做一次冒烟测试看指标是否显著变差。如果切分逻辑正确结果不会因为整体平移一天产生巨大差异。5.2 新仓和新 SKU 的冷启动预测全部预测为零现象新品上架或新仓开业的前 4 周没有历史销量LightGBM 预测输出全是 0。分仓规划模型认为这些 SKU 不需要备货导致新品上市首周即大面积断货。原因滞后特征和滚动特征在序列头部全是 NaN树模型把 NaN 当作缺失后学习到“没有历史没有需求”的隐含规律低估冷启动需求。解决对缺失的滞后特征做分组填充填充值用同仓同品类 SKU 的历史均值如果品类均值也没有再用全体 SKU 的全局均值兜底。同时新增一个“缺少历史天数”的计数特征让模型感知冷启动状态模型会据此自动调整预测值。5.3 大促效应被滚动窗口特征平均掉现象大促前 3 天流量开始上涨模型预测却还在平稳区间直到大促当天才反应过来大促结束后预测又居高不下连续高估 5 天。原因滚动窗口均值对突发的需求尖峰天然滞后大促前的小幅上涨被 7 天窗口均摊以后淹没了。解决加入促销事件特征至少包含“距离大促开始还剩多少天”和“大促期间是否生效”两个字段。用赛题里自带的流量表加购数据也能显著缓解这个问题加购量往往比销量提前 2 到 3 天爆发。5.4 缺货惩罚设太高预测越准库存越涨现象预测模型精度提升后分仓规划算出来的补货量反而越补越多仓库爆仓库存周转天数从 15 天变成了 35 天。原因缺货惩罚设成了单件毛利的 10 倍模型的安全库存水位被服务水平系数顶到上限宁可多囤也不缺货。解决用“毛利损失 紧急配送成本”去标定缺货惩罚而不是拍脑袋设一个很大的数。同时加一个约束每个仓的总库存周转天数不得超过 25 天从模型层面限制过度囤货。5.5 分仓规划忽略了仓容量求解结果根本执行不下去现象线性规划求解器输出了最优解状态码显示 Optimal但仓库运营团队反馈说 60% 的仓在第三天就爆仓了。原因库存流转约束只计算了库存量但没有把每个 SKU 的体积和仓储占用面积算进去。大件 SKU 和小件 SKU 的存储占用量是不一样的。解决仓容量约束从件数约束换成体积约束。具体做法是增加一个 sku_volume 参数把容量判断写成 sum(sku_volume[s] * inv[w][s][t]) capacity_volume[w]。这类坑属于业务约束和模型约束没对齐赛题里不会提示但实际落地时一定会遇到。6. 让规划结果经得起回看滚动回看窗口与两层验证技巧模型和线性规划都跑通之后最后一个建议是不要在校验集上只做一次评估就收手。我一般会做一个滚动回看实验把历史数据切成 24 周每次用前 20 周做训练预测后 2 周然后整体向后滚动一周再重复最终得到 24 组预测结果。这 24 组结果的平均 SMAPE 和标准差比单次切分的结果有说服力得多。因为单次切分只能说明模型在这个时间点没出问题而滚动回看能暴露模型在淡旺季转换、大促前夜等不同阶段的稳定性差异。第二层验证是分仓计划仿真。把 24 组预测结果分别灌进分仓规划模型得到 24 份补货计划再对每份计划做逐日库存模拟。看的是两个业务指标断货率和周转天数。这两个指标的均值与极端值比目标函数值更能说明问题——目标函数是三者加权和加权系数本身就有主观成分。我自己最早做这类赛题时只跑了一版线上提交就收手后来发现换一个回看窗口后结论完全反转平时表现最好的模型在大促周竟然垫底。从此以后我给自己定了一条规矩——任何需求预测和分仓规划方案不跑完滚动回看不算验证完。这个习惯帮我避免过不少线上翻车也希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站