一、业务场景与目标说明
1.1 核心问题
在零售仓配场景中,同一个 SKU 在不同时间、不同仓库会处于不同的运营状态,比如:
- 刚上新+强铺货:集中铺到大量门店,出库门店数和出库量突然放大;
- 短暂下架:仓里有货,但被暂停销售、系统下架、或临时停采,短期内无出库;
- 长期下架:商品不再运营,长时间没有出库,库存为 0 或维持极低且不流动;
- 缺货:库存为 0 或极低,但从历史看有稳定销量,说明供应跟不上需求。
目标是:
利用所有历史数据,自动识别每一天、每个仓、每个商品所处的状态,为补货、上新评估、陈列管理等提供基础标签。
二、数据与粒度设计
2.1 基础数据粒度(事实表)
建议以日粒度的出入库、库存明细作为基础事实表,每条记录至少包含:
date:日期(按自然日)warehouse_id:仓库 IDsku_id:商品 IDcate_1 / cate_2 / cate_3:一级/二级/三级品类(方便后面按品类调阈值)inventory_qty:当日期末库存数量(或日均库存)outbound_qty:当日出库量(所有门店合计)outbound_store_cnt:当日有出库行为的门店数(去重)- (可选)
inbound_qty:当日入库量
若没有
inbound_qty,可后续用库存变化进行推算。
2.2 分析粒度(打标粒度)
一条记录 = 仓库 × SKU × 日期
这是打状态标签的基本单元。后续一切逻辑都以 (warehouse_id, sku_id, date) 为唯一键。
多级品类(cate_1/2/3)不会改变粒度,而是用来确定阈值和规则(不同品类的生命周期/周转速度不同)。
三、基础指标和特征计算
在打标签之前,需要先算一些"滑动指标"和"统计特征",这些指标用于判断"异常"。
3.1 库存变化与入库推算
如果没有入库字段 inbound_qty,可用库存差和出库回推"隐式入库量":
text
inbound_qty(t) = max( inventory_qty(t) - inventory_qty(t-1) + outbound_qty(t), 0 )
解释:
- 库存变化 = 昨日库存 + 入库 - 出库。
- 整理得:入库 = 今日库存 - 昨日库存 + 出库。
- 负值直接截断为 0,避免数据噪音导致"负入库"。
这一步是为了捕捉"上新/铺货时入库量的突然放大"。
3.2 滑动窗口指标(Rolling Features)
对每个 (warehouse_id, sku_id),按日期排序,计算:
-
短期销量/门店数
roll_7d_outbound_qty:最近 7 日出库量之和roll_7d_outbound_store_cnt:最近 7 日有出库门店总数(可以按天求去重后再求和)
-
中长期销量
roll_30d_outbound_qty:最近 30 日出库量之和roll_30d_outbound_store_cnt:最近 30 日有出库门店数之和
-
库存与入库
roll_7d_inbound_qty:最近 7 日入库量之和roll_30d_inbound_qty:最近 30 日入库量之和
-
销量统计特征(历史期)
可以基于较长时间窗口(比如所有历史、或最近 90/180 天)计算每个 SKU 在各仓的统计量:
hist_outbound_mean:历史平均日出库量(排除 0 日可选)hist_outbound_std:历史日出库量标准差hist_outbound_median:历史日出库量中位数hist_outbound_MAD:基于中位数的绝对偏差(Median Absolute Deviation)hist_store_cnt_median:历史出库门店数中位数hist_store_cnt_MAD:门店数 MAD
这些统计量用于自动形成"上新强铺货"的阈值和"常规动销"的判定标准。
四、四类状态的业务定义和数学化规则
4.1 上新强铺货(New & Strong Push)
业务理解 :
该 SKU 在该仓刚开始销售,处于集中铺货阶段,短时间内对大量门店发货,出库量和出库门店数明显高于其后续常态水平。
4.1.1 新品识别(上新起点)
对每个 (warehouse_id, sku_id):
text
首销日期 first_sell_date = 最早 outbound_qty > 0 的 date
上新期长度 T_new = 按品类设定,常见 7--14 天(生鲜可以短一点)
在 [first_sell_date, first_sell_date + T_new - 1] 这个时间窗口内,是候选"上新期"。
4.1.2 强铺货特征
在上新期中,如果满足以下任一类的"超常放大"特征,即可标记为"上新强铺货":
-
门店数显著高
textoutbound_store_cnt(t) > hist_store_cnt_median + K1 * hist_store_cnt_MAD- K1 取 2~3,根据品类调整。
-
出库量显著高
textoutbound_qty(t) > hist_outbound_median + K2 * hist_outbound_MAD或使用分位数:
textoutbound_qty(t) > hist_outbound_Q90 (历史 90% 分位) -
入库量显著高(如果有 / 或用 inbound 推算)
textinbound_qty(t) > hist_inbound_median + K3 * hist_inbound_MAD
最终规则(示例):
text
若 t 在 [first_sell_date, first_sell_date + T_new - 1] 内,
且 (门店数显著高 OR 出库量显著高 OR 入库量显著高),
则状态 = "上新强铺货"。
可细化为:
- 若只有在上新期的最前几天(如前 3--5 天)放大量,其余趋于平稳,则集中标记前几天为"上新强铺货",之后转"正常上架"。
4.2 短暂下架(Short-term Off-shelf)
业务理解 :
商品本身仍在运营,但因为系统/运营决策/合规检查等原因,在短期内停止出库;通常仓里还有库存,而且下架前后都有正常出库。
4.2.1 关键特征
- 一段 不太长 的连续天数,
outbound_qty = 0; - 这段时间内
inventory_qty > 0(仓里有货却不卖); - 在这段时间的前后有一段时间存在出库(说明只是一段时间的暂停)。
4.2.2 形式化规则
设:
- 短暂下架天数区间:
- 最小天数
K_min,比如 2~3 天; - 最大天数
K_max,比如 10 天(超过就趋向"长期下架")。
- 最小天数
对每个 (warehouse_id, sku_id):
-
找到所有日区间
[t_start, t_end]满足:text对于 t ∈ [t_start, t_end]: outbound_qty(t) = 0 AND inventory_qty(t) > 0 区间长度 L = t_end - t_start + 1 K_min ≤ L ≤ K_max -
在区间前后存在出库:
text在 [t_start - P, t_start - 1] 这 P 天中,存在 t',使 outbound_qty(t') > 0 并且 在 [t_end + 1, t_end + Q] 这 Q 天中,存在 t'',使 outbound_qty(t'') > 0- P,Q 可取 7 天或按品类配置。
-
对符合条件的每个日期 t ∈ [t_start, t_end],打标:
text状态 = "短暂下架"
4.3 长期下架(Long-term Off-shelf)
业务理解 :
该 SKU 基本从该仓的货架/运营体系里撤掉了,长时间无法购买或不再补货。通常表现为:
- 很长时间
outbound_qty = 0; - 库存为 0 或极低且不再有入库;
- 前段时间曾经有较明显的销量记录。
4.3.1 阈值依赖品类
不同品类生命周期差异巨大,建议按品类配置长期下架识别天数 M:
示例:
| 一级品类 | 短暂下架上限 K_max | 长期下架阈值 M |
|---|---|---|
| 生鲜 | 3--5 天 | 7--14 天 |
| 食品杂货 | 7--10 天 | 30 天 |
| 日化百货 | 10--14 天 | 30--60 天 |
4.3.2 形式化规则
对每个 (warehouse_id, sku_id),寻找满足:
text
存在连续 M 天区间 [t_start, t_end],对所有 t ∈ [t_start, t_end]:
outbound_qty(t) = 0
并且 (以下任一成立)
1)库存长期为 0 型:
inventory_qty(t) = 0 对所有 t ∈ [t_start, t_end]
2)库存极低且无入库型:
inventory_qty(t) ≤ I_low (小阈值,如 1 或 2)
且 inbound_qty(t) ≈ 0 (可以用 roll_30d_inbound_qty 很小来判断)
3)长期无流动型:
inbound_qty(t) ≈ 0 且库存几乎不变化(|inventory_qty(t) - inventory_qty(t-1)| ≤ δ)
同时,确保在 [t_start - H, t_start - 1] 这段时间内,商品曾经有稳定销量:
text
roll_30d_outbound_qty(t_start - 1) > V_min
否则可能本来就是"从未真正上架过"的死货,不必标记为下架,而是"从未推广"。
对满足的每个日期 t ∈ [t_start, t_end],打:
text
状态 = "长期下架"
4.4 缺货(Out-of-stock)
业务理解 :
该 SKU 市场上有需求(历史看是动销商品),但此刻仓库库存为 0(或远低于合理安全库存),且不再出库,属于供应跟不上需求。
缺货通常是优先级最高的运营问题。
4.4.1 缺货识别的要素
-
当前库存为 0 或接近 0:
textinventory_qty(t) <= 安全库存阈值 I_safe(通常 0 或很小) -
当前无出库或出库远低于历史水平:
textoutbound_qty(t) = 0或者:
textoutbound_qty(t) << hist_outbound_mean (比如 < 0.2 * hist_outbound_mean) -
近期有动销/需求
防止一些本来就卖不动的"慢死货被误判缺货":
-
滑动窗口销量:
textroll_30d_outbound_qty(t-1) > V_min -
或滑动窗口出库门店数:
textroll_30d_outbound_store_cnt(t-1) > S_min
-
-
可选:门店层面的未满足订单/缺货报表
如有门店订单数据,可以叠加:
textstore_order_qty(t) > 0 但 outbound_qty(t) = 0
4.4.2 形式化规则
对 (warehouse_id, sku_id, date=t):
text
if inventory_qty(t) <= I_safe
AND ( outbound_qty(t) = 0
OR outbound_qty(t) < α * hist_outbound_mean )
AND ( roll_30d_outbound_qty(t-1) > V_min
OR roll_30d_outbound_store_cnt(t-1) > S_min ):
状态 = "缺货"
其中:
I_safe通常可取 0 或基于品类设置安全库存水平;α取 0.1~0.3;V_min、S_min由全局或品类统计确定(比如,定义"动销 SKU"为过去 90 天至少有 10 单销量、至少覆盖 3 家门店)。
五、状态优先级与冲突消解
同一天同一 SKU 可能满足多种规则(例如:既库存为 0 且长期无销量,同时刚刚过了上新期),需要定义全局优先级,保证每天只有一个最终标签。
建议优先级(从高到低):
- 缺货(最直接影响销售和消费者体验)
- 上新强铺货(新品运营阶段)
- 短暂下架
- 长期下架
- 正常
具体实现上,判定流程可以写成:
text
for 每个 warehouse_id, sku_id, date=t:
if 满足缺货条件:
label(t) = "缺货"
else if 满足上新强铺货条件:
label(t) = "上新强铺货"
else if 满足短暂下架条件:
label(t) = "短暂下架"
else if 满足长期下架条件:
label(t) = "长期下架"
else:
label(t) = "正常"
注意 :
长期下架和短暂下架本质上是一个"区间属性",可以先整体识别区间,再回填到每天。
但在最终逐日判断时按上述优先级覆盖即可。
六、多仓、多级品类的参数管理
6.1 多仓(多个仓库)
多仓不需要修改规则逻辑,只需要确保所有计算都在 (warehouse_id, sku_id) 的分组内独立进行:
- 滑动窗口、历史统计量,都以"仓+SKU"为单位;
- 这样同一 SKU 在不同仓可以有完全不同的生命周期和状态。
伪代码示意:
text
按 (warehouse_id, sku_id) 分组:
按 date 排序
计算 rolling 指标 + 历史统计指标
计算首销日期 first_sell_date
扫描日期序列进行状态判断打标
6.2 多级品类(cate_1 / cate_2 / cate_3)
多级品类主要用于配置不同的阈值,例如:
- 上新期长度
T_new; - 短暂下架最短/最长天数
K_min, K_max; - 长期下架阈值天数
M; - 缺货安全库存
I_safe; - 动销判断阈值
V_min, S_min; - 强铺货判断时阈值倍数
K1, K2, K3。
可以设计一张"品类参数表",例:
| cate_1 | cate_2 | cate_3 | T_new | K_min | K_max | M | I_safe | V_min | S_min | α(缺货倍率) |
|---|---|---|---|---|---|---|---|---|---|---|
| 生鲜 | 水果 | 进口水果 | 5 | 2 | 5 | 10 | 0 | 5 | 3 | 0.2 |
| 生鲜 | 肉禽蛋 | 冷鲜肉 | 3 | 2 | 5 | 7 | 0 | 5 | 3 | 0.2 |
| 食品 | 饮料 | 碳酸饮料 | 10 | 3 | 10 | 30 | 2 | 10 | 5 | 0.3 |
| 日化 | 清洁 | 洗衣液 | 14 | 5 | 14 | 60 | 2 | 8 | 4 | 0.3 |
在计算时,根据 SKU 的 cate_1/2/3 去匹配对应类别的参数。
七、整体实施流程(端到端)
这里给一个可直接翻译成 SQL/Python 的流程框架(按逻辑步骤描述)。
步骤 0:准备基础日表
从原始业务系统中抽取或汇总出日粒度的库存+出库表:
text
fact_daily_sku_warehouse
(
date,
warehouse_id,
sku_id,
cate_1, cate_2, cate_3,
inventory_qty,
outbound_qty,
outbound_store_cnt
)
如果有入库表,可 join 出 inbound_qty;如果没有,后续根据库存变化自动推算。
步骤 1:按仓+SKU 计算时间序列特征
对 (warehouse_id, sku_id) 分组,按 date 排序,计算:
prev_inventory_qty(上一日库存)inbound_qty(根据公式推算)roll_7d_outbound_qty/roll_30d_outbound_qtyroll_7d_outbound_store_cnt/roll_30d_outbound_store_cntroll_7d_inbound_qty/roll_30d_inbound_qty- 历史统计量:
hist_*(可先做一个按 sku+仓聚合的结果表)
步骤 2:识别上新期与上新强铺货
对每个 (warehouse_id, sku_id):
- 找出首销日期
first_sell_date(最早 outbound_qty>0 的 date); - 从品类参数表查出
T_new; - 在
[first_sell_date, first_sell_date + T_new - 1]内,判断每天是否为"强铺货"(是否超过阈值):- 若是,则
label_candidate(t, "上新强铺货") = 1。
- 若是,则
步骤 3:识别下架区间
- 寻找连续
outbound_qty = 0的区间; - 对每个区间,根据区间长度 L 判断是"短暂下架候选"还是"长期下架候选";
- 短暂下架候选:
- 要求区间内库存 > 0;
- 区间前 P 天和后 Q 天存在出库;
- 满足则区间内所有 t 标记
label_candidate(t, "短暂下架") = 1。
- 长期下架候选:
- L >= M(品类参数);
- 区间内库存为 0 或极低且无入库;
- 区间开始前有历史动销;
- 满足则区间内所有 t 标记
label_candidate(t, "长期下架") = 1。
实现上可以先通过窗口函数给"连续 0 销量"段分段,例如利用"连续区间 ID"的套路(按累加分组)。
步骤 4:每日缺货判断
对每个 (warehouse_id, sku_id, date=t):
- 从品类参数表取
I_safe、α、V_min、S_min; - 根据前文缺货公式判断是否为"缺货";
- 若是,则
label_candidate(t, "缺货") = 1。
步骤 5:按优先级合成最终状态
对每个 (warehouse_id, sku_id, date=t),基于候选标签和优先级做决策:
text
if label_candidate(t, "缺货") == 1:
label_final(t) = "缺货"
elif label_candidate(t, "上新强铺货") == 1:
label_final(t) = "上新强铺货"
elif label_candidate(t, "短暂下架") == 1:
label_final(t) = "短暂下架"
elif label_candidate(t, "长期下架") == 1:
label_final(t) = "长期下架"
else:
label_final(t) = "正常"
最终得到一张状态表:
text
sku_warehouse_daily_status
(
date,
warehouse_id,
sku_id,
cate_1, cate_2, cate_3,
status -- {上新强铺货, 短暂下架, 长期下架, 缺货, 正常}
)
八、异常数据与边界情况处理
8.1 从未销售过的 SKU
若一个 (仓, SKU) 从历史到现在 outbound_qty 始终为 0:
- 如果库存也一直为 0:可以视为"从未上架",通常可以直接给"未上架/无效 SKU"标签,不进入上述 4 类;
- 如果曾经有库存但始终没卖出去:可以视为"无动销",也可以归到"正常"或单独一类"僵尸 SKU"。
这一类可在流程前单独识别出来,避免误判为长期下架或短暂下架。
8.2 清仓尾货场景
在长期下架前,有时会出现一个短期内突然高销量、之后直接归零且不再补货,这属于"清仓"。
如果需要区分,可增加一类规则(可选):
- 在某个日期附近,
outbound_qty(t)短期内显著高于历史; - 随后立即长时间
inventory_qty=0且outbound_qty=0; - 可以给这段之后标记为"清仓后下架",但严格来说已经属于长期下架扩展。
8.3 数据缺失与异常峰值
- 对明显的异常峰值(极大或极小),可以通过
MAD或分位数方法提前进行 winsorize(截断); - 对缺失库存数据,可根据相邻几天的库存和出入库推算填补;实在无法合理推断的日期,可以不给状态或标为
未知。
九、扩展:从规则到模型(可选思路)
当前设计是一套纯规则+统计阈值的逻辑,优点是:
- 解释性强、可调参;
- 便于和业务一起 review;
如果数据规模大、特征维度多,可以进一步演化为:
- 先用这套规则在历史数据上自动打标签(弱监督标签);
- 再用这些标签训练一个分类模型(例如 XGBoost、LightGBM);
- 输入特征包括:实时库存、入库、出库、门店数、品类、价格带、促销标记等;
- 模型可以学到更复杂的非线性规律,对规则边界附近情况更鲁棒。