简介这份资源是阿里天池大数据竞赛「菜鸟-需求预测与分仓规划」赛题的完整实践包面向具备一定机器学习与数据分析基础、希望深入供应链预测场景的参赛者与算法学习者。内容围绕需求预测与分仓规划两大核心任务展开涵盖数据清洗、缺失值填充、异常值处理等预处理流程并实现XGBoost、GBDT、随机森林、SVR线性核与高斯核等多种回归模型同时按分仓单独建模以捕捉仓库特有的销售动态最后通过模型融合提升预测精度。压缩包共68个文件以58个Python脚本为主辅以6个SQL特征工程脚本、2个CSV结果文件、1个R语言ARIMA实现及1份readme说明整体约133KB结构清晰、便于复现。目前已有2332人学习下载读者可从中获取完整的赛题方案、特征工程与建模代码、模型融合思路及分仓规划优化参考适合作为供应链预测与集成学习的实战案例。1. 需求预测与分仓规划从赛题到落地一个被低估的供应链优化入口很多做算法的同学第一次看到“需求预测与分仓规划”这个赛题时第一反应是“不就是时间序列预测吗LSTM 一把梭”。但真正跑过一遍完整流程的人会告诉你预测只是前半场分仓规划才是决定最终得分的胜负手。这个赛题的核心链路是给定历史订单数据先预测未来一段时间各商品在各地区的需求量再根据预测结果决定每个仓库该备多少货、往哪些区域发货最终目标是在满足服务水平的前提下把总成本压到最低。它适合三类人想入门供应链算法的开发者、做过销量预测但没碰过库存优化的同学、以及需要一套可复现 pipeline 来打底的数据分析从业者。我见过太多人预测模型调得飞起结果分仓策略一塌糊涂排名直接掉出前 50%。这篇笔记就按我实际跑过的路径把数据清洗、特征工程、预测建模、分仓决策、避坑排查和进阶技巧一层层拆开讲。2. 数据理解与特征工程先把订单流水变成可建模的矩阵2.1 赛题数据到底长什么样这类赛题通常给三张表订单历史表、商品信息表、仓库与区域映射表。订单历史表是核心字段一般包括日期、商品 ID、区域 ID、订单量或销量。商品信息表提供品类、品牌、价格带等静态属性。仓库映射表告诉你每个仓库覆盖哪些区域、发货成本怎么算。我拿到数据后第一件事不是急着画图而是先确认三件事时间跨度有多长、缺失值集中在哪些维度、区域和仓库是不是多对多关系。常见做法是用 pandas 做一次全量 describe 和缺失率统计把每个字段的分布摸清楚。如果时间跨度不足一年季节性特征就要谨慎使用否则很容易过拟合。区域和仓库如果是多对多分仓规划就变成一个带约束的分配问题复杂度会明显上升。2.2 从原始流水到特征矩阵的四个关键步骤第一步是聚合。原始订单可能是逐笔记录需要按“日期 商品 区域”三个维度做 groupby 求和得到日粒度的销量矩阵。第二步是补齐。有些商品在某些区域某些天没有订单这不代表需求为零而是观测缺失。我一般会先用前向填充加滑动均值做平滑再标记一个缺失指示列让模型自己学。第三步是构造时间特征星期几、是否周末、是否月初月末、距大促的天数。第四步是构造滞后特征和滑窗统计量这是预测模型能不能抓住趋势的关键。import pandas as pd import numpy as np # 假设 raw_df 包含 date, item_id, region_id, qty 四列 raw_df[date] pd.to_datetime(raw_df[date]) # 按日期商品区域聚合 daily raw_df.groupby([date, item_id, region_id])[qty].sum().reset_index() # 构造完整时间索引补齐缺失日期 full_idx pd.MultiIndex.from_product( [pd.date_range(daily[date].min(), daily[date].max()), daily[item_id].unique(), daily[region_id].unique()], names[date, item_id, region_id] ) daily daily.set_index([date, item_id, region_id]).reindex(full_idx).reset_index() daily[qty] daily[qty].fillna(0) # 时间特征 daily[dayofweek] daily[date].dt.dayofweek daily[is_weekend] (daily[dayofweek] 5).astype(int) daily[day] daily[date].dt.day daily[month] daily[date].dt.month # 滞后与滑窗特征 daily daily.sort_values([item_id, region_id, date]) for lag in [1, 7, 14, 28]: daily[flag_{lag}] daily.groupby([item_id, region_id])[qty].shift(lag) for window in [7, 14, 28]: daily[frolling_mean_{window}] ( daily.groupby([item_id, region_id])[qty] .transform(lambda x: x.shift(1).rolling(window).mean()) ) daily[frolling_std_{window}] ( daily.groupby([item_id, region_id])[qty] .transform(lambda x: x.shift(1).rolling(window).std()) )这段代码的逻辑是先聚合再补齐保证每个商品在每个区域都有连续的时间序列。滞后特征用 shift 避免数据泄漏滑窗统计量也先 shift 一位再计算否则当天信息会混进特征里。参数方面lag 选 1、7、14、28 覆盖了日、周、双周、月四个周期rolling window 选 7、14、28 是为了让模型同时看到短期波动和中期趋势。如果数据量很大可以把 rolling 的 transform 换成更高效的 groupby agg 再 merge速度会快不少。2.3 品类与区域维度的降维处理商品数量动辄上千区域也有几十个直接做 one-hot 会让特征维度爆炸。我一般会先按品类做聚合把长尾商品归到“其他”类再对区域按历史销量做聚类把销量模式相似的区域合并成几个大区。这样既保留了区分度又控制了维度。常见做法是用 KMeans 对区域的历史销量曲线做聚类聚类数选 5 到 8 之间具体看轮廓系数。品类维度则可以直接用商品信息表里的品类字段如果品类太细就再往上归一层。3. 需求预测建模从基线到集成模型的完整路径3.1 为什么不能一上来就上深度学习我见过不少同学拿到赛题直接上 LSTM 或 Transformer结果训练慢、调参难、效果还不一定比 LightGBM 好。原因很简单这类赛题的数据量通常不算大商品和区域的组合虽然多但每个序列的长度有限深度学习容易过拟合。而且赛题评分往往对预测误差的容忍度有限树模型在结构化特征上的稳定性和可解释性反而更占优势。我一般会先跑一个移动平均基线再上 LightGBM最后看情况要不要加一个简单的时序模型做 ensemble。基线的作用是给你一个下限如果复杂模型连基线都跑不过那说明特征工程有问题。3.2 LightGBM 做销量预测的参数配置与训练流程LightGBM 是我在这类赛题里用得最多的模型训练快、对缺失值友好、特征重要性直观。下面是我常用的参数配置和训练代码。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit # 特征列 feature_cols [c for c in daily.columns if c not in [date, item_id, region_id, qty]] # 时间序列切分避免随机切分造成泄漏 tscv TimeSeriesSplit(n_splits3) for train_idx, val_idx in tscv.split(daily): train_data daily.iloc[train_idx] val_data daily.iloc[val_idx] lgb_train lgb.Dataset(train_data[feature_cols], train_data[qty]) lgb_val lgb.Dataset(val_data[feature_cols], val_data[qty]) params { objective: regression, metric: mae, learning_rate: 0.05, num_leaves: 64, max_depth: 8, min_data_in_leaf: 50, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, lambda_l2: 1.0, verbose: -1 } model lgb.train( params, lgb_train, num_boost_round2000, valid_sets[lgb_val], callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)] )参数说明learning_rate 设 0.05 是为了在 2000 轮内充分收敛num_leaves 和 max_depth 控制模型复杂度min_data_in_leaf 设 50 防止过拟合到个别序列。feature_fraction 和 bagging_fraction 做特征和样本采样提升泛化。lambda_l2 是 L2 正则对销量预测这种噪声较大的任务很有用。early_stopping 设 100 轮避免无效训练。metric 选 MAE 而不是 MSE因为销量数据里异常值不少MAE 对异常值更鲁棒。3.3 预测结果的校准与后处理模型输出的是连续值但实际销量是非负整数而且有些商品在某些区域就是零销量。我一般会做三步后处理先把负值截断为 0再对预测值做四舍五入取整最后对历史零销量占比超过 90% 的序列强制置零。这一步看起来简单但能明显降低误差。另外如果赛题要求预测多个时间点我会用递归预测先预测未来第一天把预测值当作已知滞后特征再预测第二天依次类推。递归预测的误差会累积所以一般只递归 7 到 14 天再长就不如直接训练一个多步输出模型。4. 分仓规划把预测数字变成可执行的库存决策4.1 分仓规划到底在优化什么预测出每个区域的需求后分仓规划要回答的问题是每个仓库应该备多少货才能以最低成本满足各区域需求。成本通常包括仓储成本、调拨成本和缺货惩罚。约束包括仓库容量、区域覆盖关系、最小起订量等。这个问题本质上是一个带约束的线性规划或混合整数规划。常见做法是用 scipy 或 PuLP 建一个简化模型先跑通再逐步加约束。如果赛题规模不大直接上求解器如果规模大就用启发式规则先做分配再用局部搜索优化。4.2 用线性规划做分仓分配的最小示例下面是一个简化的分仓分配模型假设有两个仓库、三个区域目标是调拨成本加缺货惩罚最小。import pulp # 仓库列表和区域列表 warehouses [W1, W2] regions [R1, R2, R3] # 预测需求 demand {R1: 100, R2: 150, R3: 80} # 调拨成本从仓库到区域的单位成本 transport_cost { (W1, R1): 2, (W1, R2): 4, (W1, R3): 6, (W2, R1): 5, (W2, R2): 3, (W2, R3): 2 } # 仓库容量 capacity {W1: 200, W2: 180} # 缺货惩罚系数 shortage_penalty 10 prob pulp.LpProblem(warehouse_allocation, pulp.LpMinimize) # 决策变量从仓库 w 发往区域 r 的数量 x pulp.LpVariable.dicts(x, (warehouses, regions), lowBound0, catContinuous) # 缺货量 s pulp.LpVariable.dicts(shortage, regions, lowBound0, catContinuous) # 目标函数调拨成本 缺货惩罚 prob ( pulp.lpSum(x[w][r] * transport_cost[(w, r)] for w in warehouses for r in regions) pulp.lpSum(s[r] * shortage_penalty for r in regions) ) # 约束1每个区域收到的货加缺货量等于需求 for r in regions: prob pulp.lpSum(x[w][r] for w in warehouses) s[r] demand[r] # 约束2每个仓库发货总量不超过容量 for w in warehouses: prob pulp.lpSum(x[w][r] for r in regions) capacity[w] prob.solve() print(Status:, pulp.LpStatus[prob.status]) for w in warehouses: for r in regions: print(f{w} - {r}: {x[w][r].varValue}) for r in regions: print(f{r} shortage: {s[r].varValue})这段代码的逻辑很直接决策变量是每个仓库发往每个区域的数量目标函数是调拨成本加缺货惩罚约束是需求满足和容量限制。参数方面transport_cost 可以从赛题给的仓库区域映射表里算出来capacity 是仓库容量shortage_penalty 需要根据业务容忍度来设设得太低会导致大量缺货设得太高会导致过度备货。求解器用 PuLP 默认的 CBC 就行小规模问题秒出结果。如果赛题要求整数解把 cat 改成 Integer 即可但求解时间会变长。4.3 从线性规划到启发式大规模场景的降级方案当仓库和区域数量上百时线性规划可能跑不动。这时候我会用两阶段启发式先按历史调拨比例或距离最近原则做初始分配再用局部搜索调整。具体做法是对每个区域按调拨成本排序优先从成本最低的仓库发货发完容量再顺延到下一个仓库。然后计算总成本随机交换两个区域的仓库分配如果成本下降就接受否则以一定概率接受跑几千轮就能得到一个不错的解。这种方法的优点是快、可解释、容易加业务规则缺点是可能陷入局部最优。如果时间允许我会用线性规划的解作为初始解再用启发式做微调。5. 避坑与排查那些让我排名掉出前 50% 的坑5.1 数据泄漏滞后特征没 shift 导致线下虚高现象是线下验证 MAE 很低线上提交后分数差一大截。原因是构造 rolling 特征时没有 shift当天的销量信息混进了特征里模型在验证集上“偷看”了答案。解决方法是所有时序特征一律先 shift 再计算验证集切分必须按时间切不能用随机切分。我一般会在代码里加一个检查如果某个特征的日期和标签日期相同直接报错。5.2 零销量序列处理不当导致预测全为零现象是很多商品在部分区域的预测值全是零但实际有零星需求。原因是这些序列历史零销量占比太高模型学到了“总是零”的模式。解决方法是对零销量序列做分层处理零销量占比超过 95% 的序列单独用规则预测比如用区域平均需求乘以一个衰减系数占比在 70% 到 95% 之间的序列在特征里加一个“是否零销量日”的指示变量让模型区分“真零”和“缺失”。5.3 分仓规划忽略最小起订量导致方案不可执行现象是线性规划给出的分配方案里某些仓库发往某些区域的数量是 3 件、5 件这种小数实际业务里根本没法操作。原因是模型没有加最小起订量约束。解决方法是在约束里加一个二元变量表示是否从某仓库发往某区域如果发就往大于等于最小起订量不发就为零。这会增加模型复杂度但能保证方案可落地。如果赛题不要求整数解至少要把小于某个阈值的分配量合并到其他仓库。5.4 预测粒度与分仓粒度不一致导致误差放大现象是预测按商品区域做但分仓规划按品类大区做两边对不上。原因是预测和分仓的维度没有对齐。解决方法是先确定分仓规划的决策粒度再按这个粒度聚合预测结果。如果分仓按品类大区就把商品区域的预测值按品类和大区求和再输入分仓模型。反过来如果分仓粒度更细就需要把预测结果拆解拆解时按历史比例分配。5.5 忽略大促和节假日导致预测系统性偏低现象是大促期间的预测值明显低于实际销量缺货严重。原因是模型没有大促特征或者大促样本太少学不到。解决方法是手动加一个大促标记列把历史大促日的销量做一个放大系数预测时对大促日乘以这个系数。放大系数可以用历史大促日销量除以非大促日销量的均值来估计。如果赛题没有给大促日期就从销量曲线上找突增点人工标注。6. 进阶技巧用滚动预测和情景分析提升分仓鲁棒性最后一章讲一个我实际用过、效果比较稳的技巧滚动预测加情景分析。滚动预测的意思是不要一次性预测未来 30 天然后就不管了而是每天用最新数据重新预测未来 7 天只取第一天的结果第二天再滚动一次。这样做的好处是模型能持续吸收最新信息对突发波动更敏感。代价是计算量变大但这类赛题的数据量通常撑得住。情景分析则是针对分仓规划不要只用一个预测值去优化而是生成三套需求情景乐观、中性、悲观。乐观情景用预测值乘以 1.2悲观乘以 0.8然后分别跑分仓模型看哪个方案在三套情景下的总成本最稳。我一般会选那个在中性情景下成本不是最低、但在悲观情景下缺货最少的方案。下面是一个滚动预测的伪代码框架。# 滚动预测框架 history daily.copy() predictions [] for day in range(forecast_horizon): # 用当前历史训练模型 model train_model(history) # 预测未来一天 next_day_features build_features(history, target_datehistory[date].max() pd.Timedelta(days1)) pred model.predict(next_day_features) # 把预测值追加到历史作为下一天的滞后特征 new_row next_day_features.copy() new_row[qty] pred history pd.concat([history, new_row], ignore_indexTrue) predictions.append(pred) # 情景分析 for multiplier in [0.8, 1.0, 1.2]: scenario_demand {r: demand[r] * multiplier for r in regions} solve_allocation(scenario_demand)这段框架的关键点有两个一是每次滚动都要重新训练模型不能只用一次训练好的模型递归否则误差会累积得很快二是情景分析的分仓方案要对比总成本和缺货量不能只看单一指标。参数方面forecast_horizon 一般设 7 到 14 天再长滚动收益就不明显了。multiplier 的 0.8 和 1.2 是根据历史预测误差的分布来定的如果误差标准差是 15%那 0.8 到 1.2 基本覆盖了大部分情况。我自己的习惯是每次跑完分仓规划都会把三套情景的方案放在一起看如果某个方案在悲观情景下缺货量比另外两个方案低 30% 以上那即使中性情景成本高一点我也会选它。供应链这件事后悔药很贵宁可备货多一点也别在大促时断货。希望帮到你。本文还有配套的精品资源点击获取