零售行业仓库商品数据标记

一、业务场景与目标说明

1.1 核心问题

在零售仓配场景中,同一个 SKU 在不同时间、不同仓库会处于不同的运营状态,比如:

  • 刚上新+强铺货:集中铺到大量门店,出库门店数和出库量突然放大;
  • 短暂下架:仓里有货,但被暂停销售、系统下架、或临时停采,短期内无出库;
  • 长期下架:商品不再运营,长时间没有出库,库存为 0 或维持极低且不流动;
  • 缺货:库存为 0 或极低,但从历史看有稳定销量,说明供应跟不上需求。

目标是:
利用所有历史数据,自动识别每一天、每个仓、每个商品所处的状态,为补货、上新评估、陈列管理等提供基础标签。


二、数据与粒度设计

2.1 基础数据粒度(事实表)

建议以日粒度的出入库、库存明细作为基础事实表,每条记录至少包含:

  • date:日期(按自然日)
  • warehouse_id:仓库 ID
  • sku_id:商品 ID
  • cate_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),按日期排序,计算:

  1. 短期销量/门店数

    • roll_7d_outbound_qty:最近 7 日出库量之和
    • roll_7d_outbound_store_cnt:最近 7 日有出库门店总数(可以按天求去重后再求和)
  2. 中长期销量

    • roll_30d_outbound_qty:最近 30 日出库量之和
    • roll_30d_outbound_store_cnt:最近 30 日有出库门店数之和
  3. 库存与入库

    • roll_7d_inbound_qty:最近 7 日入库量之和
    • roll_30d_inbound_qty:最近 30 日入库量之和
  4. 销量统计特征(历史期)

可以基于较长时间窗口(比如所有历史、或最近 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 强铺货特征

在上新期中,如果满足以下任一类的"超常放大"特征,即可标记为"上新强铺货":

  1. 门店数显著高

    text 复制代码
    outbound_store_cnt(t) > hist_store_cnt_median + K1 * hist_store_cnt_MAD
    • K1 取 2~3,根据品类调整。
  2. 出库量显著高

    text 复制代码
    outbound_qty(t) > hist_outbound_median + K2 * hist_outbound_MAD

    或使用分位数:

    text 复制代码
    outbound_qty(t) > hist_outbound_Q90 (历史 90% 分位)
  3. 入库量显著高(如果有 / 或用 inbound 推算)

    text 复制代码
    inbound_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)

  1. 找到所有日区间 [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
  2. 在区间前后存在出库:

    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 天或按品类配置。
  3. 对符合条件的每个日期 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 缺货识别的要素
  1. 当前库存为 0 或接近 0

    text 复制代码
    inventory_qty(t) <= 安全库存阈值 I_safe(通常 0 或很小)
  2. 当前无出库或出库远低于历史水平

    text 复制代码
    outbound_qty(t) = 0

    或者:

    text 复制代码
    outbound_qty(t) << hist_outbound_mean (比如 < 0.2 * hist_outbound_mean)
  3. 近期有动销/需求

    防止一些本来就卖不动的"慢死货被误判缺货":

    • 滑动窗口销量:

      text 复制代码
      roll_30d_outbound_qty(t-1) > V_min
    • 或滑动窗口出库门店数:

      text 复制代码
      roll_30d_outbound_store_cnt(t-1) > S_min
  4. 可选:门店层面的未满足订单/缺货报表

    如有门店订单数据,可以叠加:

    text 复制代码
    store_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_minS_min 由全局或品类统计确定(比如,定义"动销 SKU"为过去 90 天至少有 10 单销量、至少覆盖 3 家门店)。

五、状态优先级与冲突消解

同一天同一 SKU 可能满足多种规则(例如:既库存为 0 且长期无销量,同时刚刚过了上新期),需要定义全局优先级,保证每天只有一个最终标签。

建议优先级(从高到低):

  1. 缺货(最直接影响销售和消费者体验)
  2. 上新强铺货(新品运营阶段)
  3. 短暂下架
  4. 长期下架
  5. 正常

具体实现上,判定流程可以写成:

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_qty
  • roll_7d_outbound_store_cnt / roll_30d_outbound_store_cnt
  • roll_7d_inbound_qty / roll_30d_inbound_qty
  • 历史统计量:hist_*(可先做一个按 sku+仓聚合的结果表)

步骤 2:识别上新期与上新强铺货

对每个 (warehouse_id, sku_id)

  1. 找出首销日期 first_sell_date(最早 outbound_qty>0 的 date);
  2. 从品类参数表查出 T_new
  3. [first_sell_date, first_sell_date + T_new - 1] 内,判断每天是否为"强铺货"(是否超过阈值):
    • 若是,则 label_candidate(t, "上新强铺货") = 1

步骤 3:识别下架区间

  1. 寻找连续 outbound_qty = 0 的区间;
  2. 对每个区间,根据区间长度 L 判断是"短暂下架候选"还是"长期下架候选";
  3. 短暂下架候选:
    • 要求区间内库存 > 0;
    • 区间前 P 天和后 Q 天存在出库;
    • 满足则区间内所有 t 标记 label_candidate(t, "短暂下架") = 1
  4. 长期下架候选:
    • L >= M(品类参数);
    • 区间内库存为 0 或极低且无入库;
    • 区间开始前有历史动销;
    • 满足则区间内所有 t 标记 label_candidate(t, "长期下架") = 1

实现上可以先通过窗口函数给"连续 0 销量"段分段,例如利用"连续区间 ID"的套路(按累加分组)。

步骤 4:每日缺货判断

对每个 (warehouse_id, sku_id, date=t)

  1. 从品类参数表取 I_safeαV_minS_min
  2. 根据前文缺货公式判断是否为"缺货";
  3. 若是,则 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=0outbound_qty=0
  • 可以给这段之后标记为"清仓后下架",但严格来说已经属于长期下架扩展。

8.3 数据缺失与异常峰值

  • 对明显的异常峰值(极大或极小),可以通过 MAD 或分位数方法提前进行 winsorize(截断);
  • 对缺失库存数据,可根据相邻几天的库存和出入库推算填补;实在无法合理推断的日期,可以不给状态或标为 未知

九、扩展:从规则到模型(可选思路)

当前设计是一套纯规则+统计阈值的逻辑,优点是:

  • 解释性强、可调参;
  • 便于和业务一起 review;

如果数据规模大、特征维度多,可以进一步演化为:

  1. 先用这套规则在历史数据上自动打标签(弱监督标签);
  2. 再用这些标签训练一个分类模型(例如 XGBoost、LightGBM);
  3. 输入特征包括:实时库存、入库、出库、门店数、品类、价格带、促销标记等;
  4. 模型可以学到更复杂的非线性规律,对规则边界附近情况更鲁棒。
相关推荐
confiself34 分钟前
通义灵码分析ms-swift框架中CHORD算法实现
开发语言·算法·swift
做怪小疯子37 分钟前
LeetCode 热题 100——二叉树——二叉树的层序遍历&将有序数组转换为二叉搜索树
算法·leetcode·职场和发展
CoderYanger1 小时前
递归、搜索与回溯-记忆化搜索:38.最长递增子序列
java·算法·leetcode·1024程序员节
xlq223222 小时前
22.多态(下)
开发语言·c++·算法
CoderYanger2 小时前
C.滑动窗口-越短越合法/求最长/最大——2958. 最多 K 个重复元素的最长子数组
java·数据结构·算法·leetcode·哈希算法·1024程序员节
却话巴山夜雨时i3 小时前
394. 字符串解码【中等】
java·数据结构·算法·leetcode
haing20193 小时前
使用黄金分割法计算Bezier曲线曲率极值的方法介绍
算法·黄金分割
leoufung3 小时前
LeetCode 230:二叉搜索树中第 K 小的元素 —— 从 Inorder 遍历到 Order Statistic Tree
算法·leetcode·职场和发展
jyyyx的算法博客3 小时前
多模字符串匹配算法 -- 面试题 17.17. 多次搜索
算法