9308 行里三类毛病:完全重复 2 条、客户编号空 4 条、金额为负 6 条。 洗完剩 9296 行,跨 909 天,人均 7.75 单、人均 4042.71 元。 金额分布右偏:均值 521.87 元、中位数 419.64 元,报中位数更老实。
数据清洗在第 9、10、11 章都做过,这一篇的差别是「负金额」这一种毛病。 退货和冲账不是销售额,留在表里会把 M 拉低——处理方式要想清楚。
一、读进来先认三件事
read_csv 一句读完,9308 行 4 列,1200 个不同客户。
编号两列都是字符串(D 加六位、C 加四位),不是数字,别想着用它做加减。
订单日期读进来是字符串,和上一章一样,得先转 datetime 才能算天数差。
这三件事(形状、类型、主键)是每次读数据的固定动作,第 3 章讲过,这里只是又用一遍。
二、三类毛病,各有各的处理
| 毛病 | 查到 | 怎么处理 |
|---|---|---|
| 完全重复 | 2 条 | 去掉,否则 F 多算一次 |
| 客户编号空 | 4 条 | 删,不知道是谁 |
| 金额为负 | 6 条 | 删(这次),本该另存退货表 |
重复单最阴的地方是它同时污染 F 和 M:F 多一单,M 多一笔,两个指标一起错。 客户编号空的 4 条进不了按客户的统计,留着只会让总额对不上。 负金额是退货或者冲账。严格说不该直接删,应该另存一张退货表,做净销售额时再减。
这次的目的只是算 RFM,先剔掉。但报告里必须写一句「已剔除退货 6 笔」,否则口径说不清。
三、洗完对一遍账
去重 9308 减 2 得 9306;删空客户 9306 减 4 得 9302;删负金额 9302 减 6 得 9296。 一共清掉 12 条,剩 9296 条。每一步都要报数,账对不上说明中间有漏。 剩的每一条都是「某个客户花了一笔钱」,这句话就是清洗的验收标准。
时间范围 2023-01-01 到 2025-06-28,跨 909 天——比两年半少一点,因为最后一单没到月底。
四、年度对比有个口径坑
| 年份 | 订单数 | 金额(万元) |
|---|---|---|
| 2023 | 4306 | 212.37 |
| 2024 | 3800 | 201.17 |
| 2025 | 1190 | 71.59 |
看到 2025 年只有 71.59 万,第一反应是「腰斩了」。错,2025 年只有半年。 半年 71.59 万,年化约 143 万,比 2023、2024 都低——这个结论才站得住。 不同长度的窗口不能直接比。年报里这种坑最多:季度比年度、半年比全年、缺月比满月。
补一句:这份数据是造的,2025 年少不是业务下滑,是造数时窗口只到 6 月。
五、金额分布:右偏要看中位数
均值 521.87 元,中位数 419.64 元,最大 1879.96 元。 均值比中位数高 100 多块,说明分布右偏:少数大额订单把均值拉上去了。 右偏的分布,报均值会让人以为「典型订单 500 块」,其实一半订单在 420 以下。
两种补法:报中位数,或者把「大额订单有多少笔」单独说一句。 这件事在客户层面更明显:M 的均值 4042.71 元,中位数只有 1639.78 元。
六、动手 Demo:数据抽取与探索
读进来、查三样、洗三步,每一步报「减几条、剩几条」。 最后看时间范围、年度对比和金额分布,重点是右偏这件事。
import pandas as pd
df = pd.read_csv("订单流水.csv")
print("原始 %s" % (df.shape,))
print("重复 %d,客户空 %d,负金额 %d"
% (df.duplicated().sum(), df["客户编号"].isnull().sum(),
(df["订单金额"] < 0).sum()))
df = df.drop_duplicates()
df = df.dropna(subset=["客户编号"])
df = df[df["订单金额"] > 0]
df["订单日期"] = pd.to_datetime(df["订单日期"])
print("洗完 %d 行" % len(df))
print("日均单量 %.2f" % (len(df) / 909))
print("金额 均值 %.2f,中位数 %.2f,最大 %.2f"
% (df["订单金额"].mean(), df["订单金额"].median(),
df["订单金额"].max()))
实跑输出:
--------------------------------------------------------------------
数据抽取与探索
--------------------------------------------------------------------
① 读进来
--------------------------------------------------------------------
读进来 9308 行 4 列,1200 个不同客户。
订单编号是 D 加六位数字,客户编号是 C 加四位,两个都是字符串。
订单日期读进来是字符串,和上一章一样,得先转 datetime 才能算天数差。
② 查三样
--------------------------------------------------------------------
| 毛病 | 怎么查 | 这次查到 |
|---|---|---:|
| 完全重复 | duplicated().sum() | 2 条 |
| 客户编号为空 | isnull().sum() | 4 条 |
| 金额为负 | (金额 < 0).sum() | 6 条 |
负金额是退货或者冲账,不是销售额。留在表里会把 M 拉低,得单独处理。
客户编号为空的那几条,不知道是谁买的,进不了按客户的统计。
重复的两条是导库时插了两遍,不去掉会让 F 多算一次、M 多算一笔。
③ 洗:三步
--------------------------------------------------------------------
去重:9308 减 2 得 9306。
删客户编号空:9306 减 4 得 9302。
删负金额:9302 减 6 得 9296。
一共清掉 12 条,剩 9296 条。剩的每一条都是「某个客户花了一笔钱」。
退货那几条严格说不该直接删,应该另存一张退货表——这次只是为了算 RFM,先剔掉。
④ 时间和规模
--------------------------------------------------------------------
时间范围 2023-01-01 到 2025-06-28,跨 909 天。
客户 1200 个,人均 7.75 单,人均 4042.71 元。
| 年份 | 订单数 | 金额(万元) |
|---|---:|---:|
| 2023 | 4306 | 212.37 |
| 2024 | 3800 | 201.17 |
| 2025 | 1190 | 71.59 |
2025 年只有半年,不能和整年直接比。这是年报里最常见的口径坑。
⑤ 金额的分布
--------------------------------------------------------------------
均值 521.87 元,中位数 419.64 元,最大 1879.96 元。
均值明显大于中位数,说明分布右偏:少数大额订单把均值拉上去了。
遇到右偏,报中位数比报均值老实。或者把「大额订单有多少」单独说一句。
! 每个数都要说清口径:金额是订单级还是客户级、含不含退货、时间窗口多长。
dropna(subset=["客户编号"]) 只按这一列判空,别的列有缺失不受影响。
df["订单金额"] > 0 顺手把 0 也删了。金额正好是 0 的单子在业务上也没意义。
describe() 一次能看到均值、中位数、最大最小,比一个个 .mean() 省事。
30 秒实验
- 把负金额那一步改成
abs(),看总额会变多少,想想退货该怎么记才对。 - 把统计窗口从 909 天改成只看 2024 年,重算人均单量。
- 想一想:如果退货占了三成,直接删掉会不会让结论失真?
常见误区
- 半年和全年直接比:2025 只有半年,71.59 万看着像腰斩,年化后是 143 万。
- 负金额直接删不记数:报告里必须写「剔除退货 6 笔」,否则口径说不清。
- 重复单不去:F 多算一次、M 多算一笔,两个指标一起错。
- 只看均值:右偏分布下,均值不代表典型值,中位数才代表。
自测三问(点开对答案)
1. 为什么负金额要单独说一句? —— 它可能是退货或冲账,不是销售额。直接删会让总额和品类分析都失真,报告里必须交代口径。 2. 2025 年 71.59 万,能说业务腰斩了吗? —— 不能。2025 只有半年,年化约 143 万。窗口长度不同的数不能直接比。 3. 均值 521.87、中位数 419.64,说明什么? —— 分布右偏,少数大额订单抬高了均值。典型订单在 420 元上下。数据能算了。下一篇按客户聚出 R、F、M 三个值,顺便看看集中度。
- ☐ (本篇完,下一篇 12.4 计算 RFM 值)