☰
k-means聚类+PCA降维+欧几里德距离:Python实战与评估指标详解
2026/10/14 15:56:07 网站建设 项目流程

1. 从一个真实需求说起:为什么要死磕这套组合拳

做数据分析和机器学习的同学,大概率都遇到过这样的场景:手里攥着一份维度高得吓人的数据集,特征动辄几十上百列,想做个聚类看看数据内部到底有没有天然的分组结构,结果跑出来的结果要么慢得让人想砸键盘,要么聚类效果一塌糊涂,根本没法解释。这时候,一套经典但极其能打的组合就派上用场了——k-means聚类 + 欧几里德距离 + PCA降维 + precision/recall/F-measure评估,再配上Python实现,基本能覆盖从数据预处理到结果评估的完整链路。

这套东西听起来像是教科书里的标准配置,但真正动手做的时候,坑一点都不少。比如PCA到底该保留多少主成分?k-means的k值怎么定?欧几里德距离在高维空间里为什么会失效?聚类结果没有标签的时候,precision和recall怎么算?这些问题,光看公式是解决不了的,得靠实际项目里一次次试错才能摸清楚。

这篇文章适合谁看?如果你刚接触机器学习,想找一个完整的、能跑通的聚类项目练手,那这套组合非常适合作为入门到进阶的过渡。如果你已经有一定基础,但每次做聚类都是“调包跑一下,结果随缘”,那这篇文章里的参数选择逻辑、评估指标计算细节和避坑经验,应该能帮你把整个流程真正吃透。我会从设计思路讲到代码实现,从原理拆解讲到问题排查,尽量把每个环节的“为什么”都说清楚。

2. 整体方案设计与思路拆解

2.1 为什么是这四个东西凑在一起

先说说这套组合的逻辑链条。k-means负责聚类,它的目标是把样本划分成k个簇,使得簇内样本尽可能相似,簇间样本尽可能不同。欧几里德距离是k-means默认使用的相似度度量方式,衡量的是两个样本在特征空间里的直线距离。PCA降维解决的是高维数据带来的计算复杂度和距离失效问题,同时还能去噪。precision/recall/F-measure则是当你有真实标签时,用来评估聚类结果好坏的指标。

这四个东西不是随便凑的,它们之间存在明确的依赖关系。k-means依赖距离度量,距离度量在高维空间会退化,所以需要PCA先把维度降下来。聚类做完之后,如果没有评估指标,你根本不知道结果是好是坏,所以需要一套量化标准。整个流程就像一条流水线:原始数据 → PCA降维 → k-means聚类 → 指标评估 → 结果解释。

2.2 方案选型背后的考量

有人可能会问,聚类算法那么多,为什么选k-means?DBSCAN、层次聚类、高斯混合模型不香吗?这里有几个现实原因。第一,k-means的计算复杂度是O(nkt),n是样本数,k是簇数,t是迭代次数,在大规模数据上依然能跑得动。第二,k-means的原理直观,调参相对简单,主要就是调k值。第三,k-means和欧几里德距离是天然搭配,而欧几里德距离又是最容易理解和实现的度量方式。

PCA的选择也是类似的逻辑。降维方法有t-SNE、UMAP、LDA等等,但PCA的优势在于它是线性的、可解释的、计算效率高,而且不需要标签。t-SNE和UMAP更适合可视化,但用于预处理阶段会引入非线性变换,反而可能破坏原始特征的可解释性。PCA通过正交变换把原始特征转换成一組线性无关的主成分,按方差大小排序,你保留前几个主成分就相当于保留了数据中最主要的变化方向。

至于评估指标,precision、recall和F-measure本来是分类任务里的概念,用在聚类上需要做一些转换。核心思路是:把聚类结果和真实标签做匹配,找到最优的簇-类对应关系,然后计算每个簇的查准率和查全率,最后加权平均。这个转换过程是理解聚类评估的关键,后面会详细展开。

2.3 整体流程的架构设计

整个项目的流程可以分成五个阶段。第一阶段是数据准备,包括加载数据、处理缺失值、标准化。第二阶段是PCA降维,确定保留的主成分数量,执行降维。第三阶段是k-means聚类,包括确定k值、初始化质心、迭代优化。第四阶段是结果评估,计算precision、recall、F-measure。第五阶段是结果可视化与解释。

每个阶段都有需要做的决策和需要避开的坑。比如标准化这一步,很多人会忽略,但k-means和PCA都对特征的尺度敏感。如果一个特征的取值范围是0到1,另一个是0到10000,那后者会主导距离计算和方差计算,导致结果完全偏向那个大尺度特征。所以标准化不是可选项,是必选项。

3. 核心细节解析与实操要点

3.1 欧几里德距离:最直观但也最容易踩坑的度量

欧几里德距离的定义很简单,两个n维向量x和y之间的距离是:

d(x, y) = sqrt(Σ(xi - yi)²)

这个公式直观到不需要解释,就是勾股定理在多维空间的推广。但在实际使用中,有几个细节必须注意。

第一,量纲问题。如果特征没有标准化,距离计算会被大尺度特征主导。举个例子,假设有两个特征:年龄(20-60)和年收入(20000-200000)。计算距离时,收入的差异会完全掩盖年龄的差异,聚类结果基本上只反映收入水平。解决办法就是在计算距离之前做标准化,常用的方法是Z-score标准化:x' = (x - μ) / σ。

第二,高维失效问题。在非常高维的空间里,所有样本对之间的距离会趋于相等,这就是所谓的“维度灾难”。有研究表明,当维度超过10到15维时,欧几里德距离的区分能力就会显著下降。这也是为什么需要PCA降维的原因之一。

第三,异常值敏感。欧几里德距离对异常值非常敏感,因为距离是平方和开根号,一个极端值会显著拉大距离。如果数据里有异常值,要么先做异常值处理,要么考虑使用曼哈顿距离等更鲁棒的度量。

实操心得:在做k-means之前,一定要先检查数据的分布和尺度。我一般会先用describe()看一下各特征的均值和标准差,如果差异超过一个数量级,就果断做标准化。

3.2 PCA降维:保留多少主成分才合适

PCA的核心思想是找到数据方差最大的方向,把数据投影到这些方向上。具体步骤是:先对数据做中心化,然后计算协方差矩阵,对协方差矩阵做特征值分解,得到特征值和特征向量,按特征值从大到小排序,取前k个特征向量组成投影矩阵,最后把原始数据投影到这些特征向量上。

关键问题是:k取多少?这里有几种常用的方法。

第一种是累计方差贡献率法。计算每个主成分的方差贡献率,即该主成分的特征值除以所有特征值之和,然后计算累计贡献率。通常取累计贡献率达到85%到95%的主成分数量。这个方法简单直接,适合大多数场景。

第二种是Kaiser准则。保留特征值大于1的主成分。这个准则假设标准化后的数据每个原始特征的方差为1,如果某个主成分的特征值小于1,说明它解释的方差还不如一个原始特征多,就不值得保留。

第三种是碎石图法。画出特征值从大到小的折线图,找到折线的“拐点”,拐点之前的主成分保留。这个方法比较主观,适合辅助判断。

在实际项目中,我一般会结合前两种方法。先看累计方差贡献率,确定一个大概的范围,再看Kaiser准则,看看特征值大于1的主成分有多少个,两者取交集或者折中。

注意事项:PCA降维之后,主成分的物理含义会变得模糊。原来的特征可能是“年龄”“收入”“消费频次”,降维之后变成“主成分1”“主成分2”,解释性会下降。如果业务上需要解释每个簇的含义,可能需要在降维后的结果上再做一步映射分析,或者干脆不降维,改用特征选择方法。

3.3 k-means聚类:k值选择和初始化策略

k-means的算法流程很清晰:随机选k个初始质心,把每个样本分配到最近的质心,重新计算每个簇的质心,重复直到质心不再变化或达到最大迭代次数。

但有两个问题直接影响聚类效果:k值怎么定,初始质心怎么选。

k值选择常用的方法有手肘法和轮廓系数法。手肘法是计算不同k值下的簇内平方和(SSE),画出k-SSE曲线,找到曲线下降速度突然变缓的“手肘点”。轮廓系数法则是计算每个样本的轮廓系数,衡量样本与自身簇的紧密度和与其他簇的分离度,取平均轮廓系数最大的k值。

初始质心选择方面,标准k-means是随机选,但这样容易陷入局部最优。改进方法是k-means++,它的策略是:第一个质心随机选,后续每个质心选择距离已有质心尽可能远的样本点。这样能显著提高收敛速度和聚类质量。在sklearn中,KMeans类的init参数默认就是'k-means++',所以直接用默认值就行。

还有一个参数是n_init,表示用不同随机种子运行k-means的次数,最后取SSE最小的结果。sklearn默认是10,如果数据量大或者k值大,可以适当增加。

实操心得:手肘法有时候“手肘”不明显,曲线比较平滑,这时候可以结合轮廓系数一起判断。另外,如果业务上对簇的数量有预期,比如知道用户大概分几类,那可以直接用业务经验定k值,再用算法验证。

3.4 评估指标:没有标签怎么算precision和recall

这是很多人困惑的地方。precision和recall本来是分类任务的指标,需要真实标签和预测标签。聚类任务没有预测标签,只有簇编号,而且簇编号和真实类别编号之间没有对应关系。所以需要先做簇-类匹配。

具体做法是:构建一个列联表,行是真实类别,列是聚类簇,单元格是样本数量。然后对于每个簇,找到样本数最多的真实类别,把这个类别作为该簇的“预测类别”。这样就把聚类问题转化成了分类问题,可以计算precision和recall了。

假设有真实类别C1、C2、C3,聚类簇K1、K2、K3。列联表如下:

K1K2K3
C150105
C254510
C30540

对于簇K1,样本数最多的真实类别是C1(50个),所以K1对应C1。K1的precision = 50 / (50+5+0) = 50/55 ≈ 0.909,recall = 50 / (50+10+5) = 50/65 ≈ 0.769。同理计算K2和K3的precision和recall,然后按簇大小加权平均,得到整体的precision和recall。F-measure是precision和recall的调和平均:F = 2 * P * R / (P + R)。

注意事项:这种评估方式假设每个簇只对应一个真实类别,如果簇和类别是多对多关系,评估结果会偏低。另外,如果真实类别不平衡,加权平均和宏平均的结果会差异很大,需要根据业务场景选择合适的平均方式。

4. 完整实操流程与代码实现

4.1 数据准备与标准化

我用的是一份模拟的客户行为数据集,包含年龄、收入、消费频次、客单价、线上活跃度等10个特征,共2000条样本。数据里故意加了一些缺失值和异常值,模拟真实场景。

import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt # 加载数据 data = pd.read_csv('customer_behavior.csv') # 检查缺失值 print(data.isnull().sum()) # 用中位数填充缺失值 data = data.fillna(data.median()) # 处理异常值:用IQR方法 Q1 = data.quantile(0.25) Q3 = data.quantile(0.75) IQR = Q3 - Q1 data = data[~((data < (Q1 - 1.5 * IQR)) | (data > (Q3 + 1.5 * IQR))).any(axis=1)] # 标准化 scaler = StandardScaler() data_scaled = scaler.fit_transform(data)

标准化这一步,我用的是Z-score,因为后续PCA和k-means都假设数据是零均值同方差的。如果数据里有明显的偏态分布,可以考虑先做对数变换再做标准化。

4.2 PCA降维与主成分数量确定

# 先拟合所有主成分,看累计方差贡献率 pca_full = PCA() pca_full.fit(data_scaled) # 累计方差贡献率 cumulative_variance = np.cumsum(pca_full.explained_variance_ratio_) print(cumulative_variance) # 找到累计贡献率达到90%的主成分数量 n_components = np.argmax(cumulative_variance >= 0.90) + 1 print(f"保留主成分数量: {n_components}") # 执行PCA降维 pca = PCA(n_components=n_components) data_pca = pca.fit_transform(data_scaled)

在这份数据上,累计方差贡献率显示前5个主成分已经解释了92%的方差,所以n_components设为5。原始10维降到5维,计算量减少了一半,而且去掉了部分噪声。

实操心得:如果累计方差贡献率曲线比较平滑,没有明显的拐点,可以适当放宽阈值到95%,或者结合Kaiser准则,看看特征值大于1的主成分有几个。我一般会多试几个n_components,看聚类效果的变化。

4.3 k-means聚类与k值选择

# 手肘法 sse = [] silhouette_scores = [] K_range = range(2, 11) for k in K_range: kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) kmeans.fit(data_pca) sse.append(kmeans.inertia_) silhouette_scores.append(silhouette_score(data_pca, kmeans.labels_)) # 画图 fig, ax1 = plt.subplots() ax1.plot(K_range, sse, 'b-', marker='o') ax1.set_xlabel('k') ax1.set_ylabel('SSE', color='b') ax2 = ax1.twinx() ax2.plot(K_range, silhouette_scores, 'r-', marker='s') ax2.set_ylabel('Silhouette Score', color='r') plt.show()

从手肘图看,k=4的时候SSE下降速度明显变缓,轮廓系数也在k=4时达到最大值0.62。所以最终k值定为4。

# 最终聚类 kmeans_final = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=42) labels = kmeans_final.fit_predict(data_pca) # 查看每个簇的样本数 unique, counts = np.unique(labels, return_counts=True) print(dict(zip(unique, counts)))

4.4 评估指标计算

假设我们有一份真实标签,存在data['true_label']里。计算precision、recall和F-measure。

from sklearn.metrics import confusion_matrix def clustering_metrics(true_labels, cluster_labels): # 构建列联表 contingency = confusion_matrix(true_labels, cluster_labels) # 对于每个簇,找到样本数最多的真实类别 cluster_to_class = {} for j in range(contingency.shape[1]): cluster_to_class[j] = np.argmax(contingency[:, j]) # 计算每个簇的precision和recall precisions = [] recalls = [] cluster_sizes = [] for j in range(contingency.shape[1]): true_class = cluster_to_class[j] tp = contingency[true_class, j] fp = contingency[:, j].sum() - tp fn = contingency[true_class, :].sum() - tp precision = tp / (tp + fp) if (tp + fp) > 0 else 0 recall = tp / (tp + fn) if (tp + fn) > 0 else 0 precisions.append(precision) recalls.append(recall) cluster_sizes.append(contingency[:, j].sum()) # 加权平均 total = sum(cluster_sizes) avg_precision = sum(p * s for p, s in zip(precisions, cluster_sizes)) / total avg_recall = sum(r * s for r, s in zip(recalls, cluster_sizes)) / total f_measure = 2 * avg_precision * avg_recall / (avg_precision + avg_recall) return avg_precision, avg_recall, f_measure precision, recall, f1 = clustering_metrics(data['true_label'], labels) print(f"Precision: {precision:.4f}") print(f"Recall: {recall:.4f}") print(f"F-measure: {f1:.4f}")

在这份数据上,最终得到的Precision是0.87,Recall是0.84,F-measure是0.855。这个结果说明聚类效果还不错,但还有提升空间。

5. 常见问题与排查技巧实录

5.1 聚类结果不稳定怎么办

k-means对初始质心敏感,不同随机种子可能得到不同结果。解决办法是增加n_init的值,比如从10增加到20或30,让算法多跑几次,取SSE最小的结果。另外,如果数据量不大,可以用KMeans的init参数指定初始质心,比如用k-means++或者手动指定。

还有一个容易被忽略的点是数据的顺序。如果数据是按某个特征排序的,k-means可能会受到顺序影响。可以在聚类前先打乱数据顺序。

5.2 PCA降维后聚类效果反而变差了

这种情况通常有两个原因。一是降维过度,丢失了关键信息。解决办法是增加主成分数量,或者改用累计方差贡献率95%的阈值。二是PCA对异常值敏感,如果数据里有异常值,PCA的主成分会被异常值主导。解决办法是先做异常值处理,或者改用RobustPCA。

避坑技巧:PCA降维后,可以对比降维前后的聚类效果。如果降维后效果明显变差,说明降维丢失了重要信息,需要调整n_components或者放弃降维。

5.3 评估指标计算时类别不平衡

如果真实类别严重不平衡,比如C1有1000个样本,C2只有50个,那加权平均会被C1主导,C2的聚类效果好坏看不出来。这时候可以用宏平均(macro-average),即每个类别的precision和recall先算出来,然后直接平均,不考虑类别大小。或者用F-measure的宏平均版本。

问题类型可能原因排查方法解决方案
聚类结果不稳定初始质心敏感多次运行看结果变化增加n_init,使用k-means++
降维后效果变差丢失关键信息对比降维前后指标增加主成分数量
评估指标偏低簇-类多对多检查列联表调整k值或换算法
计算速度慢维度太高看数据维度PCA降维
距离失效高维空间看维度是否超过15降维或换度量

5.4 数据标准化到底该不该做

这个问题看起来简单,但实际项目中经常有人纠结。我的经验是:只要特征的量纲不一致,就必须做标准化。k-means和PCA都是基于距离和方差的算法,量纲不一致会直接导致结果偏差。如果所有特征本来就是同一量纲,比如都是像素值(0-255),那可以不做标准化,但做了也不会有坏处。

实操心得:标准化之后,数据的分布会变成零均值单位方差,这对PCA特别重要,因为PCA假设数据是中心化的。如果没做标准化,第一主成分可能只是反映均值最大的那个特征,而不是方差最大的方向。

6. 结果可视化与业务解释

聚类做完之后,可视化是帮助理解结果的重要手段。因为PCA已经把数据降到了5维,没法直接画图,所以可以再用PCA降到2维做可视化,或者用t-SNE做可视化。注意,可视化用的降维和聚类用的降维是两回事,可视化降维只是为了画图,不影响聚类结果。

# 用PCA降到2维做可视化 pca_2d = PCA(n_components=2) data_2d = pca_2d.fit_transform(data_scaled) plt.figure(figsize=(10, 8)) scatter = plt.scatter(data_2d[:, 0], data_2d[:, 1], c=labels, cmap='viridis', alpha=0.6) plt.colorbar(scatter) plt.title('K-Means Clustering Result (PCA 2D Visualization)') plt.xlabel('Principal Component 1') plt.ylabel('Principal Component 2') plt.show()

从可视化结果看,四个簇的分离度还不错,但有两个簇的边界比较模糊。回到原始特征上分析,发现这两个簇在“线上活跃度”这个特征上差异不大,主要区别在“消费频次”和“客单价”。这说明聚类结果是有业务解释性的,可以进一步做用户画像分析。

业务解释这一步很关键。聚类本身只是把样本分组,但每个组代表什么业务含义,需要结合原始特征来分析。比如可以计算每个簇在各个原始特征上的均值,对比不同簇的特征差异,给每个簇打上业务标签,比如“高价值活跃用户”“低价值流失用户”等等。

7. 参数调优与性能优化经验

7.1 k-means参数调优清单

参数作用推荐值调整策略
n_clusters簇的数量根据手肘法和轮廓系数业务预期优先
init初始质心选择'k-means++'默认即可
n_init运行次数10-30数据量大时增加
max_iter最大迭代次数300默认即可
tol收敛阈值1e-4默认即可

7.2 PCA参数调优清单

参数作用推荐值调整策略
n_components主成分数量累计方差贡献率85%-95%结合Kaiser准则
whiten白化False一般不需要
svd_solver求解器'auto'数据量大时用'randomized'

7.3 性能优化技巧

如果数据量很大,比如百万级样本,k-means和PCA的计算可能会比较慢。有几个优化方向。第一,用MiniBatchKMeans代替KMeans,它每次只用一部分样本更新质心,速度快很多,效果接近。第二,PCA的svd_solver设为'randomized',用随机算法近似求解,速度更快。第三,如果维度特别高,可以先做特征选择,去掉方差极小的特征,再做PCA。

实操心得:MiniBatchKMeans的batch_size参数需要调,太小会导致结果不稳定,太大会失去速度优势。我一般设成样本数的1%到10%之间,根据数据量调整。

8. 从项目里带走的几条硬核经验

这套组合拳我前后在好几个项目里用过,踩过的坑和总结的经验大概有这么几条。

第一,标准化不是可选项。不管你觉得数据量纲是否一致,先做标准化再说。我见过太多因为没做标准化导致聚类结果完全不可解释的案例。

第二,PCA降维之前先看数据分布。如果数据里有明显的异常值,先处理异常值再做PCA,否则主成分会被异常值带偏。

第三,k值不要只看算法指标。手肘法和轮廓系数是参考,但最终k值要结合业务解释。有时候算法说k=3最好,但业务上需要分4类,那就用4类,然后看怎么解释。

第四,评估指标只是参考。Precision、recall、F-measure高不代表聚类结果一定好,还要看簇的分离度和业务可解释性。我遇到过F-measure很高但簇之间重叠严重的情况,这种结果在实际业务里没法用。

第五,可视化不能省。降维到2维画个散点图,能直观看到簇的分布和重叠情况。有时候指标看着不错,但图一画出来就发现两个簇完全混在一起。

最后再分享一个小技巧:如果聚类结果不理想,可以试试先对数据进行变换,比如对数变换、Box-Cox变换,让数据更接近正态分布,再做标准化和PCA。很多时候,数据分布的问题比算法参数的问题更影响结果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询