☰
电商需求预测与分仓规划:从特征工程到模型融合的完整方案
2026/10/14 14:06:41 网站建设 项目流程

简介:这份资源是阿里天池大数据竞赛「菜鸟-需求预测与分仓规划」赛题的完整参赛资料包,面向具备一定机器学习与数据分析基础、希望深入供应链预测与仓储优化实战的开发者与竞赛选手。内容围绕需求预测与分仓规划两大核心任务展开,涵盖数据清洗、缺失值与异常值处理、特征工程、样本生成等预处理环节,并给出XGBoost、GBDT、随机森林、SVR(线性核与高斯核)等多种回归模型的实现,以及分仓独立建模与模型融合的完整思路。资源包共68个文件,以58个Python脚本为主,辅以6个SQL特征处理脚本、2个CSV数据文件、1个R语言ARIMA实现和1份说明文档,压缩包约133KB,结构清晰、便于按模块查阅。目前已有2332人学习下载,读者可从中获取赛题方案、特征构造逻辑、多模型对比与融合代码及成本计算脚本,适合作为供应链预测类项目的参考范例。

1. 需求预测与分仓规划:一份能跑通全流程的竞赛方案拆解

电商大促前两周,运营群里最常出现的对话是「这个仓备多少货」和「那个区域要不要临时租仓」。需求预测偏了,要么爆仓发不出,要么压货压到现金流断裂;分仓规划错了,跨区调拨的物流成本能吃掉全部毛利。这份天池赛题方案围绕「菜鸟-需求预测与分仓规划」展开,核心是把历史订单拆成「预测未来需求量」和「决定每个仓发多少」两个耦合问题,用回归模型加模型融合给出可复现的解法。它适合已经会调 sklearn、但没完整走过「特征工程→多模型融合→分仓决策」链路的从业者,也适合想拿一套现成 pipeline 改自己业务数据的同学。下面按我实际拆包复现的顺序讲,参数和坑都落在具体位置。

2. 需求预测建模:从原始订单到可训练特征矩阵

2.1 数据字段与预测粒度怎么定

拿到订单明细后第一件事不是急着 fit,而是确认预测粒度。赛题给的是「商品-仓库-日期」维度的出货量,但真实业务里往往还要叠加「区域」和「促销标签」。我一般先把字段分成四类:标识列(商品 ID、仓库 ID)、时间列(日期、星期、是否节假日)、数值列(历史出货量、库存量)、类别列(商品类目、仓库所在区域)。预测目标通常是未来 1 到 7 天的日出货量,粒度太细会导致稀疏,太粗又失去分仓意义。

常见做法是把日期做滑窗聚合,生成 lag 特征。比如用过去 7 天、14 天、28 天的均值、最大值、标准差作为输入。这里有个容易翻车的点:lag 窗口不能跨过预测起点,否则就是标签泄漏。我见过有人把未来三天的数据也滚进特征里,线下 AUC 高得离谱,线上直接崩。正确做法是按时间切分训练集和验证集,比如前 80% 天做训练,后 20% 做验证,滑窗只在训练段内滚动。

import pandas as pd import numpy as np def build_lag_features(df, target_col, lags=[7, 14, 28]): """ df: 含 date, item_id, warehouse_id, target_col 的明细 返回带 lag 统计量的特征表 """ df = df.sort_values(['item_id', 'warehouse_id', 'date']) grp = df.groupby(['item_id', 'warehouse_id'])[target_col] for lag in lags: # 过去 lag 天的均值,shift(1) 保证不包含当天 df[f'lag_{lag}_mean'] = grp.transform( lambda x: x.shift(1).rolling(lag, min_periods=1).mean() ) df[f'lag_{lag}_std'] = grp.transform( lambda x: x.shift(1).rolling(lag, min_periods=1).std() ) return df

这段代码的关键在shift(1),它把当前天的值排除在统计窗口外。min_periods=1是为了让序列开头不产生 NaN,否则后面模型训练要额外做填充。lags列表按业务周期选,快消品用 7/14/28,耐用品可以拉到 30/60/90。如果数据量很大,transform加lambda会慢,可以换成groupby().rolling()再 merge,但逻辑不变。

2.2 回归模型选型与训练脚本

特征矩阵建好后,基模型我通常选三个:LightGBM 处理高维稀疏特征,XGBoost 做稳定基线,Ridge 回归当线性兜底。不要一上来就上深度学习,表格数据上 GBDT 的性价比最高。LightGBM 的参数重点调num_leaves(控制树复杂度,一般 31 到 127)、learning_rate(0.05 到 0.1)、feature_fraction(0.7 到 0.9)。XGBoost 对应调max_depth(4 到 8)和subsample(0.7 到 0.9)。

import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error def train_lgb(X_train, y_train, X_val, y_val): params = { 'objective': 'regression_l1', # MAE 对异常值更稳 'metric': 'mae', 'num_leaves': 63, 'learning_rate': 0.05, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': -1 } dtrain = lgb.Dataset(X_train, y_train) dval = lgb.Dataset(X_val, y_val, reference=dtrain) model = lgb.train( params, dtrain, num_boost_round=2000, valid_sets=[dval], callbacks=[lgb.early_stopping(100), lgb.log_evaluation(200)] ) pred = model.predict(X_val) print('MAE:', mean_absolute_error(y_val, pred)) return model

objective选regression_l1而不是默认的 L2,是因为出货量常有促销尖峰,L2 会被大值带偏。early_stopping(100)表示验证集 100 轮不提升就停,防止过拟合。TimeSeriesSplit在时序场景比随机 KFold 靠谱,但赛题数据如果按天连续,直接切最后 20% 更省事。训练完记得把feature_importance导出来看,如果lag_7_mean排第一但lag_28_std几乎为零,说明长周期波动没被捕捉,要回去检查数据是否做了差分。

3. 模型融合:把三个基模型的预测拧成一股绳

3.1 加权融合与 Stacking 的取舍

单模型 MAE 通常在 8% 到 12% 之间浮动,融合后能压到 7% 左右。最简单的是加权平均,权重按验证集 MAE 倒数分配。但加权平均有个前提:基模型误差要相对独立。如果 LightGBM 和 XGBoost 用了同一套特征和相似参数,它们的预测相关性可能到 0.95 以上,融合收益极小。我一般先算预测值的 Pearson 相关系数,超过 0.9 就换模型或换特征子集。

Stacking 更稳但更重。做法是用基模型在验证集上的预测作为新特征,再训一个 Ridge 或小 LightGBM 做元学习器。注意元学习器的训练数据必须来自交叉验证的 out-of-fold 预测,否则还是标签泄漏。实操里我用 5 折时序交叉验证生成 OOF 预测,每折内训基模型、折外预测,最后拼成完整的新特征矩阵。

from sklearn.linear_model import Ridge from sklearn.model_selection import TimeSeriesSplit def stacking_predict(X, y, base_models, meta_model, n_splits=5): tscv = TimeSeriesSplit(n_splits=n_splits) oof = np.zeros((len(X), len(base_models))) for fold, (tr_idx, val_idx) in enumerate(tscv.split(X)): for i, model in enumerate(base_models): model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) oof[val_idx, i] = model.predict(X.iloc[val_idx]) meta_model.fit(oof, y) return meta_model, oof

TimeSeriesSplit保证每折验证集都在训练集之后,符合时序逻辑。oof矩阵的列数等于基模型数量,行数等于样本数。元学习器选 Ridge 是因为它不容易过拟合,alpha设 1.0 到 10.0 之间。如果基模型预测尺度差异大,先做标准化再喂给元学习器。融合后的预测还要做后处理:把负值截断到 0,因为出货量不可能为负;对超过历史最大值的预测做封顶,防止促销期外推失控。

3.2 分仓规划:从预测值到发货决策

需求预测输出的是「每个商品在每个仓未来要出多少」,分仓规划要回答的是「每个仓实际备多少、从哪个仓发」。赛题里分仓目标通常是最小化「调拨成本 + 缺货惩罚」。我一般把它建模成运输问题:供给端是各仓库存,需求端是各区域预测需求量,边权是调拨单价。用线性规划或最小费用流求解,小规模直接 scipy 的linprog,大规模上 OR-Tools。

from scipy.optimize import linprog import numpy as np def allocate(warehouse_supply, region_demand, cost_matrix): """ warehouse_supply: 各仓可用库存 list region_demand: 各区域预测需求 list cost_matrix: 仓到区的单位调拨成本,shape=(n_wh, n_region) """ n_wh, n_region = cost_matrix.shape c = cost_matrix.flatten() # 等式约束:每个仓发出的总量 <= 库存 A_ub = [] b_ub = [] for i in range(n_wh): row = np.zeros(n_wh * n_region) row[i*n_region:(i+1)*n_region] = 1 A_ub.append(row) b_ub.append(warehouse_supply[i]) # 等式约束:每个区域收到的总量 = 需求 A_eq = [] b_eq = [] for j in range(n_region): row = np.zeros(n_wh * n_region) row[j::n_region] = 1 A_eq.append(row) b_eq.append(region_demand[j]) res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=(0, None), method='highs') return res.x.reshape(n_wh, n_region)

cost_matrix的构造是分仓规划里最容易被低估的一步。它不只是快递单价,还要叠加时效权重和仓库操作成本。我一般用「基础运费 × 时效系数 + 固定操作费」来算,时效系数按区域远近设 1.0 到 1.5。linprog的method='highs'比默认单纯形快很多,适合几千个变量的规模。如果需求大于总供给,等式约束会无解,这时要把需求约束改成<=并加缺货惩罚项,或者先做一轮需求优先级排序。

4. 避坑与排查:五个让方案翻车的真实原因

4.1 现象:线下 MAE 很低,线上预测全偏

原因通常是特征里混入了未来信息。比如用「当天库存」预测「当天出货」,而库存是出货后更新的,等于偷看了答案。解决方法是把所有特征按时间戳对齐到预测起点之前,用shift或asof合并,并在验证集上做一次「时间穿越检测」:随机打乱日期后 MAE 应该显著变差,如果没变,说明特征没时序信号。

4.2 现象:融合后效果不如单模型

基模型预测相关性太高,融合只是重复投票。先算相关系数矩阵,超过 0.9 的模型只留一个,或者给不同模型喂不同特征子集。另一个原因是元学习器过拟合,把alpha调大或换成简单平均。我一般先跑加权平均,只有它比单模型好才上 Stacking。

4.3 现象:分仓结果出现大量跨区调拨

成本矩阵没设对,或者仓库供给约束写反了。检查A_ub的方向:linprog默认是A_ub @ x <= b_ub,如果写成>=会得到荒谬解。另外确认cost_matrix的对角线(同区调拨)是不是最小的,如果不是,说明基础运费算错了。

4.4 现象:LightGBM 训练到一半报内存不足

num_leaves设太大或max_bin默认 255 导致直方图爆内存。把num_leaves降到 63 以下,max_bin调到 127,或者用feature_fraction做列采样。数据量超过千万行时,先做类别特征编码压缩,把高基数 ID 用 target encoding 或 frequency encoding 替代 one-hot。

4.5 现象:预测值出现负数或极端大值

回归模型没有输出约束。后处理加np.clip(pred, 0, historical_max * 1.5),historical_max取该商品该仓过去 90 天的最大出货量。如果负值比例超过 5%,说明模型选错了,换objective='regression_l1'或加对数变换。

5. 进阶技巧:用滚动预测和分位数回归兜住尾部风险

方案跑通后,我习惯再加两个动作。一是滚动预测:不要只训一次模型用到底,而是每天用最新数据重新训一版,预测未来 7 天。这样能捕捉促销节奏变化,代价是训练时间翻倍,可以用增量训练或只更新最后几棵树来省算力。二是分位数回归,把objective换成quantile,alpha设 0.9,得到 P90 预测值。分仓规划时用 P90 而不是均值来备货,缺货率能降三成,代价是库存周转慢一点。具体选哪个分位数,看业务对缺货和压货的容忍度,我一般 P75 到 P90 之间试。

# 分位数回归示例:预测 P90 出货量 params_q = { 'objective': 'quantile', 'alpha': 0.9, 'metric': 'quantile', 'num_leaves': 63, 'learning_rate': 0.05, 'verbose': -1 } # 训练流程与普通回归一致,预测值直接作为备货上限

验证分位数效果不能只看 MAE,要看「预测值大于真实值的比例」是否接近 0.9。如果只有 0.7,说明模型低估了尾部,要加更多滞后特征或换更长的训练窗口。我一般把均值预测和 P90 预测都存下来,分仓时对高周转商品用 P90,对长尾商品用均值,避免过度备货。

从那以后我每次做完需求预测,都会强制走一遍「时间穿越检测 + 预测相关性检查 + 分仓成本矩阵复核」这三步,少一步都不敢把结果交给运营。希望帮到你。

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

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

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

立即咨询