SCENARIO ① · DRILL-DOWN 库存压不住了,老板要个说法

老板问一句话,答案穿过五层:场景 → 三句话 → 报告页 → 规则引擎 → Excel 单元格。

这一页是总览。后面三页把每句话单独拆到底——每个数字都能回溯到某一个 Excel 列、某一行代码、某一条公式。经得起财务总监追问。

场景 ① · 季度末盘点前,老板突然问:"为什么账上躺着这么多钱?"
你拿出一份脱敏样例,三句话说清楚:钱压在哪、浪费在哪、先动哪。不许翻表,不许说"我回去查查"。
1
钱压在哪 → 压在死货和头部
总库存 ¥2,966,900,其中 28%(¥831,000)是死货;头部 5% 的 SKU 占了 51.2% 的销售额——钱两头压。
2
浪费在哪 → 浪费在一刀切的 45 天备货
公式说中位 17.8 天就够(范围 9.5~27),你一刀切备 45 天——¥943,686 资金压在仓库睡大觉。
3
先动哪 → 先补 1248、先清 366、先裁决 34
1,248 个缺货风险先补货、366 个死货先清仓、34 个"死货∩主力"打架的先裁决——其他的排队。
L0 · 场景
老板的一句话:"为什么账上躺着这么多钱?"
触发时点:季度末盘点前。回答约束:三句话、带数字、不当场翻表。
L1 · 三句话
钱压在哪 / 浪费在哪 / 先动哪
对应 diagnostics.html P2 执行摘要——Governing Message 独立成页。
L2 · 报告页
P3 总账四数 · P4 四类清单 · P5 三个动作 · P6 假设边界
每句话由哪一页支撑,见 P2/P3/P4 的下钻链。
L3 · 规则引擎
run_rules.py · 两个模块
模块1 四规则(缺货/死货/主力/长尾+打架);模块2 EOQ(EOQ、安全库存、ROP、备货天数、节省)。
L4 · Excel 数据
sku_table.csv 9 列 → sku_table_with_eoq.csv 25 列
每个数字最终落在某一列的某一个单元格。字段级映射见最后一页。
句 ① · 钱压在哪 ¥2,966,900 · 28% 死货 · 51.2% 集中度

"钱压在哪"这五个字,从报告 P3 总账一路追到 Excel 的「销售额」和「未动销天数」两列。

报告页
diagnostics.html · P3 总账(Top Line)
四个数:库存 ¥2,966,900、死货 ¥831,000(28%)、浪费(句②)、集中度 TOP5 = 51.2%。老板 10 秒看完。
规则
模块1 · 规则2(死货)+ 销售额汇总
死货 = 连续 21 天没动销;死货金额 = 死货 SKU 的销售额加总。集中度 = 销售额前 5% SKU 的占比(帕累托)。
代码
run_rules.py · 规则 2 与汇总
dead = df["未动销天数"] >= 21          # 366 个 SKU
dead_amt = df.loc[dead, "销售额"].sum()  # ¥831,000
total    = df["销售额"].sum()             # ¥2,966,900
top5_pct = df["销售额"].nlargest(int(N*0.05)).sum() / total  # 51.2%
EXCEL 列
sku_table.csv · 3 列喂出这句话
销售额 → 总账 ¥2,966,900 与 TOP5 集中度;未动销天数 → 判定死货;SKU编码 → 定位到具体 366 个货。
说出口
"账上 296 万里,83 万是 21 天没动过的死货;一半销售额靠 5% 的款撑着。"
两个数字一个对比,老板立刻知道钱压在两头。
句 ② · 浪费在哪 45 天 vs 17.8 天 · ¥943,686

"浪费在哪"不是靠感觉说"备多了"——是每个 SKU 一条公式,3000 条公式加总出 ¥943,686。

报告页
diagnostics.html · P3 总账「浪费」数 + P6 假设边界
P6 写明:提前期 14 天、服务水平 95%(Z=1.645)、持有成本率 25%、订货成本 ¥50/单。敢写假设,才经得起追问。
规则
模块2 · EOQ + 安全库存 + 再订货点
每个 SKU 单独算需求波动 CV(卖得稳的少备、卖得飘的多备),再推备货天数: CV = |销量30天 − 月均| ÷ 月均 安全库存 SS = Z × (日销量×CV) × √提前期 ROP = 日销量×提前期 + SS → 备货天数 = ROP ÷ 日销量 多备占用资金 = (45 − 公式天数) × 日销量 × 单价
代码
run_rules.py · 模块 2 核心四行
df["CV"] = ((df["销量30天"] - df["月均"]).abs() / df["月均"]).clip(0.1, 1.0)
df["安全库存"] = (Z * df["日销量"] * df["CV"] * np.sqrt(LEAD_TIME))
df["ROP"]   = df["日销量"] * LEAD_TIME + df["安全库存"]
df["多备占用资金"] = ((45 - df["公式备货天数"]) * df["日销量"] * df["单价"]).clip(lower=0)
EXCEL 列
3 列原始 → 4 列新增
喂入:销量30天 月均(算 CV)、销量6月(算日销量)、单价(算资金)。
产出:CV 安全库存 ROP 公式备货天数 多备天数 多备占用资金——全部写回 sku_table_with_eoq.csv。
说出口
"一刀切备 45 天,公式逐 SKU 算下来中位 18 天就够——94 万资金白压着。"
附赠一句:其中 2,498 个 SKU 改 EOQ 订货节奏,一年再省 ¥932,535 订货+持有成本。
句 ③ · 先动哪 1248 先补 · 366 先清 · 34 先裁决

"先动哪"不是给 3000 行全表——是 4 条规则跑出 4 张清单,重叠的 34 个最先裁决。

报告页
diagnostics.html · P4 四类清单 + P5 三个动作
P4 给老板看分类汇总(做决策),P5 给执行层看动作:谁做、多久见效、验收标准。不写"加强管理"这种空话。
规则
模块1 · 四规则 + 打架检测
缺货风险 = 近30天销量 > 月均×1.5;死货 = 21 天没动销;主力 = 销量前 30%;长尾 = 销量后 50%。
打架 = 死货 ∩ 主力——"该清的"和"不能清的"是同一批,必须人来裁决,机器不替你做决定。
代码
run_rules.py · 模块 1 完整四行 + 一行打架
risk = df["销量30天"] > df["月均"] * 1.5          # 1,248 缺货风险
dead = df["未动销天数"] >= 21                    #   366 死货
main = df["销量6月"] >= df["销量6月"].nlargest(int(N*0.30)).min()  # 901 主力
tail = df["销量6月"] <= df["销量6月"].nsmallest(int(N*0.50)).max() # 1,501 长尾
conflict = dead & main                           #    34 打架 ← 最先裁决
EXCEL 列
4 列喂出 4 张清单
销量30天 月均 → 缺货清单;未动销天数 → 死货清单;销量6月 → 主力与长尾清单。清单落到 SKU 编码级,执行层照着搬。
说出口
"1248 个要断货的先补,366 个死货先清,34 个又是死货又是主力的——今天就得有人拍板。"
34 这个数字最值钱:它证明规则之间会打架,而你的体系能看见打架。
L4 · EXCEL FIELD MAP 9 列进 · 25 列出

字段地图:9 列原始数据各喂哪条规则,16 列新增各属哪个模块——全部可复现。

列名来源喂给哪条规则 / 产出什么
SKU编码原始主键,所有清单的落点
商品名称原始展示用
销量6月原始规则3 主力规则4 长尾EOQ·日销量/年需求
销量30天原始规则1 缺货EOQ·CV 波动
月均原始规则1 缺货EOQ·CV 波动
单价原始EOQ·持有成本/多备资金
未动销天数原始规则2 死货
销售额原始总账 ¥2,966,900 · TOP5 集中度 51.2% · 死货金额 ¥831,000
最后动销原始展示用(死货清单佐证)
日销量 / 年需求新增销量6月 ÷ 180 · ×365
CV新增|销量30天 − 月均| ÷ 月均,截断在 [0.1, 1.0]
EOQ新增√(2 × 年需求 × ¥50 ÷ 持有成本)
安全库存 / ROP新增SS = 1.645×日销量×CV×√14 · ROP = 14天需求 + SS
公式备货天数新增ROP ÷ 日销量 → 中位 17.8 天(9.5~27)
多备天数 / 多备占用资金新增45 − 公式天数 · ×日销量×单价 → 合计 ¥943,686
现年成本 / 优年成本 / 潜在节省新增仅年销≥100 件的 2,498 个 SKU 参与 → 合计 ¥932,535/年
数据源与复现:UCI Online Retail 真实流水(54 万行)聚合为 3,000 个 SKU → sku_table.csv(9 列)→ 一条命令 python run_rules.py → sku_table_with_eoq.csv(25 列)。所有数字可当场重跑验证,不接受"你信我"。