零售库存预测案例:预测结果要接上补货决策
从一次补货翻车说起
去年我参与了一个连锁便利店的补货系统优化项目。上线前,团队花了三个月做需求预测模型,MAPE 做到了 12%,在离线评估里算不错的数字。结果上线第一周,三家门店的鲜食类商品缺货率飙升到 23%,同时常温饮品的库存周转天数从 15 天涨到了 28 天。
问题出在哪里?预测模型输出的结果直接存进了一张「预测结果表」,然后——就没有然后了。补货决策还是靠门店店长凭经验在 Excel 里算。预测是预测,补货是补货,中间没有人把两者接起来。
这篇文章把我在零售库存预测系统上踩过的坑和学到的经验做一次系统整理。核心观点是:预测只是中间产物,系统设计的终点是补货动作。如果预测结果不能进入补货决策的数据流,模型的精度再高也没有业务价值。
需求预测的基本框架
预测系统在整个供应链中的位置
库存预测不是一个孤立的算法问题,它嵌在「需求感知 → 需求预测 → 库存计划 → 补货执行」这条链路里。Databricks 的零售需求预测参考架构把数据管道分成了三层:Bronze(原始数据接入)、Silver(清洗与标准化)、Gold(生成 SKU/门店/天粒度的需求特征)[1]。每一层的输出质量直接决定下一层的上限。
安全库存不是固定值
安全库存的经典公式是 SS = Z × σd × √LT,其中 Z 对应目标服务水平,σd 是需求标准差,LT 是供应商交期 [2]。这个公式隐含了一个假设:需求服从正态分布,交期固定。实际情况远没有这么理想。
Invent.ai 的一篇分析指出,静态安全库存公式把交期当作常量,忽略了品类差异和突发物流中断 [3]。比如鲜食类的保质期只有 3-7 天,交期的轻微波动就会导致缺货或报废;而常温食品的交期弹性大得多,但需求受促销影响更明显。
现代方案倾向于动态计算安全库存——根据实时销售数据、在途库存和供应商交期波动,每天重新计算每个 SKU 在每个门店的安全水位。这比一次性设好固定值再也不改的做法要靠谱得多。
预测方法的选择
零售场景常用的预测方法可以按复杂度分成几个层级,每种方法的适用场景和局限性不一样:
| 方法类别 | 代表算法 | 适用场景 | 局限性 | 实施复杂度 |
|---|---|---|---|---|
| 统计方法 | 移动平均、指数平滑 | 需求稳定的标品,历史数据充分 | 无法处理促销、天气等外部变量 | 低 |
| 时序分解 | SARIMA、Holt-Winters | 有明显季节性和趋势的品类 | 对突变敏感,需要较多历史数据 | 中 |
| 机器学习 | XGBoost、LightGBM、Random Forest | 多因素影响、促销频繁的品类 | 需要特征工程,可解释性较差 | 中高 |
| 深度学习 | LSTM、Transformer、TFT | 海量 SKU、复杂交互关系 | 数据量要求大,调参成本高 | 高 |
| 混合策略 | 统计 + ML 集成 | 全品类覆盖 | 系统复杂度高,需要路由逻辑 | 高 |
京东零售在 2025 年公开了他们的时序大模型方案——6B 参数的 TimeHF 模型,用 210B 条多场景销售数据训练,覆盖 20,000 款商品的自动补货预测,精度相对提升 33.21% [4]。他们还引入了针对时序预测的 RLHF(基于人类反馈的强化学习)机制,把行业专家的先验知识注入模型权重。这个思路值得关注,但实施门槛也高——对于大多数中小零售企业,从 XGBoost + 特征工程开始,比直接上 6B 大模型务实得多。
案例一:缺货期间的需求被低估
场景
一家连锁便利店有 200 家门店,上线需求预测系统后,发现常温奶品类的预测值持续低于实际销量。门店反馈缺货很严重,但系统显示「需求下降了」。
翻车
根因是数据清洗时,团队把缺货期间的销量直接当作了真实需求。门店在缺货那几天根本无货可卖,POS 数据记的是 0 或者很低的数字。模型学到的是「这个商品需求在下降」,于是给出的预测越来越保守,形成了一个恶性循环:
缺货 → POS 销量低 → 模型预测更低 → 补货量减少 → 继续缺货修复
解决方案是在数据层增加「缺货标记」和「需求还原」逻辑。当某天的门店库存为 0 或接近 0,且 POS 销量异常低于近期均值时,系统把这天标记为「疑似缺货」,用同期同类门店的销量做插值还原。
# 坏做法:直接拿 POS 销量当需求
def get_demand(pos_sales):
"""POS 销量直接当需求,缺货期间会严重低估"""
return pos_sales # 缺货日销量为 0,直接喂给模型# 好做法:识别缺货期,用参照组还原需求
def get_demand(pos_sales, store_inventory, peer_avg_sales):
"""
识别缺货日,用同区域门店均值还原需求。
缺货判断:库存 <= 安全库存的 10% 且销量 < 近 7 天均值的 30%
"""
demand = pos_sales.copy()
safety_threshold = store_inventory.quantile(0.1)
is_stockout = (
(store_inventory <= safety_threshold) &
(pos_sales < peer_avg_sales * 0.3)
)
# 缺货日用参照组均值替代
demand[is_stockout] = peer_avg_sales[is_stockout]
return demand这个逻辑听起来简单,但实际落地时需要考虑几个细节:
| 判断维度 | 阈值建议 | 说明 |
|---|---|---|
| 库存水位 | ≤ 安全库存的 10% | 太低可能是盘点差异,不一定是真缺货 |
| 销量偏离 | < 近期均值的 30% | 太严格会漏掉部分缺货日 |
| 参照组选择 | 同区域、同商圈类型门店 | 跨城市对比没有意义 |
| 还原上限 | 不超过参照组均值的 150% | 防止异常高值污染训练集 |
案例二:促销预测和常规预测用同一套模型
场景
一家中型超市的预测系统对日常销量的预测还算准确,但每次促销活动前,补货量总是不对——要么备多了导致活动结束后大量退货,要么备少了活动第二天就断货。
翻车
团队最初的做法是:把促销当作一个二值特征 is_promotion 扔进模型。模型只学到了「促销期间销量会高一些」,但具体高多少、不同促销力度的差异、促销前囤货效应和促销后回落,全部捕捉不到。
问题在于,促销对销量的影响不是一个固定倍率。买一送一和满减 10 元的拉动效果完全不同;促销前一周消费者可能减少购买等促销(前置效应);促销结束后需求会回落甚至低于基线(后置效应)。一个二值特征根本装不下这些信息。
修复
重构特征工程,把促销信息拆解成多个维度,并且引入促销前后的时间窗口特征。
# 坏做法:促销只用一个二值特征
def build_features(df):
"""简单二值化,丢失促销力度和时间效应"""
df['is_promotion'] = df['promotion_type'].notna().astype(int)
return df# 好做法:促销特征多维拆解 + 时间窗口
def build_promotion_features(df):
"""
将促销信息拆解为力度、类型、前后窗口多个特征。
让模型能区分不同促销策略的效果差异。
"""
# 促销力度编码:0=无, 1=满减, 2=折扣, 3=买赠
df['promo_intensity'] = df['promotion_type'].map(
{'none': 0, 'discount_10': 1, 'discount_20': 2, 'bogo': 3}
).fillna(0)
# 促销前囤货效应:距促销开始的天数(-7 到 -1)
df['days_to_promo'] = (
(df['promo_start'] - df['date']).dt.days.clip(-7, -1)
)
# 促销后回落期:距促销结束的天数(0 到 7)
df['days_after_promo'] = (
(df['date'] - df['promo_end']).dt.days.clip(0, 7)
)
# 无促销时这两个特征置空
df.loc[df['days_to_promo'] > 0, 'days_to_promo'] = np.nan
df.loc[df['days_after_promo'] < 0, 'days_after_promo'] = np.nan
return df效果对比:
| 指标 | 改造前 | 改造后 | 变化 |
|---|---|---|---|
| 促销期 MAPE | 38% | 19% | -19pp |
| 促销后退货率 | 12% | 5% | -7pp |
| 活动缺货率 | 27% | 11% | -16pp |
| 模型可解释性 | 差(单一特征) | 好(特征重要性可归因) | 定性提升 |
案例三:预测结果和补货决策之间断开了
场景
开头提到的那个项目。模型精度不错,但门店店长不用预测结果。他们有自己的 Excel 模板,按「过去 7 天销量 × 1.2」来估算补货量。
翻车
系统输出的是「未来 7 天预测销量」,但门店补货需要的是「建议补货量」。这两者之间差了安全库存计算、在途库存扣减、最小包装量取整、门店陈列容量约束等一系列业务规则。没有人在中间做这层转换,预测结果就变成了一张没人看的表。
修复
我们在预测模型和门店补货之间加了一层「补货决策引擎」,把预测结果转化为可执行的补货建议。
# 坏做法:预测结果直接输出,不管下游怎么用
def output_forecast(forecast_model, sku, store, horizon):
"""只输出预测值,不接补货逻辑"""
prediction = forecast_model.predict(sku, store, horizon)
return {'sku': sku, 'store': store, 'forecast': prediction}
# 然后存到数据库,等着别人来用——但没人来用# 好做法:预测结果接入补货决策引擎
def generate_replenishment(forecast, current_stock, in_transit,
safety_stock, min_order_qty, shelf_capacity):
"""
从预测值到补货建议的完整转换。
考虑在途库存、安全库存、最小订货量和货架容量。
"""
# 预测期总需求
demand = forecast['prediction'].sum()
# 可用库存 = 现有库存 + 在途 - 安全库存
available = current_stock + in_transit - safety_stock
# 净需求量
net_need = max(0, demand - available)
# 最小订货量取整
order_qty = math.ceil(net_need / min_order_qty) * min_order_qty
# 不超过货架容量(门店不能堆满仓库)
order_qty = min(order_qty, shelf_capacity - current_stock)
return {
'sku': forecast['sku'],
'store': forecast['store'],
'order_qty': max(0, order_qty),
'reason': f"预测需求{demand}, 可用{available}, 缺口{net_need}"
}补货决策引擎的核心参数:
| 参数 | 含义 | 典型取值 | 影响 |
|---|---|---|---|
| 预测 horizon | 预测覆盖天数 | 7-14 天 | 太短无法覆盖交期,太长预测精度下降 |
| 安全库存系数 | 对应目标服务水平的 Z 值 | 1.28(90%)/ 1.65(95%) | 越高库存越大但缺货越少 |
| 最小订货量 | 供应商包装约束 | 按品类不同,6/12/24 | 避免拆箱,但可能导致多订 |
| 货架容量 | 门店物理空间限制 | 按品类和门店不同 | 防止补货过多无处存放 |
人工调整要可记录、可追踪
模型不可能知道所有信息。门店附近的临时活动、道路施工、隔壁新店开业、竞品关店——这些「模型不知道的信息」掌握在一线运营手里。系统要允许人工调整预测值,关键是调整必须留痕。
我们在系统里设计了一个「人工覆盖」表:
override_id | sku | store | date | model_value | human_value | reason | created_by每条调整记录都带有原因字段和责任人。这样做有两个好处:一是事后可以复盘调整是否合理;二是这些「人工标签」可以反哺模型——比如发现某位店长总是高估活动效果,后续可以在模型里校准。
| 调整类型 | 典型原因 | 调整幅度 | 频率 |
|---|---|---|---|
| 活动调整 | 社区活动、节庆 | +20% ~ +100% | 低频,可预期 |
| 异常扣减 | 施工、封路 | -30% ~ -80% | 低频,突发 |
| 竞品影响 | 新店开业、促销 | -10% ~ -30% | 中频 |
| 供应链异常 | 供应商断供 | 手动清零到货预期 | 极低频 |
指标体系:从预测精度到业务结果
很多团队把 MAPE、RMSE 当作预测系统的核心指标。这些指标衡量的是模型本身好不好,但业务关心的是另一个问题:库存预测系统到底有没有让缺货率下降、周转天数缩短、毛利提升?
我把指标分成三层:
| 指标层级 | 具体指标 | 衡量对象 | 谁关心 |
|---|---|---|---|
| 模型层 | MAPE、RMSE、Bias | 预测精度 | 算法团队 |
| 决策层 | 补货建议采纳率、人工覆盖率 | 预测到决策的转化 | 产品/运营 |
| 业务层 | 缺货率、库存周转天数、滞销率、毛利影响 | 经营结果 | 业务负责人 |
一个常见的陷阱是:模型层的 MAPE 在改善,但业务层的缺货率没有变化。原因可能是预测到补货的中间环节有问题——安全库存参数不合理、最小订货量太大、补货频率跟不上需求变化。所以三层指标必须同时看,不能只看模型指标就宣布成功。
上线检查清单
经过几个项目的迭代,我整理了一份库存预测系统上线前的检查清单,按阶段分组:
数据准备阶段
- 缺货期间的销量已识别并用参照组还原,不存在把缺货当作需求下降的情况
- 促销信息已拆解为多维度特征(力度、类型、前后窗口),而非二值标记
- 门店主数据(面积、商圈类型、配送路径)与 SKU 主数据已对齐,无孤立记录
- 历史数据至少覆盖 1 年,包含完整季节性周期;新门店有至少 8 周数据才纳入预测
模型与特征阶段
- 按品类分组训练或选择模型,生鲜/常温/日用不共用同一套超参
- Bias(偏差)指标已纳入监控——系统性的预测偏高和偏低对库存的影响方向不同
- 模型更新频率已确定:生鲜类日更,常规范周更,并在数据漂移时触发重训
- 新 SKU 的冷启动方案已确定(同类均值、相似品映射或人工设定初始水位)
决策与执行阶段
- 预测结果到补货建议的转换逻辑已实现,包含安全库存、在途、最小订货量、货架容量
- 人工调整入口已开放,每条调整带有原因、责任人和时间戳
- 补货建议的采纳率监控已配置,采纳率低于 60% 的门店需要排查原因
- 业务层指标(缺货率、周转天数、滞销率)的看板和告警已上线
上线后运营
- 每周回顾预测偏差最大的 Top 20 SKU,区分是数据问题、模型问题还是业务异常
- 月度复盘人工调整记录,识别可以沉淀为规则或模型特征的调整模式
- 季度评估安全库存参数是否需要根据实际交期波动和需求变异重新校准
小结
零售库存预测系统不是一个纯算法项目,它的难点在「接上」——数据清洗要接上业务语义(缺货不是需求下降),预测模型要接上促销和季节的外部信息,预测结果要接上补货决策引擎的安全库存和包装约束,人工调整要接上模型迭代的反馈闭环。
每一层「接上」都可能导致价值漏损。我见过的失败案例里,很少有模型本身不行就放弃的,大部分是接上环节出了问题。做系统设计时,从补货动作倒推数据需求,比从模型能力正推输出结果要靠谱。
参考资料
[1] Databricks. Retail Demand Forecasting Reference Architecture. 2025.
[2] Slimstock. Safety Stock Inventory Q&A. 2024.
[3] Invent.ai. Safety Stock Management for Retail: Moving Beyond Static Formulas. 2026.
[4] 京东零售数据团队. 京东零售首次公开!6B参数时序大模型实现20000款商品自动补货预测. 2025.
[5] 供应链管理专栏. 安全库存的设置:库存计划的看家本领. 2023.
[6] Omniful AI. Advanced Inventory Forecasting Methods: Machine Learning and Beyond. 2025.
[7] 知乎专栏. 对供应链第一道防线《需求预测》的实践与总结. 2023.