
1. 项目缘起为什么用Python做聚类分析如果你正在处理一堆看起来杂乱无章的数据想从中找出一些内在的规律把相似的东西归到一起那么你很可能需要“聚类分析”。这就像整理一个混乱的衣柜把衬衫、裤子、外套分别放到不同的区域只不过我们整理的对象是数据点。在数学建模、数据分析、市场细分、用户画像构建等场景下聚类都是一个非常基础且强大的工具。过去很多人一提到统计分析尤其是聚类第一个想到的就是SPSS。确实SPSS的图形化界面和预设流程对新手非常友好点点鼠标就能出结果。但它的局限性也很明显流程固定、灵活性差、难以集成到自动化流水线中、处理大规模数据时可能力不从心而且正版软件费用不菲。相比之下Python凭借其开源、免费、生态丰富、灵活度极高的特点已经成为数据科学领域的事实标准。用Python做聚类意味着你可以从数据清洗、特征工程、模型训练、结果可视化到模型部署实现全流程的自主控制和自动化这对于需要复现、迭代或嵌入到更大系统中的数学建模项目来说是至关重要的。网络上关于“Python安装”、“Python环境配置”的搜索热度一直很高这恰恰说明了有大量的人正试图从SPSS这类工具转向Python但在起步阶段就遇到了环境搭建的“拦路虎”。同时像“人狗大作战Python代码”这类趣味项目也反映了大家希望通过有趣的应用来学习Python。本文将从一个数学建模实践者的角度手把手带你用Python实现聚类分析不仅告诉你“怎么做”更会深入解释“为什么这么做”并分享那些官方文档里不会写的实操经验和避坑指南。2. 环境奠基构建稳健的Python数据分析工作区工欲善其事必先利其器。一个混乱、包冲突的Python环境是数据科学工作最大的噩梦之一。很多人卡在“请安装缺失的包以使用此工作流”这类错误上其根源往往在于环境管理不当。2.1 Python解释器与包管理器的选择首先忘掉系统自带的Python。直接从Python官网下载并安装最新稳定版如Python 3.11。安装时务必勾选“Add Python to PATH”这是避免后续在命令行中找不到python和pip命令的关键一步。安装完成后打开终端Windows用CMD或PowerShellMac/Linux用Terminal输入python --version和pip --version验证安装成功。接下来是包管理器。pip是标配但对于复杂项目我强烈推荐使用conda或uv。conda不仅管理Python包还能管理非Python依赖和环境特别适合科学计算领域。uv是一个用Rust写的、速度极快的Python包安装器和解析器近年来备受关注。对于新手可以从pip开始对于涉及复杂本地库如某些机器学习库需要特定版本的CUDA的项目conda更有优势追求极致速度和现代工作流的可以尝试uv。2.2 虚拟环境项目隔离的黄金法则这是必须养成的习惯。永远不要在全局Python环境中安装项目依赖。为每个项目创建独立的虚拟环境可以彻底避免包版本冲突。假设你的项目文件夹叫cluster_analysis。使用venvPython内置# 在项目根目录下执行 python -m venv venv # 激活环境 # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate激活后你的命令行提示符前会出现(venv)字样表示你已进入该虚拟环境。在此环境下用pip install安装的所有包都只属于这个项目。使用condaconda create -n cluster_analysis python3.11 conda activate cluster_analysis2.3 核心库安装与“依赖地狱”规避对于聚类分析我们需要以下核心库请在激活的虚拟环境中安装# 基础三件套数据处理、科学计算、可视化 pip install numpy pandas matplotlib seaborn # 机器学习核心库 pip install scikit-learn # 高级可视化可选但推荐 pip install plotly这里有一个关键经验不要一次性安装所有库也不要盲目安装最新版。特别是scikit-learn它的版本可能与numpy、scipy存在依赖关系。一个稳健的做法是先安装一个较新的、但非最新的稳定版本组合。例如pip install numpy1.24.3 pandas2.0.3 scikit-learn1.3.0如果后续需要其他库如用于降维可视化的umap-learn再按需安装。遇到安装错误时仔细阅读错误信息通常是某个底层C库缺失在Windows上常见或版本不兼容。对于Windows用户可以从 这个网站 下载预编译的.whl文件进行安装常能解决棘手的编译问题。2.4 开发工具配置VSCode的高效设置VSCode是当前非常流行的轻量级编辑器。配置Python环境只需几步安装Python扩展由Microsoft发布。打开你的项目文件夹。按下CtrlShiftP输入“Python: Select Interpreter”选择你刚创建的虚拟环境中的python.exe通常在venv/Scripts/或venv/bin/目录下。创建一个.vscode/settings.json文件可以配置代码格式化工具如black、静态检查如pylint等让开发更顺畅。至此一个干净、隔离、专用于聚类分析项目的Python环境就搭建好了。记住所有后续操作都在这个激活的虚拟环境中进行。3. 数据炼金术聚类前的预处理与探索没有高质量的数据再好的聚类算法也是徒劳。数据预处理通常要花费整个项目60%以上的时间。我们以一个模拟的客户数据集为例假设我们有一家电商的客户消费数据包含“年龄”、“年收入万元”、“消费频率次/月”、“平均客单价元”等字段。3.1 数据加载与初步审视import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 设置中文显示和图表样式如果数据中有中文 plt.rcParams[font.sans-serif] [SimHei, DejaVu Sans] plt.rcParams[axes.unicode_minus] False sns.set_style(whitegrid) # 假设我们从CSV文件加载数据 # df pd.read_csv(customer_data.csv) # 这里我们模拟生成一些数据 np.random.seed(42) n_samples 300 df pd.DataFrame({ 年龄: np.random.randint(18, 70, n_samples), 年收入_万元: np.random.exponential(scale15, sizen_samples).round(1) 10, # 右偏分布 消费频率_次月: np.random.poisson(lam4, sizen_samples), 平均客单价_元: np.random.normal(loc150, scale50, sizen_samples).round(0) }) # 故意加入一些缺失值和异常值 df.iloc[10, 1] np.nan df.iloc[50, 2] 100 # 异常高的消费频率 df.iloc[150, 3] -10 # 不可能的客单价 print(数据形状:, df.shape) print(\n前5行数据:) print(df.head()) print(\n数据基本信息:) print(df.info()) print(\n描述性统计:) print(df.describe())运行这段代码你会立刻对数据有一个整体认识有多少行、多少列、每列的数据类型、是否有缺失值、数据的分布范围均值、标准差、最小最大值等。df.describe()对于快速发现异常值非常有用比如“平均客单价_元”的最小值是-10这显然是不合理的。3.2 缺失值与异常值处理缺失值处理对于聚类分析简单删除缺失值过多的样本或特征是一种方法。如果缺失不多可以考虑填充。数值型数据常用中位数或均值填充类别型数据用众数填充。但要注意填充可能会引入偏差特别是当数据不是随机缺失时。在我们的例子中只有一处缺失我们可以用该列的中位数填充。# 检查每列缺失值数量 print(df.isnull().sum()) # 用中位数填充数值列缺失值 df_filled df.copy() for col in df_filled.select_dtypes(include[np.number]).columns: median_val df_filled[col].median() df_filled[col].fillna(median_val, inplaceTrue)异常值处理异常值会严重扭曲聚类结果尤其是基于距离的算法如K-Means。识别异常值有多种方法标准差法假设数据服从正态分布将超出均值±3倍标准差范围的值视为异常值。但对于非正态分布数据不适用。箱线图法IQR法更稳健不依赖于分布假设。将小于Q1-1.5IQR或大于Q31.5IQR的值视为异常值。# 使用箱线图可视化异常值 fig, axes plt.subplots(2, 2, figsize(12, 8)) axes axes.ravel() for idx, col in enumerate([年龄, 年收入_万元, 消费频率_次月, 平均客单价_元]): df_filled.boxplot(columncol, axaxes[idx]) axes[idx].set_title(f{col} 箱线图) plt.tight_layout() plt.show() # 基于IQR法识别并处理异常值以‘消费频率_次月’为例 Q1 df_filled[消费频率_次月].quantile(0.25) Q3 df_filled[消费频率_次月].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR # 将异常值用边界值替换盖帽法这是一种较温和的处理方式 df_cleaned df_filled.copy() df_cleaned[消费频率_次月] np.clip(df_cleaned[消费频率_次月], lower_bound, upper_bound) # 对于‘平均客单价_元’的负值可以直接用最小值替换或视为缺失值处理 df_cleaned[平均客单价_元] df_cleaned[平均客单价_元].apply(lambda x: x if x 0 else df_cleaned[平均客单价_元][df_cleaned[平均客单价_元]0].min())处理异常值时没有绝对正确的答案。需要结合业务理解来判断一个值是真正的“异常行为”还是“数据错误”。盖帽法、删除或替换为缺失值后再填充都是常见策略。3.3 特征标准化为什么这是聚类的必选项这是聚类分析中最关键的一步之一。我们的特征“年龄”范围可能在18-70“年收入”范围在10-100。如果不进行标准化计算距离时“年收入”的微小差异如1万元就会完全主导“年龄”的巨大差异如10岁这显然不符合我们对“相似度”的直观理解。常用标准化方法Z-Score标准化将数据缩放到均值为0标准差为1。适用于数据大致服从正态分布的情况。x_new (x - mean) / stdMin-Max归一化将数据缩放到[0, 1]区间。x_new (x - min) / (max - min)Robust标准化使用中位数和四分位距IQR进行缩放对异常值不敏感。对于聚类我通常首选Z-Score标准化因为它能保持数据的分布形状并且使不同特征具有可比性。使用scikit-learn的StandardScaler可以轻松实现from sklearn.preprocessing import StandardScaler # 我们需要聚类的特征列 features_for_clustering [年龄, 年收入_万元, 消费频率_次月, 平均客单价_元] X df_cleaned[features_for_clustering] # 初始化标准化器拟合并转换数据 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 转换回DataFrame便于查看 X_scaled_df pd.DataFrame(X_scaled, columnsfeatures_for_clustering) print(X_scaled_df.describe()) # 现在均值约等于0标准差约等于1现在所有特征都处于同一量级聚类算法可以公平地对待每一个特征。4. 算法核心K-Means聚类实战与肘部法则经过充分预处理的数据X_scaled已经准备好了。现在进入核心环节选择算法并实施聚类。K-Means是最著名、最常用的聚类算法它试图将数据划分为K个簇使得每个点到其所属簇中心的距离平方和最小。4.1 K-Means算法原理简述算法流程可以概括为初始化随机选择K个点作为初始簇中心质心。分配将每个数据点分配到距离它最近的质心所在的簇。更新重新计算每个簇中所有点的平均值作为新的质心。迭代重复步骤2和3直到质心的位置不再发生显著变化或达到最大迭代次数。它的目标是最小化簇内平方和Within-Cluster Sum of Squares, WCSS也称为“惯性Inertia”。WCSS越小说明簇内样本越相似。4.2 寻找最佳的K值肘部法则与轮廓系数K-Means最大的挑战在于K值簇的数量需要事先指定。如何确定K这里介绍两种最实用的方法。方法一肘部法则原理是计算不同K值下的WCSS并绘制曲线。WCSS会随着K增大而减小因为每个簇会更精细。我们寻找一个点增加K所带来的WCSS下降幅度突然变缓这个点像“肘部”对应的K值就是较好的选择。from sklearn.cluster import KMeans # 尝试不同的K值 wcss [] k_range range(1, 11) # 通常尝试1到10 for k in k_range: kmeans KMeans(n_clustersk, initk-means, n_init10, random_state42) kmeans.fit(X_scaled) wcss.append(kmeans.inertia_) # inertia_属性即WCSS # 绘制肘部法则图 plt.figure(figsize(8, 5)) plt.plot(k_range, wcss, bo-) plt.xlabel(簇的数量 K) plt.ylabel(簇内平方和 (WCSS)) plt.title(肘部法则寻找最佳K值) plt.grid(True) plt.show()观察生成的折线图寻找那个“拐弯”的点。例如可能在K3或K4处曲线从陡峭变得平缓。这个点就是肘部对应的K可以作为候选。方法二轮廓系数轮廓系数结合了簇内的凝聚度和簇间的分离度。对于每个样本点ia(i)i到同簇其他点的平均距离凝聚度。b(i)i到其他簇中所有点的平均距离的最小值分离度。轮廓系数 s(i) (b(i) - a(i)) / max(a(i), b(i))s(i)的取值范围在[-1, 1]之间。越接近1说明样本i聚类越合理越接近-1说明样本i更应该被分配到相邻的簇接近0则说明样本i在两个簇的边界上。所有样本的轮廓系数的平均值可以用来评估整体聚类效果。from sklearn.metrics import silhouette_score silhouette_scores [] k_range range(2, 11) # 轮廓系数至少需要2个簇 for k in k_range: kmeans KMeans(n_clustersk, initk-means, n_init10, random_state42) cluster_labels kmeans.fit_predict(X_scaled) silhouette_avg silhouette_score(X_scaled, cluster_labels) silhouette_scores.append(silhouette_avg) print(fK{k}时轮廓系数为: {silhouette_avg:.4f}) plt.figure(figsize(8, 5)) plt.plot(k_range, silhouette_scores, ro-) plt.xlabel(簇的数量 K) plt.ylabel(轮廓系数平均值) plt.title(轮廓系数法寻找最佳K值) plt.grid(True) plt.show()选择轮廓系数最大的K值。通常肘部法则和轮廓系数会给出相近的建议可以综合两者进行判断。假设我们通过两种方法认为K3是一个不错的选择。4.3 执行聚类与结果解读# 使用K3进行最终聚类 optimal_k 3 final_kmeans KMeans(n_clustersoptimal_k, initk-means, n_init10, random_state42) cluster_labels final_kmeans.fit_predict(X_scaled) # 将聚类标签添加回原始数据或清洗后的数据便于分析 df_cleaned[Cluster] cluster_labels # 查看每个簇的大小 print(df_cleaned[Cluster].value_counts().sort_index()) # 查看每个簇在各个特征上的均值反标准化到原始尺度更易解释 cluster_profile df_cleaned.groupby(Cluster)[features_for_clustering].mean() print(\n各簇特征均值:) print(cluster_profile)现在我们可以初步解读这三个客户群簇0可能代表“低收入、低频次”客户。簇1可能代表“高收入、高客单价”的优质客户。簇2可能代表“中等收入、高频次”的活跃客户。 这只是一个示例真实业务解读需要结合更多背景知识。5. 可视化与评估让聚类结果“看得见摸得着”聚类结果不能只停留在数字上必须通过可视化来验证其合理性和发现深层信息。5.1 二维投影可视化我们的数据是四维的无法直接可视化。需要先进行降维。最常用的方法是主成分分析PCA它将数据投影到方差最大的两个或三个主成分上尽可能保留原始信息。from sklearn.decomposition import PCA # 降维到2D用于绘图 pca PCA(n_components2) X_pca pca.fit_transform(X_scaled) # 创建一个包含PCA结果和簇标签的DataFrame vis_df pd.DataFrame(X_pca, columns[PC1, PC2]) vis_df[Cluster] cluster_labels # 绘制散点图 plt.figure(figsize(10, 8)) scatter plt.scatter(vis_df[PC1], vis_df[PC2], cvis_df[Cluster], cmapviridis, alpha0.7, edgecolorsk, s50) plt.xlabel(第一主成分 (PC1)) plt.ylabel(第二主成分 (PC2)) plt.title(客户数据聚类结果PCA降维) plt.colorbar(scatter, label簇标签) plt.grid(True, linestyle--, alpha0.5) # 标注每个簇的质心在PCA空间的位置 centers_pca pca.transform(final_kmeans.cluster_centers_) plt.scatter(centers_pca[:, 0], centers_pca[:, 1], cred, markerX, s200, label簇中心) plt.legend() plt.show() # 查看PCA解释的方差比例 print(fPC1解释了 {pca.explained_variance_ratio_[0]:.2%} 的方差) print(fPC2解释了 {pca.explained_variance_ratio_[1]:.2%} 的方差) print(f前两个主成分累计解释了 {pca.explained_variance_ratio_.sum():.2%} 的方差)如果累计方差解释率较高如70%说明这个二维投影较好地代表了原始数据图中的簇分离情况是可信的。如果很低则说明数据在高维空间中结构复杂二维视图丢失了很多信息此时需要谨慎解读图形。5.2 平行坐标图与雷达图对于多维数据平行坐标图是展示各簇特征分布的强大工具。from pandas.plotting import parallel_coordinates # 为平行坐标图准备数据需要标准化前的原始值或缩放后的值来观察差异 # 这里使用标准化后的值使所有特征在同一尺度 plot_df X_scaled_df.copy() plot_df[Cluster] cluster_labels plt.figure(figsize(12, 6)) parallel_coordinates(plot_df, Cluster, colormapviridis, alpha0.5) plt.title(平行坐标图各簇特征分布) plt.xlabel(特征) plt.ylabel(标准化后的值) plt.grid(True) plt.show()通过平行坐标图你可以清晰地看到每个簇在哪些特征上具有高值或低值从而更精确地定义每个簇的轮廓。例如簇1的线可能在“年收入”和“客单价”上位置很高而在“频率”上位置较低。5.3 聚类效果的定量评估除了轮廓系数还有一些内部评估指标不依赖真实标签戴维森堡丁指数值越小越好代表簇内距离小簇间距离大。卡林斯基-哈拉巴斯指数值越大越好。from sklearn.metrics import davies_bouldin_score, calinski_harabasz_score db_index davies_bouldin_score(X_scaled, cluster_labels) ch_index calinski_harabasz_score(X_scaled, cluster_labels) print(f戴维森堡丁指数 (越低越好): {db_index:.4f}) print(f卡林斯基-哈拉巴斯指数 (越高越好): {ch_index:.4f})这些指标可以与不同K值或不同算法下的结果进行对比辅助决策。但切记没有绝对“最好”的指标聚类最终要服务于业务目标结果的可解释性和实用性往往比指标高零点几分更重要。6. 进阶探索其他聚类算法与缺失值处理K-Means虽好但并非万能。它假设簇是凸形的、各向同性的并且对异常值敏感。当数据形状复杂或存在特殊需求时需要其他算法。6.1 层次聚类层次聚类不需要预先指定K值它会构建一个树状图谱系图展示数据点如何一步步合并或分裂。你可以通过设定距离阈值或查看树状图来决定在何处切割形成最终的簇。from scipy.cluster.hierarchy import dendrogram, linkage from matplotlib import pyplot as plt # 计算连接矩阵这里使用沃德法Wards method最小化簇内方差 linked linkage(X_scaled, methodward) # 绘制树状图 plt.figure(figsize(12, 7)) dendrogram(linked, orientationtop, distance_sortdescending, show_leaf_countsTrue, truncate_modelastp, # 只显示最后p个合并 p20) # 显示最后20次合并 plt.title(层次聚类树状图) plt.xlabel(样本索引或簇大小) plt.ylabel(距离) plt.show()通过树状图你可以直观地看到数据的层次结构并根据纵向“距离”的突然增大来选择切割点从而确定簇数。6.2 基于密度的DBSCANDBSCAN非常适合发现任意形状的簇并能识别噪声点异常值。它不需要指定簇数而是定义两个参数eps邻域半径和min_samples核心点所需的最小邻域样本数。from sklearn.cluster import DBSCAN dbscan DBSCAN(eps0.5, min_samples5) dbscan_labels dbscan.fit_predict(X_scaled) # 查看结果-1代表噪声点 unique_labels set(dbscan_labels) n_clusters len(unique_labels) - (1 if -1 in unique_labels else 0) n_noise list(dbscan_labels).count(-1) print(f估计的簇数量: {n_clusters}) print(f识别出的噪声点数量: {n_noise}) print(f簇标签分布: {pd.Series(dbscan_labels).value_counts().sort_index()})DBSCAN的参数调优是关键。可以通过绘制k-距离图来辅助选择eps对每个点计算它到第min_samples个最近邻的距离然后对所有点排序绘图寻找拐点。6.3 缺失值数据的聚类以SOM为例输入的热词中提到了“自组织神经网络(SOM)能否对存在缺失值的数据进行聚类分析”。这是一个很好的问题。SOM是一种无监督神经网络可以将高维数据映射到低维通常是二维的离散网格上同时保持拓扑结构。关键在于SOM的训练算法本身通常不能直接处理缺失值。常见的处理策略有删除如果缺失值很少直接删除缺失样本。插补在训练SOM之前使用均值、中位数、KNN、模型预测等方法填充缺失值。SOM训练完成后对于有缺失的新数据可以先使用训练好的插补模型补全再映射到SOM网格。修改距离计算在SOM的竞争层计算最佳匹配单元时对于有缺失值的特征在计算欧氏距离时忽略该维度即只基于非缺失特征计算。这需要自定义距离函数。使用专门算法有些研究提出了能直接处理缺失值的SOM变体。因此虽然SOM是一个强大的聚类和可视化工具但在面对缺失值时仍需依赖前置的数据插补步骤或进行算法层面的定制。在实践中更通用的做法是在聚类流程的最前期也就是我们第3章讲的数据预处理阶段就系统性地处理好缺失值问题这样后续无论使用K-Means、DBSCAN还是SOM都不会再受其困扰。7. 实战避坑与性能优化指南纸上得来终觉浅绝知此事要躬行。以下是一些从实际项目中总结出的经验教训。7.1 特征工程比算法选择更重要聚类结果的质量极度依赖于输入特征。除了标准化还需考虑特征选择移除高度相关的特征。例如“年收入”和“消费总额”可能强相关保留一个即可。可以使用相关系数矩阵来识别。corr_matrix df_cleaned[features_for_clustering].corr() sns.heatmap(corr_matrix, annotTrue, cmapcoolwarm, center0) plt.title(特征间相关系数热力图) plt.show()特征构造有时原始特征不能很好地区分群体。例如可以构造“收入-年龄比”、“消费频率与客单价的乘积总消费意愿指数”等新特征可能揭示更深层的模式。维度灾难特征不是越多越好。过高的维度会使数据点在所有方向上都很稀疏距离概念失效导致聚类效果下降这被称为“维度诅咒”。在聚类前使用PCA进行降维不仅是为了可视化有时也能提升聚类性能。7.2 K-Means的随机性与初始化陷阱K-Means的初始质心是随机选择的这可能导致每次运行结果略有不同尤其是当数据本身分离度不高时。scikit-learn的KMeans默认使用n_init10和initk-means来缓解这个问题。n_init10算法会用不同的初始质心运行10次并选择WCSS最小的那次作为最终结果。initk-means一种智能初始化方法使初始质心彼此远离通常比完全随机初始化效果更好、更快。务必设置random_state如random_state42以保证结果的可复现性这在数学建模和学术研究中是基本要求。7.3 处理不同尺度与分布的特征我们之前用了Z-Score标准化这假设特征大致是正态分布的。如果某个特征存在严重的偏态如我们的“年收入”是指数分布Z-Score标准化后异常值的影响仍然很大。此时可以考虑对数变换np.log1p(x)处理右偏数据。RobustScaler使用中位数和四分位距缩放对异常值不敏感。分位数变换将数据映射到均匀分布或正态分布。需要根据数据的实际分布和业务含义来选择变换方法。例如对收入取对数在经济学上常用来衡量相对差异而非绝对差异。7.4 当聚类结果不理想时如果轮廓系数很低或者簇的区分不明显可以尝试检查数据重新审视预处理步骤异常值处理是否得当特征选择是否合理尝试其他算法用DBSCAN看看是否能发现密度簇用层次聚类看看树状图结构。调整特征进行特征变换如多项式特征、交互项或使用非线性降维方法如t-SNE、UMAP后再聚类。但要注意t-SNE/UMAP主要用于可视化其输出坐标不适合直接作为聚类输入因为它们不保持全局距离。接受现实数据本身可能就没有明显的簇状结构而是均匀或渐变的。聚类不是“无中生有”的魔法。7.5 在大数据集上的性能考量当数据量很大例如数十万以上时标准的K-Means可能会变慢。可以考虑使用MiniBatchKMeans它每次只使用数据的一个子集来更新质心速度更快通常是近似解。对于超大数据可以考虑基于采样的方法或者使用更高效的实现如faiss库。在实施聚类前先用一个子样本进行算法选择和参数调优。最后记住聚类是一个探索性数据分析过程没有绝对正确的答案。最好的聚类方案是那个能让你对数据产生新见解并能被业务逻辑所验证和解释的方案。不要过分追求数学上的最优而要追求洞察上的有用。