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

标准化前标准差是 182、7、6750,标准化后三个都是 1——这才叫一人一票。 肘部法在 k=4 附近平下去,但这次没有漂亮的直角,取 4 有业务判断的成分。 聚完必须回头看均值表:0 号 92 人平均 M 25865 元,3 号 191 人平均 R 569 天。

📝 本章和前面几章的关系

上一篇把 R、F、M 算出来了,这一篇做三件事:标准化、定 k、跑 KMeans。 第 8 章讲过 KMeans 的原理,这一篇关心的是「怎么用对」。

一、不标准化会出什么事

R 的标准差 182.05,F 的 7.05,M 的 6750.89。M 是 R 的三十七倍。 距离是各维差的平方相加。M 那一项平方后比别人大三个数量级,R 和 F 约等于不存在。 换句话说:不标准化,你做的其实是「按消费额聚类」,R 和 F 白算了。

这件事不看不知道,一看就明白——所以标准化前后各算一次标准差,是最省事的自检。 自检的习惯比记住结论更重要:换一份数据,你得能自己发现同样的问题。

二、标准化,再把 R 取反

StandardScaler 对每一列做「减均值、除标准差」,三列都变成均值 0、标准差 1。 再把 R 那一列取负,方向就统一了:三个值都是越大越好。 标准化之后三列标准差都是 1.00,说明现在一人一票,谁也不占便宜。

取负要在标准化之后做。先取负再标准化,结果一样,但读代码的人容易看糊涂。 Z[:, 0] = -Z[:, 0] 这一句里,0 是「R 在第几列」。列序一变,这句就错了。

三、该分几堆:肘部法

k 簇内平方和 比上一档降了多少
2 1539 —
3 752 787
4 565 187
5 430 135
6 312 118

k 从 2 加到 3 降了 787,从 3 到 4 只降 187,再往后每加一档降得更少。 但说实话,这次没有一个漂亮的直角拐点。曲线是慢慢平下去的。 肘部法给不了确定答案,它只能帮你排除「k 太大」。取 4 更多是业务判断。

重要、潜力、一般、流失,四类客户的说法在业务上站得住。数学给范围,业务定数字。

四、跑 KMeans:两个参数不能省

KMeans(n_clusters=4, n_init=10, random_state=42),三个参数都要写。 n_init=10 是「换 10 组初始点各跑一遍,取最好的」,新版不写会出警告。 random_state=42 固定随机性。不写的话,同一份数据两次跑出来的堆号可能不一样。

跑完每堆人数是 92、498、419、191,四堆都分到了人,没有空堆。 这一步最容易犯的错是把 n_clusters 和业务类别数搞混——它是超参数,不是结论。

五、回头看均值表,才认识每一堆

簇 人数 平均 R(天) 平均 F(次) 平均 M(元)
0 号 92 57.4 29.0 25865.64
2 号 498 112.5 7.5 3220.14
1 号 419 310.3 4.9 1512.39
3 号 191 569.3 4.4 1226.68

这一步不能省。聚类给的 0、1、2、3 没有任何含义,含义全在这张表里。 0 号:R 最小、F 和 M 都最高,来的勤买得多,这就是最值钱的一堆。 3 号:R 最大,569 天没来了,F 和 M 都低,基本可以判流失。

簇号是随机的,别写死「0 号最重要」。每次重跑堆号可能变,靠均值表认人。

六、动手 Demo:数据转换与客户聚类

从 rfm.csv 读,标准化再把 R 取反,肘部法试 k,最后 k=4 跑一遍并看均值表。 第二节那个「标准化后三个标准差都是 1」是自检的锚点。

import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

rfm = pd.read_csv("rfm.csv", index_col=0)
X = rfm[ ["R", "F", "M"]].values.astype(float)
print("标准化前 %.2f %.2f %.2f" % tuple(X.std(axis=0)))

Z = StandardScaler().fit_transform(X)
Z[:, 0] = -Z[:, 0]
print("标准化后 %.2f %.2f %.2f" % tuple(Z.std(axis=0)))

for k in (2, 3, 4, 5, 6):
    km = KMeans(n_clusters=k, n_init=10, random_state=42).fit(Z)
    print("k=%d 簇内平方和 %.0f" % (k, km.inertia_))

rfm["簇"] = KMeans(n_clusters=4, n_init=10, random_state=42).fit_predict(Z)
g = rfm.groupby("簇").agg(人数=("R", "size"), 平均R=("R", "mean"),
                         平均F=("F", "mean"), 平均M=("M", "mean"))
print(g.sort_values("平均M", ascending=False).round(2).to_string())

实跑输出:

--------------------------------------------------------------------
数据转换与客户聚类
--------------------------------------------------------------------

  ① 不标准化会出什么事
  --------------------------------------------------------------------
  R 的标准差 182.05,F 的标准差 7.05,M 的标准差 6750.89。
  M 的标准差是 R 的上百倍。距离里 M 那一项一平方,其它两项等于不存在。
  换句话说:不标准化,你做的其实是「按消费额聚类」,R 和 F 白算了。
  这件事不看不知道,一看就明白——所以标准化前后各算一次标准差,是最省事的自检。

  ② 标准化,再把 R 取反
  --------------------------------------------------------------------
  StandardScaler 对每一列做「减均值、除标准差」,三列都变成均值 0、标准差 1。
  再把 R 那一列取负,方向就统一了:三个值都是越大越好。
  标准化之后的标准差:1.00、1.00、1.00。
  三个都是 1,说明三列现在一人一票,谁也不占便宜。

  ③ 先看该分几堆
  --------------------------------------------------------------------

| k | 簇内平方和 | 比上一档降了多少 |
|---|---:|---:|
| 2 | 1539 | — |
| 3 | 752 | 787 |
| 4 | 565 | 187 |
| 5 | 430 | 135 |
| 6 | 312 | 118 |

  k 从 2 加到 3,降了 787;从 3 到 4 只降 187;再往后每加一档降得更少。
  曲线到这个位置就平下去了——但说实话,这次没有一个漂亮的直角拐点。
  肘部法给不了确定答案,它只能帮你排除「k 太大」。取 4 更多是业务判断:
  重要、潜力、一般、流失,四类客户的说法在业务上站得住。数学给范围,业务定数字。

  ④ 跑 KMeans
  --------------------------------------------------------------------
  n_init=10 是「换 10 组初始点各跑一遍,取最好的」,不加会有警告。
  random_state=42 固定随机性,保证每次跑出来一样。这两条不写,结果不可复现。
  每一堆的人数:0 号 92 人、1 号 419 人、2 号 498 人、3 号 191 人。

  ⑤ 回头看每堆长什么样
  --------------------------------------------------------------------

| 簇 | 人数 | 平均 R(天) | 平均 F(次) | 平均 M(元) |
|---|---:|---:|---:|---:|
| 0 号 | 92 | 57.4 | 29.0 | 25865.64 |
| 2 号 | 498 | 112.5 | 7.5 | 3220.14 |
| 1 号 | 419 | 310.3 | 4.9 | 1512.39 |
| 3 号 | 191 | 569.3 | 4.4 | 1226.68 |

  这一步不能省。聚类给的 0、1、2、3 没有任何含义,含义全在这张表里。
  平均 M 最高的那堆,平均 R 也最小——这就是最值钱的一堆。
  平均 R 最大的那堆,F 和 M 都低——这堆人已经很久没来了,而且本来买得也不多。
  ! 簇号是随机的,别写死「0 号最重要」。每次重跑,堆号可能变,靠均值表认人。

tuple(X.std(axis=0)) 把三个标准差直接喂给 % 格式化,省得写三遍。 Z[:, 0] = -Z[:, 0] 里那个 0 是列号,对应 ["R", "F", "M"] 的第一列。列序一改就得跟着改。 fit_predict 等于 fit 之后再取 labels_,一步拿到堆号。

30 秒实验

  1. 把 Z[:, 0] = -Z[:, 0] 注释掉,看 0 号簇的均值变成什么样。
  2. 把 n_clusters 改成 3 和 5,各跑一次,看均值表怎么合并或拆分。
  3. 想一想:如果不标准化只取负,聚类结果会更像「按金额排」还是「按 R 排」?

常见误区

  1. 不标准化直接聚类:距离被 M 占满,R 和 F 形同虚设。
  2. 忘了给 R 取反:最活跃的客户会被打进「流失」那堆。
  3. 不设 random_state:堆号每次都变,改了半天不知道改动有没有用。
  4. 拿簇号当业务含义:0 号是不是最重要,得看均值表。
自测三问(点开对答案) 1. 标准化之后三个标准差都是 1,说明什么? —— 三列现在同量级,距离里每一维的权重大致相当,R 和 F 不会被 M 压掉。 2. 肘部法这次给出了明确答案吗? —— 没有。曲线是慢慢平下去的,没有直角。取 4 有一半是业务判断。 3. 为什么 `random_state` 一定要写? —— KMeans 的初始点是随机的,不固定的话每次结果不同,无法复现也无法比较。

四堆分出来了,但它们只有编号。下一篇给它们起名字,并拿「真身份」验一遍。

相关笔记