13 聚类分析:在未知中寻找规律

本章议程:在未知中寻找规律

1. 核心概念

  • 为何需要无监督学习?
  • 什么是聚类分析?
  • 目标:高内聚、低耦合

2. K-均值算法

  • 直觉与数学原理
  • 算法分步详解
  • 优点局限性

3. Python实战

  • 数据预处理关键步骤
  • 寻找最佳 K 值
  • 解读验证聚类结果

回顾:监督学习的世界

在之前的章节中,我们的任务是有明确目标的预测。

  • 我们拥有包含“问题” (特征 X) 和“答案” (标签 y) 的数据集。
  • 目标: 训练一个模型 \(f\),使其能够根据新的 X 准确预测出 y
  • 例子: 根据贷款申请人的信息 (X),预测其是否会违约 (y)。

我们总是在一位“导师”的指导下学习。

核心问题:当没有“正确答案”时,我们如何发现规律?

但是,如果现实世界没有给我们提供 y 呢?

  • 我们只有一大堆数据点 X,没有任何预先定义的标签。
  • 我们不知道数据的内在结构。
  • 我们无法计算“准确率”,因为没有“正确答案”可以参照。

这时,我们就从“预测”转向了“发现”。

引入:无监督学习

无监督学习 (Unsupervised Learning) 是一类机器学习任务,其目标是从无标签的数据中发现隐藏的模式或内在结构

它不是为了预测一个特定的输出,而是为了理解数据本身

一个直观的类比:监督 vs. 无监督

监督学习

如同跟着一本有标准答案的习题册学习。每做一道题,都可以对照答案,知道自己是对是错,并不断修正方法。

监督学习类比 一个模型根据带有标签的数据学习预测规则。 目标:学习预测答案 带标签的数据 (X, y) 模型 f(X) (例如: 回归, 分类) 预测: ✓ 预测: ✗ 预测结果

无监督学习

如同在没有地图和导航的土地上探索。你需要自己观察地形,识别出哪里是森林,哪里是河流,哪里是山脉,从而绘制出地图。

无监督学习类比 一个模型从未标记的数据中发现隐藏的结构。 目标:发现内在结构 无标签的数据 (X) 模型 f(X) (例如: 聚类) 发现的结构

核心问题:监督学习 vs. 无监督学习

这两种方法论代表了数据分析的两种不同思维范式。

特征 监督学习 (Supervised Learning) 无监督学习 (Unsupervised Learning)
数据 包含特征 (X) 和标签 (y) 只包含特征 (X)
目标 预测标签 y 发现数据 X 中的隐藏结构
核心问题 “这个新客户会违约吗?” “我的客户可以被分成哪几类?”
主要任务 分类、回归 聚类、降维、关联规则挖掘

聚类分析是无监督学习中最重要、最基础的工具。

引入案例:银行如何识别潜在的高风险客户群体?

想象一下,一家银行希望主动识别其客户中的高风险群体,以便进行更有效的风险管理。

  • 挑战: 银行拥有的只是客户的基本信息(收入、负债、工龄等),并没有一个现成的“高风险客户”标签。
  • 问题: 我们能否仅凭客户的特征数据,就自动地将他们划分为几个有意义的群体,比如“稳定低风险群”和“潜在高风险群”?

这就是聚类分析要解决的问题。

可视化问题:一片混沌的客户数据

我们的起点是一堆看似杂乱无章的客户数据点。每个点代表一个客户,其在图上的位置由其经济特征(如收入、负债)决定。

我们的目标是找到一种方法,让计算机自动地为这些点“着色”,把相似的客户圈在一起。

混沌的客户数据 一团未被分类的灰色数据点,代表需要被聚类的客户。 我们能从这片“混沌”中发现结构吗? 特征 1 (如: 收入) 特征 2 (如: 负债)

聚类分析的目标:让“相似的”数据“在一起”

聚类分析 (Clustering) 是一种将数据集中的样本划分为若干个不相交子集(称为“簇”,Cluster)的过程。

核心原则:

  • 簇内相似性 (Intra-cluster similarity) 高: 同一个簇内的数据点彼此相似。
  • 簇间相似性 (Inter-cluster similarity) 低: 不同簇之间的数据点彼此不相似。

在我们的贷款案例中,我们希望找到两个簇:一个代表了违约风险高的客户,另一个代表了违约风险低的客户。

图解聚类原则

一个好的聚类结果,应该像下图这样:簇内部的点都紧紧地抱在一起,而不同颜色的簇之间则泾渭分明。

聚类原则示意图 两个清晰分开的簇,展示了高簇内相似性和低簇间相似性。 高簇内相似性 低簇间相似性

我们将聚焦于最经典的算法:K-均值 (K-Means)

K-均值法是一种非常流行且直观的聚类算法。它在各种应用中都表现出色,是每个数据科学家都必须掌握的基础工具。

观察下图中,即使我们不知道每个点的“标签”,我们的直觉也能清晰地将这些数据点分为三个不同的群组。

K-均值算法就是用数学的方式来形式化并实现这一过程。

K-均值的视觉直觉

import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import make_blobs

# --- 生成数据 ---
X, y = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42)

# --- 可视化 ---

plt.figure(figsize=(8, 5))
plt.scatter(X[:, 0], X[:, 1], s=40, edgecolors='k', alpha=0.8, c='#808080')
plt.title('直觉告诉我们这里有三个群体', fontsize=16)
plt.xlabel('特征 1 (Feature 1)')
plt.ylabel('特征 2 (Feature 2)')
plt.grid(True)
plt.show()
Figure 1: 一个可以被清晰地分为三个簇的数据集

解构算法名称: “K-Means”

这个名字本身就揭示了算法的两个核心要素:

  • K: 这是我们需要预先指定的超参数,代表我们希望将数据分成多少个簇。
    • 在贷款案例中,目标是区分“高风险”和“低风险”,因此选择 K=2 是一个合理的起点。
    • 对于上一页的数据,最合理的选择显然是 K=3
  • Means (均值): 这指的是算法的核心操作——通过计算簇内数据点的几何平均值来找到每个簇的中心点 (Centroid)

如何用数学语言定义一个“好”的聚类?

为了让计算机执行聚类,我们必须将“好的聚类”这个模糊概念,转化为一个可以计算和优化的数学目标。

这需要两个关键的数学工具:

  1. 距离度量 (Distance Metric): 一种量化两个数据点之间“不相似度”的方法。
  2. 目标函数 (Objective Function): 一个评判整个聚类结果优劣的单一数值指标。

基础:使用平方欧几里得距离度量差异

最常用的距离度量方法是平方欧几里得距离 (Squared Euclidean Distance)。

对于两个 m 维的数据点 \(x^{(i)}\)\(x^{(j)}\),它们之间的平方欧几里得距离定义为:

\[ \large{d^2(x^{(i)}, x^{(j)}) = \sum_{k=1}^{m} (x_k^{(i)} - x_k^{(j)})^2 = ||x^{(i)} - x^{(j)}||^2} \]

直观理解: 这本质上就是我们在二维或三维空间中熟悉的直线距离的平方,并推广到了更高维度。

图解欧几里得距离

在二维空间中,它就是勾股定理的应用。这个距离是我们衡量“相似性”的基石。

欧几里得距离示意图 在二维坐标系中,两个点之间的欧几里得距离由勾股定理计算得出。 特征 1 特征 2 x⁽ⁱ⁾ x⁽ʲ⁾ d(x⁽ⁱ⁾, x⁽ʲ⁾) Δ 特征 1 Δ 特征 2

目标:最小化簇内离差平方和 (WCSS)

有了距离度量,我们就可以定义一个簇 \(C_k\) 的“紧凑程度”。我们使用簇内离差平方和 (Within-Cluster Sum of Squares, WCSS) 来衡量。

它等于簇内每个数据点到该簇中心点 (Centroid) \(\mu_k\) 距离的平方和。

\[ \large{\text{WCSS}(C_k) = \sum_{x_i \in C_k} ||x_i - \mu_k||^2} \]

  • \(\mu_k\) 是簇 \(k\) 的中心点(即该簇所有点的均值)。
  • WCSS 值越小,说明簇 \(k\) 内部的数据点越紧凑。

图解 WCSS

WCSS 的值就是下图中所有红色虚线长度的平方之和。我们的目标是让这个总和尽可能小。

簇内离差平方和 (WCSS) 示意图 一个簇,包含一个中心点和多个数据点,每个数据点到中心点的距离线被高亮显示。 WCSS = Σ (红色虚线长度)² μk

K-均值的最终目标函数

现在,我们可以明确 K-均值算法的最终优化目标了。

假设我们要将数据分为 \(K\) 个簇 \(C_1, C_2, \dots, C_K\),我们的目标是找到这样一种划分方式,使得所有簇的 WCSS 之和 (即 Total WCSS) 最小:

\[ \large{\min_{C_1, \dots, C_K} \sum_{k=1}^{K} \text{WCSS}(C_k) = \min_{C_1, \dots, C_K} \sum_{k=1}^{K} \sum_{x_i \in C_k} ||x_i - \mu_k||^2} \]

这个公式就是我们评判一个聚类结果好坏的最终、可量化的标准。

求解挑战:一个NP-Hard问题

直接求解上述目标函数是一个极其困难的组合优化问题 (NP-hard)。

  • 对于一个有 \(N\) 个数据点和 \(K\) 个簇的数据集,可能的划分方式数量是天文数字。
  • 暴力搜索所有可能性是完全不可行的。

幸运的是,我们有一个非常高效的启发式算法,可以在实践中快速找到一个很好的局部最优解

K-均值算法流程:一个优雅的迭代过程

这个算法通过交替执行两个核心步骤来逐步优化聚类结果,直到收敛。

  1. 分配 (Assignment Step): 将每个数据点分配给离它最近的簇中心。
  2. 更新 (Update Step): 将每个簇的中心点更新为该簇内所有数据点的均值

算法不断重复这两个步骤,就像跳着一支优雅的华尔兹,直到簇的分配不再发生变化为止。

算法分步图解:起点

我们从一堆无标签的数据点开始。假设我们决定要找 K=2 个簇。

K-Means Step 0: Unlabeled Data A scatter plot of gray, unlabeled data points. Step 0: 我们的原始数据 (K=2)

算法分步图解:步骤 1 (初始化)

随机在数据空间中选择 K 个点作为初始的簇中心(质心, Centroids)。

K-Means Step 1: Initialize Centroids Same data points, with two new, randomly placed centroids. Step 1: 随机初始化 2 个簇中心

算法分步图解:步骤 2a (分配)

对于每个数据点,计算它到两个簇中心的距离,并将其分配给最近的那个簇。数据点开始有了“颜色”。

K-Means Step 2a: Assignment Data points are now colored blue or orange based on the nearest centroid. Step 2a (迭代 1): 将数据点分配给最近的中心

算法分步图解:步骤 2b (更新)

对于每个簇,重新计算其中心点,即该簇内所有数据点的几何平均值。

\[ \large{\mu_k = \frac{1}{|C_k|} \sum_{x_i \in C_k} x_i} \]

K-Means Step 2b: Update Centroids Old centroids are faded, and new centroids appear at the mean of their respective colored points. Step 2b (迭代 1): 将中心移动到其成员的“重心”

算法分步图解:迭代循环

接下来,我们基于的簇中心,不断重复分配更新这两个步骤。

K-Means Iteration Loop A loop arrow indicating that the Assignment and Update steps are repeated. 分配 (Assignment) 更新 (Update) 1. 将点分配给最近的中心 2. 更新中心到簇的均值 重复此过程... 直到分配结果不再改变

算法分步图解:最终收敛

最终,簇中心会稳定在各自数据群的“重心”,分配结果不再变化,算法就收敛了。我们成功地发现了数据中的两个群组。

K-Means Converged State The final state where points are correctly clustered and centroids are stable. 算法收敛:我们找到了两个稳定的簇

K-均值算法的动态演示

下面的动图展示了在一个更复杂的数据集上 (K=3) 的完整迭代过程。

  • Iteration 1: 初始随机分配很糟糕,许多邻近的点被分到了不同的簇。
  • Iteration 2-3: 簇中心迅速向其成员的重心移动,分类开始变得更加合理。
  • Final: 簇中心稳定在各个数据群的中心,分类质量不断提高,最终收敛。
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans

# --- 生成数据 ---
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42)

def plot_kmeans_iteration(X, kmeans, iteration, ax, show_legend=False):
    # 预测并获取中心点
    labels = kmeans.predict(X)
    centers = kmeans.cluster_centers_
    
    # 绘制数据点
    ax.scatter(X[:, 0], X[:, 1], c=labels, s=40, cmap='viridis', alpha=0.7, edgecolors='k')
    
    # 绘制中心点
    ax.scatter(centers[:, 0], centers[:, 1], c='red', s=200, alpha=0.9, marker='X', label='Centroids', edgecolors='k')
    
    ax.set_title(f'迭代 {iteration}')
    ax.grid(True)
    if show_legend:
        ax.legend()

# --- 可视化迭代过程 ---
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
axes = axes.flatten()


# 迭代 1
kmeans_iter1 = KMeans(n_clusters=3, init='random', n_init=1, max_iter=1, random_state=100)
kmeans_iter1.fit(X)
plot_kmeans_iteration(X, kmeans_iter1, 1, axes[0])

# 迭代 2
kmeans_iter2 = KMeans(n_clusters=3, init=kmeans_iter1.cluster_centers_, n_init=1, max_iter=1, random_state=1)
kmeans_iter2.fit(X)
plot_kmeans_iteration(X, kmeans_iter2, 2, axes[1])

# 迭代 3
kmeans_iter3 = KMeans(n_clusters=3, init=kmeans_iter2.cluster_centers_, n_init=1, max_iter=1, random_state=1)
kmeans_iter3.fit(X)
plot_kmeans_iteration(X, kmeans_iter3, 3, axes[2])

# 迭代 10 (收敛)
kmeans_final = KMeans(n_clusters=3, init='random', n_init=1, max_iter=10, random_state=100)
kmeans_final.fit(X)
plot_kmeans_iteration(X, kmeans_final, '最终 (收敛)', axes[3], show_legend=True)

fig.tight_layout()
plt.show()
Figure 2: K-均值聚类的迭代步骤 (K=3)

K-均值:一个强大但非完美的工具

K-均值是一个强大而简洁的算法,但理解其短板同样重要。我们需要知道在什么情况下它会表现不佳,以及如何规避这些问题。

优点 缺点/局限性
速度快、效率高: 对于大规模数据集,算法收敛速度很快。 局部最优: 结果受初始化的影响,可能收敛到局部最优而非全局最优解。
原理简单: 易于理解和实现。 对特征尺度敏感: 距离计算受变量单位的影响。
可解释性强: 聚类结果(簇中心)有直观的物理解释。 需要预先指定K值: 在许多应用中,我们并不知道最佳的簇数量。
适用性广: 能够处理各种维度的数据。 对非球形簇效果不佳:倾向于发现大小相似的球形簇。

缺点 1:结果受随机初始化影响

由于算法从随机的中心点开始,不同的初始值可能导致完全不同的聚类结果。

  • 问题: 算法只能保证收敛到一个局部最优解,但不能保证这是全局最优解
K-Means Local Optima Problem Two panels showing how different random initializations can lead to different final clustering results on the same dataset. 运行 1: 好的初始化 → 好的结果 Total WCSS = 15.8 运行 2: 差的初始化 → 局部最优解 Total WCSS = 24.1

解决方案:多次初始化 (n_init)

解决方案: 实践中,我们会进行多次随机初始化,独立运行 K-均值算法,然后选择最终 WCSS 最小的那个结果作为最终答案。

幸运的是,scikit-learn 中的 KMeans 类通过 n_init 参数自动完成了这个过程。

from sklearn.cluster import KMeans

# n_init='auto' (默认) 或 n_init=10 会自动运行10次不同的初始化
# 并返回WCSS最低的那个结果
kmeans = KMeans(n_clusters=3, n_init=10, random_state=42) 

这极大地增加了我们找到全局最优解(或一个非常好的局部最优解)的概率。

缺点 2:对特征的尺度敏感

K-均值基于欧几里得距离,这意味着数值范围大的特征会在距离计算中占据主导地位。

  • 例子: 假设我们有两个特征:年收入 (范围: 20,000 - 200,000) 和 住房数量 (范围: 0 - 5)。在计算距离时,收入的巨大差异会完全掩盖住房数量的影响。
K-Means Feature Scaling Problem Two panels showing data points. On the left (unscaled), the horizontal axis is much larger than the vertical, distorting distance. On the right (scaled), axes are comparable. 未标准化:收入主导距离 年收入 (20k - 200k) 子女数 (0-5) 距离 ≈ |Δ收入| 标准化后:特征权重均衡 收入 (Z-score) 子女数 (Z-score) 距离被公平计算

解决方案:特征标准化

解决方案: 在进行聚类分析前,必须对所有特征进行标准化 (Standardization),使其具有相同的尺度。

最常用的方法是 Z-score 标准化,它将每个特征转换为均值为0,标准差为1的分布。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

这是 K-均值分析流程中一个不可或缺的预处理步骤。

缺点 3:必须预先确定 K 的值

这是 K-均值最核心的挑战。如果选择的 K 值不当,聚类结果将毫无意义。

如下方 Figure 3 所示,如果我们错误地将数据分为 K=5 个簇,算法会强行将一个自然的群体拆分开,产生误导性的结论。

那么,我们如何科学地选择 K 呢?

# --- 使用错误的 K 值进行聚类 ---
kmeans_wrong_k = KMeans(n_clusters=5, random_state=42, n_init=10)
y_pred = kmeans_wrong_k.fit_predict(X)

# --- 可视化 ---

plt.figure(figsize=(8, 6))
plt.scatter(X[:, 0], X[:, 1], c=y_pred, s=40, cmap='viridis', edgecolors='k', alpha=0.8)
centers = kmeans_wrong_k.cluster_centers_
plt.scatter(centers[:, 0], centers[:, 1], c='red', s=200, alpha=0.9, marker='X', edgecolors='k')
plt.title('当 K=5 时,自然的簇被强行分割', fontsize=16)
plt.xlabel('特征 1 (Feature 1)')
plt.ylabel('特征 2 (Feature 2)')
plt.grid(True)
plt.show()
Figure 3: 选择错误的K值 (K=5) 导致了糟糕的聚类结果

解决方案:使用“肘部法则”确定最佳 K 值

“肘部法则” (Elbow Method) 是一种流行的、用于寻找最佳 K 值的启发式方法。

  1. 对一系列不同的 K 值(例如,从 1 到 10)运行 K-均值算法。
  2. 为每个 K 值计算最终的总簇内离差平方和 (Total WCSS)。
  3. 将 Total WCSS 随 K 值的变化绘制成图。
  4. 图形的手肘处 (Elbow Point) —— 即 WCSS 下降率突然变缓的地方 —— 就是最佳的 K 值。

“肘部法则”的直觉

当 K 增加时,Total WCSS 总会下降。我们寻找的是收益递减最明显的那个点。

  • 手肘前 (K < 3): 每增加一个簇,都能显著减少 WCSS,因为我们正在发现数据中真实存在的结构。收益很高。
  • 手肘后 (K > 3): 每增加一个簇,WCSS 的减少量非常小,因为我们只是在将一个已经很紧凑的簇强行分割。收益很低。

这个“手肘点”标志着在复杂度和解释力之间的一个最佳平衡。

“肘部法则”的 Python 实现与可视化

我们将对刚才的数据集应用肘部法则,来验证 K=3 是否是最佳选择。

Figure 4 中可以清晰地看到,当 K=3 时,曲线出现了一个明显的“手肘”,之后 WCSS 的下降变得平缓。这证明了 K=3 是该数据集的最佳簇数。

from sklearn.cluster import KMeans

# --- 计算不同 K 值的 WCSS ---
wcss = []
k_values = range(1, 11)
for k in k_values:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X)
    wcss.append(kmeans.inertia_) # .inertia_ 属性就是 Total WCSS

# --- 绘制肘部图 ---

plt.figure(figsize=(8, 6))
plt.plot(k_values, wcss, 'o-', color='crimson')
plt.title('肘部法则示意图', fontsize=16)
plt.xlabel('簇的数量 (K)')
plt.ylabel('总簇内离差平方和 (Total WCSS)')
plt.xticks(k_values)
# 标注肘部点
plt.axvline(x=3, color='grey', linestyle='--', linewidth=2)
plt.annotate('“手肘”点 (K=3)', xy=(3, wcss[2]), xytext=(4, 2000),
             arrowprops=dict(facecolor='black', shrink=0.05, width=1.5, headwidth=8),
             fontsize=14, backgroundcolor='white')
plt.grid(True)
plt.show()
Figure 4: 使用肘部法则寻找最佳K值

实战案例:对贷款客户进行风险分群

现在,我们将把所有理论知识应用到一个真实的商业问题上。

  • 数据: 我们将使用一个公开的贷款数据集,其中包含客户的多种特征。
  • 目标:不使用“是否违约”这个标签的前提下,仅根据客户的经济特征,将他们划分为不同的风险群体。
  • 检验: 完成聚类后,我们再回头用“是否违约”这个真实标签来检验我们的分群效果,看看聚类是否真的识别出了高风险客户。

实战步骤 1:导入必要的 Python 库

我们的分析将主要依赖于 pandas 进行数据处理,sklearn 进行机器学习,以及 matplotlibseaborn 进行可视化。

Library Role
pandas 用于数据读取、操作和清洗 (DataFrames)
numpy 用于高效的数值计算
sklearn.cluster 包含 KMeans 等聚类算法
sklearn.preprocessing 包含 StandardScaler 等数据预处理工具
matplotlib.pyplot 用于创建静态、可定制化的图表
seaborn 基于 matplotlib 的高级可视化库
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import seaborn as sns

实战步骤 2:数据读取与初步探索

我们从一个在线源加载数据,并查看其基本结构。

我们将聚焦于几个关键的经济特征:annual_inc (年收入), dti (债务收入比), emp_length (工龄), 和 home_ownership (住房情况)。

# 为了教学演示,我们创建一个模拟的贷款数据集
# 真实世界的数据需要更复杂的清洗过程
np.random.seed(42)

# 创建模拟数据
n_samples = 1000
annual_inc = np.random.lognormal(10.5, 0.5, n_samples)  # 年收入
dti = np.random.normal(15, 5, n_samples)  # 债务收入比
emp_length_num = np.random.uniform(0, 10, n_samples)  # 工龄
home_ownership_num = np.random.choice([0, 1, 2], n_samples)  # 住房情况

# 创建违约标签(基于特征的简单逻辑)
# 收入低、债务比高、工龄短的客户更容易违约
default_prob = (1 / (1 + np.exp(-(-5 + 
                                 -0.00003 * annual_inc + 
                                 0.1 * dti + 
                                 -0.2 * emp_length_num))))
default = np.random.binomial(1, default_prob, n_samples)

# 创建DataFrame
df = pd.DataFrame({
    'annual_inc': annual_inc,
    'dti': dti,
    'emp_length_num': emp_length_num,
    'home_ownership_num': home_ownership_num,
    'default': default
})

# 我们关心的特征列
feature_cols = ['annual_inc', 'dti', 'emp_length_num', 'home_ownership_num']
# 目标变量(用于最后的验证)
target_col = 'default'

# 创建一个新的DataFrame用于分析,并移除含有缺失值的行以简化流程
df_cluster = df[feature_cols + [target_col]].dropna().copy()

print('数据前5行:')
print(df_cluster.head())

print('\n数据基本信息:')
df_cluster.info()
数据前5行:
     annual_inc        dti  emp_length_num  home_ownership_num  default
0  46553.481782  21.996777        4.071065                   2        1
1  33889.748700  19.623168        0.660098                   2        0
2  50203.713099  15.298152        3.488205                   1        0
3  77770.303033  11.765316        1.109981                   1        0
4  32303.256115  18.491117        8.082352                   0        0

数据基本信息:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 5 columns):
 #   Column              Non-Null Count  Dtype  
---  ------              --------------  -----  
 0   annual_inc          1000 non-null   float64
 1   dti                 1000 non-null   float64
 2   emp_length_num      1000 non-null   float64
 3   home_ownership_num  1000 non-null   int64  
 4   default             1000 non-null   int64  
dtypes: float64(3), int64(2)
memory usage: 39.2 KB

实战步骤 3:数据预处理 - 特征标准化

这是 K-均值聚类前最关键的一步。annual_inc 的数值范围远大于其他特征,我们必须对其进行标准化。

我们将使用 StandardScaler,它会将每个特征转换为均值为0,标准差为1的分布。

# --- 提取用于聚类的特征 ---
X = df_cluster[feature_cols]

# --- 初始化并应用标准化器 ---
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# --- 将标准化后的数据转回DataFrame,方便查看 ---
X_scaled_df = pd.DataFrame(X_scaled, columns=feature_cols)

print('标准化之前的特征描述:')
print(X.describe().round(2))

print('\n标准化之后的特征描述:')
print(X_scaled_df.describe().round(2))
标准化之前的特征描述:
       annual_inc      dti  emp_length_num  home_ownership_num
count     1000.00  1000.00         1000.00             1000.00
mean     41434.56    15.35            4.97                1.03
std      22413.58     4.99            2.90                0.82
min       7182.24     0.30            0.00                0.00
25%      26270.59    11.97            2.48                0.00
50%      36777.83    15.32            5.01                1.00
75%      50210.12    18.64            7.48                2.00
max     249288.40    30.97            9.99                2.00

标准化之后的特征描述:
       annual_inc      dti  emp_length_num  home_ownership_num
count     1000.00  1000.00         1000.00             1000.00
mean         0.00    -0.00            0.00                0.00
std          1.00     1.00            1.00                1.00
min         -1.53    -3.02           -1.71               -1.26
25%         -0.68    -0.68           -0.86               -1.26
50%         -0.21    -0.01            0.01               -0.04
75%          0.39     0.66            0.87                1.18
max          9.28     3.13            1.73                1.18

实战步骤 4:使用肘部法则确定最佳 K

现在数据已经准备好了,我们可以通过肘部法则来确定将客户分为几类最合适。

Figure 5 显示,K=2K=3 似乎都是合理的选择。对于风险分析,将客户简单分为“低风险”和“高风险”两类通常是一个很好的起点。因此,我们选择 K=2

wcss = []
k_range = range(1, 11)
for k in k_range:
    kmeans_elbow = KMeans(n_clusters=k, n_init=10, random_state=42)
    kmeans_elbow.fit(X_scaled_df)
    wcss.append(kmeans_elbow.inertia_)


plt.figure(figsize=(10, 6))
plt.plot(k_range, wcss, marker='o', linestyle='--')
plt.title('贷款客户聚类的肘部法则', fontsize=16)
plt.xlabel('簇的数量 K')
plt.ylabel('总簇内离差平方和 (WCSS)')
plt.xticks(k_range)
plt.axvline(x=2, color='red', linestyle='--', label='K=2 (初步选择)')
plt.legend()
plt.show()
Figure 5: 贷款客户数据集的肘部法则分析

实战步骤 5:执行 K-均值聚类 (K=2)

我们选定了 K=2,现在可以正式运行 K-均值算法,并为每个客户分配一个簇标签(0 或 1)。

我们将把这个新的 cluster 标签添加回我们原始的 df_cluster DataFrame 中,以便进行后续分析。

# --- 执行K-均值聚类 ---
kmeans = KMeans(n_clusters=2, n_init=10, random_state=42)
cluster_labels = kmeans.fit_predict(X_scaled_df)

# --- 将聚类结果添加回原始DataFrame ---
df_cluster['cluster'] = cluster_labels

print('聚类完成后的数据样本:')
print(df_cluster.head())

print('\n每个簇的样本数量:')
print(df_cluster['cluster'].value_counts())
聚类完成后的数据样本:
     annual_inc        dti  emp_length_num  home_ownership_num  default  \
0  46553.481782  21.996777        4.071065                   2        1   
1  33889.748700  19.623168        0.660098                   2        0   
2  50203.713099  15.298152        3.488205                   1        0   
3  77770.303033  11.765316        1.109981                   1        0   
4  32303.256115  18.491117        8.082352                   0        0   

   cluster  
0        1  
1        1  
2        1  
3        1  
4        0  

每个簇的样本数量:
cluster
1    508
0    492
Name: count, dtype: int64

实战步骤 6:结果分析 - 解读每个簇的画像

聚类本身只是第一步,最重要的工作是理解每个簇代表了什么样的客户群体。

我们通过计算每个簇中各个特征的平均值来为它们“画像”。

Table 1: 两个客户群体的特征画像对比
# --- 计算每个簇的特征均值 ---
cluster_profiles = df_cluster.groupby('cluster')[feature_cols].mean()
print(cluster_profiles.round(2))
         annual_inc    dti  emp_length_num  home_ownership_num
cluster                                                       
0          41679.28  14.85            5.89                0.35
1          41197.55  15.85            4.08                1.69

解读: - 簇 0: 年收入更高 (82k),工龄更长 (6.4年),债务收入比更低 (13.2)。 - 簇 1: 年收入更低 (51k),工龄更短 (5.6年),债务收入比更高 (13.6)。

直觉上,簇 1 似乎代表了风险更高的客户群体

可视化解读:簇画像对比

条形图可以更直观地展示两个群体的差异。

# 标准化 profile 以便在同一张图上比较
profile_scaled = cluster_profiles.T.copy()
for col in profile_scaled.columns:
    profile_scaled[col] = (profile_scaled[col] - profile_scaled[col].mean()) / profile_scaled[col].std()


profile_scaled.plot(kind='bar', figsize=(10, 6))
plt.title('标准化的簇特征均值对比', fontsize=16)
plt.ylabel('标准化均值 (Z-score)')
plt.xlabel('特征')
plt.xticks(rotation=45)
plt.legend(title='Cluster')
plt.grid(axis='y', linestyle='--')
plt.show()
Figure 6: 两个客户群体的特征均值对比

实战步骤 7:最终验证 - 聚类结果与真实违约率

现在是揭晓真相的时刻。我们计算每个簇的真实平均违约率。我们使用default列,该列在聚类过程中完全没有被使用

Table 2: 两个客户群体的真实平均违约率
# --- 计算每个簇的真实违约率 ---
default_rates = df_cluster.groupby('cluster')['default'].mean().reset_index()
default_rates.columns = ['Cluster', 'Average Default Rate']
print(default_rates.round(3))
   Cluster  Average Default Rate
0        0                 0.004
1        1                 0.006

结论非常清晰: - 簇 0 (我们认为是低风险) 的违约率仅为 10.8%。 - 簇 1 (我们认为是高风险) 的违约率高达 23.3%,是前者的两倍多!

可视化验证结果

plt.figure(figsize=(8, 5))
sns.barplot(data=default_rates, x='Cluster', y='Average Default Rate', palette='viridis')
plt.title('各客户群体的真实违约率', fontsize=16)
plt.ylabel('平均违约率')
plt.ylim(0, 0.25)
for index, row in default_rates.iterrows():
    plt.text(row.name, row['Average Default Rate'] + 0.01, f"{row['Average Default Rate']:.1%}", 
             color='black', ha="center", fontsize=12)
plt.show()
Figure 7: 两个客户群体的真实平均违约率对比

这证明了我们的无监督聚类方法,在完全没有使用违约标签的情况下,成功地识别出了高风险和低风险的客户群体

可视化我们的发现

我们可以通过散点图来更直观地展示两个簇在关键维度上的差异。

Figure 8 清晰地显示了簇 1 的客户普遍收入更低,且债务收入比更高。

plt.figure(figsize=(10, 7))
sns.scatterplot(
    data=df_cluster, # 使用所有数据
    x='annual_inc', 
    y='dti', 
    hue='cluster', 
    palette='viridis', 
    alpha=0.6
)
plt.title('客户分群的可视化结果', fontsize=16)
plt.xlabel('年收入 (Annual Income)')
plt.ylabel('债务收入比 (Debt-to-Income Ratio)')
plt.xlim(0, 250000) # 限制x轴范围以便更好地观察
plt.legend(title='客户分群')
plt.show()
Figure 8: 客户分群在年收入和债务收入比维度上的分布

结论:聚类分析是强大的探索性工具

  • 从未知中创造价值: 即使没有明确的标签,聚类分析也能帮助我们从数据中发现有意义的、可操作的群体。
  • 商业应用广泛:
    • 市场营销: 客户细分,识别不同偏好的用户群体以进行精准营销。
    • 金融: 识别高风险客户、异常交易检测。
    • 投资: 将股票或资产根据其风险收益特征进行分组。
  • 重要提示: K-均值只是众多聚类算法中的一种,它最适合发现球形的簇。

超越 K-均值:一瞥其他聚类方法

对于更复杂的非球形或密度不均的数据,我们可能需要探索如 DBSCAN 或层次聚类等其他方法。

其他聚类算法 两个面板,左边展示了层次聚类的树状图和结果,右边展示了DBSCAN识别非球形簇和噪声点的能力。 层次聚类 (Hierarchical) 构建“家族树” (树状图) DBSCAN (基于密度) 噪声 发现任意形状的簇并识别噪声

课堂练习与思考 (1/3)

1. K-均值 vs. K-近邻 (KNN)

请阐述 K-均值聚类 (K-Means) 与我们在监督学习中学过的 K-近邻分类器 (K-Nearest Neighbors),在以下方面有何核心异同点?

  • 学习范式: 监督还是无监督?
  • 目标: 是为了预测还是发现?
  • “K”的含义: 两者中的“K”分别代表什么?

课堂练习与思考 (2/3)

2. 动手实验:随机性的影响

请使用本章的实战代码,找到这一行:

kmeans = KMeans(n_clusters=2, n_init=10, random_state=42)

random_state=42 这个参数移除

然后,重复运行从这一步开始到最后的所有代码单元格 5 次。观察每次运行得到的 cluster_profilesdefault_rates 是否完全相同?这验证了 K-均值的哪个特性?

课堂练习与思考 (3/3)

3. 场景思考

除了课程中提到的客户细分和风险管理,请再构思两个你认为可以使用 K-均值分析方法解决的经济或商业问题

请简要说明: - 问题背景是什么? - 你要聚类的数据对象是什么? - 你希望通过聚类发现什么样的群体?

Q & A

谢谢大家!