📊 第12章 Python 实现客户价值分析
📄 一句话概括

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 秒实验

  1. 把负金额那一步改成 abs(),看总额会变多少,想想退货该怎么记才对。
  2. 把统计窗口从 909 天改成只看 2024 年,重算人均单量。
  3. 想一想:如果退货占了三成,直接删掉会不会让结论失真?

常见误区

  1. 半年和全年直接比:2025 只有半年,71.59 万看着像腰斩,年化后是 143 万。
  2. 负金额直接删不记数:报告里必须写「剔除退货 6 笔」,否则口径说不清。
  3. 重复单不去:F 多算一次、M 多算一笔,两个指标一起错。
  4. 只看均值:右偏分布下,均值不代表典型值,中位数才代表。
自测三问(点开对答案) 1. 为什么负金额要单独说一句? —— 它可能是退货或冲账,不是销售额。直接删会让总额和品类分析都失真,报告里必须交代口径。 2. 2025 年 71.59 万,能说业务腰斩了吗? —— 不能。2025 只有半年,年化约 143 万。窗口长度不同的数不能直接比。 3. 均值 521.87、中位数 419.64,说明什么? —— 分布右偏,少数大额订单抬高了均值。典型订单在 420 元上下。

数据能算了。下一篇按客户聚出 R、F、M 三个值,顺便看看集中度。

相关笔记