DATA PREP · 数据整备 P1 / 5 · THE FUNNEL

54 万行交易流水里,只有 3000 行能用来做诊断。

缺口就在于这中间的一步:ERP 里存的是「每一笔交易」,报告要的是「每个 SKU 的一年」。这不是格式转换,是一次有判断的取舍——八道闸门,每一道都要你决定什么算数、什么不算数。

原始流水
UCI Online Retail
541,909 行
—
① 删空值
540,455
−1,454
② 剔取消单
C 开头发票
531,167
−9,288
③ 剔退货
数量或单价 ≤ 0
530,104
−1,063
④ 剔非商品码
POST / M / PADS
527,918
−2,186
⑤ 按 SKU 聚合
行 → 商品
3,912 个 SKU
527,918 行
⑥ 近 6 月有动销
3,447 个
−465
⑦ 销量 ≥ 8 件
剔纯样品
3,110 个
−337
⑧ 取销量前 3000
第 3000 名 = 11 件
3,000 行 × 9 列
✅ 出口
为什么每一道都算「有判断的取舍」而不是「清理垃圾」:取消单剔不剔,取决于你算的是「净销量」还是「下单意愿」;纯样品要不要删,取决于这家店有几成靠批发。同一个动作,选不同的口径,最后那句「钱压在哪」的答案会变。整备阶段就要把口径定死,后面所有数字才有得辩。
P2 · 脏数据图鉴 WHAT "DIRTY" ACTUALLY LOOKS LIKE

四种脏法,每种都能让报告错一个数量级。

下面每一行都是从这 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),纯字母的就是系统科目或调整项。
P3 · 从「行」到「SKU」 AGGREGATION · 9 COLUMNS

清洗只解决「哪些行算数」,聚合才解决「一行代表一个商品」。

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,那就该回头检查是不是筛太狠了。
P4 · 出口验收 ACCEPTANCE CRITERIA

怎么证明这张表是「干净」的?靠它能复算出报告里的每一个数字。

所谓干净,不是「看着整齐」,是任何一个人拿到这张表,都能算出和你一模一样的结论。下面五条验收标准,缺一条这张表就算没过关。

验收项标准真实结果
✓行数对齐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 替不了——它不知道你的「一码多名」是哪几个码。
👉 看五套真实 ERP 的脏导出演示——聚水潭 / 旺店通 / 金蝶 / 用友 / Shopify,每个雷都标在具体格子上,可下载 xlsx 对照。
P5 · 总结 WHY THIS IS THE MOAT

公式是公开的,把脏数据变成能签字的表,才是这行真正卖的东西。

别人能抄走的部分
EOQ = √(2DS/H)、SS = Z·σ·√L、ABC 帕累托 80/20 —— 百度一下全是答案,AI 一秒给你。这部分没有护城河,别把它当卖点。
别人抄不走的部分(本页讲的就是这个)
知道 9,288 行取消单要剔、知道 POST 是邮费不是商品、知道单价要取中位数、知道客户 ERP 里哪几个码是一码多名 —— 这些不在任何教程里,只能一个一个项目踩出来。踩过的项目越多,这张「脏数据图鉴」越厚。
所以生意的形态
不是「卖一份报告」,是「每个季度重跑一次」——因为客户的 ERP 每个月都会变脏:新品上一码多名、老品下架没标记、活动价混进正常价。你卖的是持续把脏数据洗干净的能力,不是那套公式。
回到客户会问的那句话:「我直接问 AI 不就行了?」
答:「可以。你把这张 3000 行的表导出来,我帮你算——但表导不出来,对吧?」