54 万行交易流水里,只有 3000 行能用来做诊断。
缺口就在于这中间的一步:ERP 里存的是「每一笔交易」,报告要的是「每个 SKU 的一年」。这不是格式转换,是一次有判断的取舍——八道闸门,每一道都要你决定什么算数、什么不算数。
④ 剔非商品码
POST / M / PADS
−2,186
⑤ 按 SKU 聚合
行 → 商品
527,918 行
⑧ 取销量前 3000
第 3000 名 = 11 件
✅ 出口
为什么每一道都算「有判断的取舍」而不是「清理垃圾」:取消单剔不剔,取决于你算的是「净销量」还是「下单意愿」;纯样品要不要删,取决于这家店有几成靠批发。同一个动作,选不同的口径,最后那句「钱压在哪」的答案会变。整备阶段就要把口径定死,后面所有数字才有得辩。
四种脏法,每种都能让报告错一个数量级。
下面每一行都是从这 54 万行里抓的真实记录,不是我编的例子。看清它们长什么样,你才知道为什么要写那四行代码。
① 取消单9,288 行
InvoiceNo=C536379 · StockCode=D
Quantity=−1 · UnitPrice=27.50
发票号以 C 开头的都是取消/冲销单。数量是负的,如果不清,它会把你算出来的销量往回收——一个热销款可能因为大量退货变成负销量。
② 退货 / 负数量474 行
InvoiceNo=537032 · StockCode=21275
Quantity=−30 · UnitPrice=0.00
最极端一行:Quantity=−9,600
有些退货不走取消单,直接一条负数量。单价还常常是 0。不清掉,你的「平均单价」会被拉进阴沟。
③ 非商品码2,186 行
POST · DOT · M · PADS · S · B · m
AMAZONFEE · DCGSSBOY · DCGSSGIRL
这是最致命的陷阱。POST 是邮费,M 是手工调账,AMAZONFEE 是平台佣金,PADS 是包装耗材,DCGSSBOY/GIRL 是赠品套装。它们出现几千次——不清掉,「销量冠军」就是一个叫 POST 的邮费科目,你的主力款清单从第一行就是错的。
④ 空值1,454 行
StockCode=空 或 Description=空
→ 无法归属到任何商品
没有商品编码就没法算它的销量、没法算它压了多少钱。这类行只能删——但要在报告里说明删了多少,否则客户会怀疑你藏了数据。
这四行代码就是全部防线的核心
df = raw.dropna(subset=["StockCode", "Description"]) # ① 删空值
df = df[~df["InvoiceNo"].astype(str).str.startswith("C")] # ② 剔取消单
df = df[(df["Quantity"] > 0) & (df["UnitPrice"] > 0)] # ③ 剔退货
df = df[~df["StockCode"].astype(str).str.fullmatch(r"[A-Za-z]+")] # ④ 剔纯字母非商品码
第 ④ 行是最容易被漏、也最致命的一行。大部分人清洗时会记得删负数和空值,但很少有人想到「邮费也是一个 StockCode」。r"[A-Za-z]+" 的意思是:编码全是字母、一个数字都没有——正常商品码一定带数字(如 85099B、21275),纯字母的就是系统科目或调整项。
清洗只解决「哪些行算数」,聚合才解决「一行代表一个商品」。
527,918 行流水 → 3,912 个 SKU。这一步有 5 个 groupby,其中两个藏着你必须做对的判断。
| 目标列 | 怎么来 | 为什么是这个函数 |
| 销量6月 | 近 180 天 Quantity.sum() | 件数求和,最直接。窗口锚定数据最后一天(2011-12-09),不是「今天」——历史数据集没今天 |
| 销量30天 | 近 30 天 Quantity.sum() | 跟月均做对比,判断「最近是加速还是熄火」 |
| 最后动销 | InvoiceDate.max() | 取最后一次成交日期,喂给「未动销天数」 |
| 单价 | UnitPrice.median() ← 不是 mean | 关键判断 ① |
| 商品名称 | Description.mode() ← 不是 first | 关键判断 ② |
| 未动销天数 | 数据最后一天 − 最后动销 | 派生列,口径已在整备阶段定死 |
| 月均 | 销量6月 ÷ 6 | 派生列 |
| 销售额 | 销量6月 × 单价 | 派生列,全店加起来 = 报告里的总库存金额 |
关键判断 ①:单价为什么用中位数,不用平均数
# 同一个 SKU 在不同单据上的单价可能差几十倍:
# 促销价 ¥0.01 ← 会输入的「乌龙价」或 1 分钱活动价
# 正常价 ¥2.08 ← 真实售价
# 批发价 ¥1.50 ← 大客户折扣
# ...
df.groupby("StockCode")["UnitPrice"].median() # ✅ 中位数:取中间那个价
df.groupby("StockCode")["UnitPrice"].mean() # ❌ 平均数:被 0.01 拖垮
一个 SKU 卖了 100 次正常价 ¥2.08,只要出现 10 次 ¥0.01 的乌龙价,平均数会被拉到 ¥1.89,中位数仍是 ¥2.08。单价错 → 销售额错 → 报告里「钱压在哪」那个总数字直接错。这是整备阶段最容易埋雷的地方。
关键判断 ②:商品名为什么用众数,不用第一条
df.groupby("StockCode")["Description"].agg(lambda s: s.mode().iat[0]) # ✅ 取出现最多的写法
df.groupby("StockCode")["Description"].first() # ❌ 取第一条,可能是个错字
同一个编码,不同单据上的描述常常有细微差别:大小写不统一、多了个空格、员工打字时简写了。取「第一条」等于随机抽一个写法;取「众数」等于让多数票决定真名。这一列不影响算钱,但影响客户看不看得懂——客户认不出这是哪个货,整份报告他就不会信。
第 ⑦ 道闸:销量 ≥ 8 件这条线怎么定
3,447 个有动销的 SKU 里,有 337 个半年只卖了不到 8 件——这些是样品、零星补单、员工内购,不是商品。门槛设 8 件后剩 3,110 个,再取前 3000 个,第 3000 名的销量正好是 11 件。
这两个数字互相验证:门槛 8 件和排名 3000 名咬合得上,说明这条线不是拍脑袋定的,是自己长出来的。如果你设门槛 50 件,结果只剩 1,800 个 SKU,那就该回头检查是不是筛太狠了。
怎么证明这张表是「干净」的?靠它能复算出报告里的每一个数字。
所谓干净,不是「看着整齐」,是任何一个人拿到这张表,都能算出和你一模一样的结论。下面五条验收标准,缺一条这张表就算没过关。
| 验收项 | 标准 | 真实结果 |
| ✓ | 行数对齐 | 3000 行,一行一个 SKU,无重复编码 | 3000 行 / 3000 唯一码 |
| ✓ | 无空值 | 9 列全部非空 | 0 空值 |
| ✓ | 类型正确 | 销量是整数,单价是两位小数,日期是 YYYY-MM-DD | 已转换 |
| ✓ | 数字可复算 | 销售额.sum() = 报告 P3 的总库存金额 | ¥2,966,900 |
| ✓ | 规则可复跑 | python run_rules.py 一条命令,四张清单数量不变 | 1248 / 366 / 901 / 1501 |
最后一条是整备的终点线。整备做得好不好,唯一的判据是「别人跑一遍能不能得到同样的数」。这也正好是 AI 替你算不了的原因——AI 能给你公式,给不了你一张能被复算的表。
但真实客户的数据,比这个脏十倍
UCI 这份数据是「教科书级的脏」——问题清楚、边界干净、没有历史包袱。真实中小企业的 ERP 导出会多出下面这七种麻烦,每种都要单独做判断:
| 真实 ERP 的脏法 | 不处理会怎样 |
| 一码多名:换包装/换供应商时新开编码,同一个货变成 2 个 SKU | 销量被劈成两半,两边都进长尾,实际是主力款 |
| 一名多码:颜色/尺码当独立编码,颜色混卖时算不清 | 单款看销量很低,合并后是爆款——分不清该备哪个色 |
| 组合装拆不开:套装卖出去,库存扣的是组成件 | 组成件的销量被虚高,套装本身看不到真实需求 |
| 赠品混在销量里:赠品以 0 元或 0.01 元出库 | 赠品变成「销量冠军」,单价均值被拖到地板上 |
| 退货两种记法:有的冲原单,有的单独开负单 | 同一笔退货被算两次,或者一次都没算 |
| 含税/不含税混用:采购单含税,销售单不含税 | 销售额和成本口径不一致,毛利算出来是假的 |
| 停售品没标记:下架的货还在系统里挂着库存 | 被算成「死货」要求清仓,实际早就清完了 |
这七条就是「数据整备」真正的工程量。上面那张 funnel 图(54 万 → 3000)是
标准答案;真实客户的活儿,是在这张标准答案之外,再逐个问清这七个问题。
这也是为什么这一步 AI 替不了——它不知道你的「一码多名」是哪几个码。
公式是公开的,把脏数据变成能签字的表,才是这行真正卖的东西。
别人能抄走的部分
EOQ = √(2DS/H)、SS = Z·σ·√L、ABC 帕累托 80/20 —— 百度一下全是答案,AI 一秒给你。这部分没有护城河,别把它当卖点。
别人抄不走的部分(本页讲的就是这个)
知道 9,288 行取消单要剔、知道 POST 是邮费不是商品、知道单价要取中位数、知道客户 ERP 里哪几个码是一码多名 —— 这些不在任何教程里,只能一个一个项目踩出来。踩过的项目越多,这张「脏数据图鉴」越厚。
所以生意的形态
不是「卖一份报告」,是「每个季度重跑一次」——因为客户的 ERP 每个月都会变脏:新品上一码多名、老品下架没标记、活动价混进正常价。你卖的是持续把脏数据洗干净的能力,不是那套公式。
回到客户会问的那句话:「我直接问 AI 不就行了?」
答:「可以。你把这张 3000 行的表导出来,我帮你算——但表导不出来,对吧?」