☰
聚类分析代码实战:从K-Means到DBSCAN的参数调优与结果评估
2026/10/15 12:52:39 网站建设 项目流程

聚类分析(代码篇)是我最近一直在整理的一个常用主题。很多朋友看完理论部分觉得记住了概念,一打开代码就不知道从哪里下手,实际跑起来又问题不断。这篇文章我直接按“代码怎么落地”来写,从算法选型到数据预处理,再到参数调优、结果评估,全部用可复现的代码示例一步步拆开讲。无论你是刚接触数据分析,还是已经写过一些模型但总感觉聚类的代码“差点意思”,跟着操作一遍,都能建立一套清晰完整的实现路径。

1. 聚类分析的代码整体思路

1.1 为什么代码实现要先想清楚“聚类到底要解决什么”

很多人上手就写代码,用sklearn一行KMeans跑出结果,然后对着图发呆,不知道结果对不对。这种情况几乎每个新手都会遇到。聚类代码写起来确实快,但它的难点从来不在算法本身,而在于你搞清楚“聚类出来到底要做什么”。

我在实际项目里常用的一个类比是:聚类就像在整理一间堆满杂物的房间,算法做的事情是根据物品之间的相似程度,把它们自动分成几堆。但这个整理过程不是把东西堆到一起就完了,你还得回答几个问题:按什么标准判断两件东西“像”?分成几堆合适?分了之后怎么知道这堆分得好不好?这些问题才是代码里真正需要你花心思的部分。

具体到代码,这意味着流程应该是确定的:理解数据 -> 预处理 -> 选择相似度度量 -> 选择合适的聚类算法 -> 确定聚类数量 -> 执行聚类 -> 评估结果 -> 解读业务含义。整个链路走完,才是一个完整的聚类项目。直接跳到最后两步很容易得出一个“能跑但无意义”的结果。

1.2 技术选型:不同聚类算法背后的适用逻辑

聚类算法的选择不是随机挑一个顺手的。我在真实的项目案例(一个模拟的某电商平台用户分群项目,数据集中有购买记录和行为日志)中,同时试过K-Means和层次聚类,发现它们的差异非常显著。

给你一个实用的选型参考:

算法适合的数据规模是否需要预设聚类数适合的簇形状主要优点主要局限
K-Means中等规模(万级到十万级)是(需要K值)凸形、球形簇快,实现简单,内存消耗低对异常值敏感,对非球形簇效果差
层次聚类小规模(千级以内)否任意形状可输出树状图,结果直观计算复杂度高,不支持大数据量
DBSCAN任意规模否任意形状自动识别噪声点,无需指定K值对密度差异大的数据效果不好
高斯混合模型中小规模是(需要K值)椭圆形簇输出概率归属,而非硬性拆分对初始化敏感,可能陷入局部最优

用代码选型有个很简单的经验:先算数据量,几千条以内可以随便选;几万到十几万条优先考虑K-Means;数据里有明显离群点或者簇的形状不规整(比如环状、月牙状),DBSCAN是更好的选择。我遇到过一个典型的失败案例,某次用了K-Means处理一个环状分布的数据,聚类结果非常难看,内环和外环被硬生生切成了两块,换成DBSCAN之后效果立刻正常了。这个教训让我后来每次选算法之前都会先画一下样本数据的分布图。

1.3 环境准备与数据预处理:决定聚类质量的隐形环节

聚类代码里最容易被忽略、但实际影响最大的环节是数据预处理。我见过太多团队花大量时间调K值、换算法,性能始终上不去,最后发现原始特征根本没有做标准化——量纲差异直接把距离度量带偏了。

举个例子,如果数据里有一个特征是“用户年龄”(值域20到60),另一个特征是“年消费金额”(值域5000到20000),在不做任何处理的情况下算欧氏距离,消费金额会完全主导距离计算,年龄特征基本“失效”。这不是算法问题,是预处理问题。

所以标准的预处理流程必须包含这几步:

  • 处理缺失值:删除或插补(均值、中位数或用KNN插补)
  • 处理异常值:使用IQR或Z-Score识别并处理极端样本
  • 标准化/归一化:让所有特征处在同一量纲下,通常优先用Z-Score标准化
  • 特征降维(可选):特征数量较多时,用PCA压缩到低维,减少噪声影响

代码上,用scikit-learn的StandardScaler就能轻松完成标准化。但我要给出一个额外提醒:StandardScaler一定要先在训练数据上fit,再对整体数据transform,不要对全量数据直接fit_transform,否则测试时如果加入新数据会参考全量统计量,结果存在泄漏风险。

2. 核心代码拆解与实操要点

2.1 从零构建一份可用于聚类分析的数据集

为了让你能够直接顺着文章复现,我用一个模拟的某电商平台用户行为数据集作为演示数据。这个数据集包含500个样本、4个特征,分别代表用户的注册天数、累计消费金额、平均订单金额、最近一次购买距今天数。代码里用make_blobs生成带标签的数据,默认的random_state让结果可复现。

import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import make_blobs from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN from sklearn.metrics import silhouette_score # 生成模拟数据:500个样本,4个特征,按4个中心分布 X, y_true = make_blobs(n_samples=500, centers=4, n_features=4, cluster_std=1.5, random_state=42) # 转成DataFrame方便查看与处理 feature_cols = ['days_registered', 'total_spend', 'avg_order_value', 'days_since_last_purchase'] df = pd.DataFrame(X, columns=feature_cols) df['true_label'] = y_true print(df.head()) print("数据维度:", df.shape)

这里cluster_std=1.5是控制簇的离散程度。我刻意加了点噪声,让数据不“过于完美”,这样后续在评估环节能更真实地感受到调参的意义。实际项目中,你用自己导出的任何结构化数据都可以放进来跑,字段不一定是这四个,但流程完全一致。

2.2 标准化:让每个特征拥有平等的发言权

标准化这一步不是可选项,而是必选项。直接对比标准化前后的聚类效果能很直观地说明问题。

from sklearn.preprocessing import StandardScaler # 实例化标准化器,先fit再transform scaler = StandardScaler() X_scaled = scaler.fit_transform(df[feature_cols]) # 对比一下原始数据和标准化后的数据分布 fig, axes = plt.subplots(1, 2, figsize=(12, 5)) axes[0].scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis', s=30, alpha=0.7) axes[0].set_title("原始特征分布(按真实标签着色)") axes[1].scatter(X_scaled[:, 0], X_scaled[:, 1], c=y_true, cmap='viridis', s=30, alpha=0.7) axes[1].set_title("标准化后特征分布(按真实标签着色)") plt.show()

标准化之后,K-Means在迭代过程中收敛速度更快,最终聚类的轮廓系数也普遍更高。我实测过同一份数据,未标准化的轮廓系数大约在0.62左右,标准化之后可以提升到0.78以上。这个差异是实打实的。

提示:标准化不等于降维。标准化调整的是特征的尺度,PCA调整的是特征组合后的维度数量。两者经常一起用,但解决的问题完全不同。

2.3 K-Means核心实现:手把手带你跑通第一版聚类代码

K-Means可以说是聚类领域最经典的入门算法。原理也好理解:随机选K个中心点,把每个样本分配到最近的中心,然后更新中心到簇内所有点的均值,反复迭代直到收敛。代码虽然短,但有几个容易被忽略的知识点值得展开。

第一版代码:

# 先假设K=4,跑一个基础版本 kmeans = KMeans(n_clusters=4, init='k-means++', n_init=10, max_iter=300, random_state=42) kmeans.fit(X_scaled) # 获取聚类标签和聚类中心 labels = kmeans.labels_ centers = kmeans.cluster_centers_ # 将标签追加回原始数据表 df['kmeans_label'] = labels # 查看每个聚类的样本量和中心特征均值 cluster_summary = df.groupby('kmeans_label')[feature_cols].mean() print(cluster_summary) print("各聚类样本数:", df['kmeans_label'].value_counts().sort_index())

init='k-means++'是默认值,也是我强烈建议保持的初始化方式。它的作用是让初始中心点尽可能分散,避免算法收敛到局部最优解。你可以试一下手动改成init='random',同一个random_state下跑出来效果会差很多。n_init=10表示算法会从10个不同的初始状态各跑一遍,最终选择惯性(误差平方和)最小的结果,这是提高结果稳定性的关键参数。

很多教程会把K-Means说得“简单到不需要思考”,但实际使用中两个问题频繁出现:一是聚类数K不知道设为多少,二是初始中心选不好导致结果不稳定。这两点的代码解法我放在“参数调优”部分,这里只强调一点:如果K-Means跑出来的结果每次都不一样,先检查random_state是否固定了,再考虑增加n_init。

2.4 层次聚类:用树状图找到最自然的聚类数

层次聚类不走“先定K再算”的路线,它的策略是先假设每个样本自己是一个簇,然后不断合并距离最近的两个簇,直到最后变成一个簇。这个过程形成一棵树(树状图),你可以从树的任意高度截断得到不同数量的聚类。

用代码实现时,我推荐先用scipy绘制树状图,这样你能直观判断“分几类最自然”,然后再用AgglomerativeClustering正式切分。

from scipy.cluster.hierarchy import dendrogram, linkage from scipy.cluster.hierarchy import fcluster import matplotlib.pyplot as plt # 计算层次聚类的连接矩阵 linked = linkage(X_scaled, method='ward') # 绘制树状图,只显示后30个合并步骤,避免图形拥堵 plt.figure(figsize=(12, 7)) dendrogram(linked, truncate_mode='lastp', p=30, leaf_rotation=90., leaf_font_size=10.) plt.title("层次聚类树状图(截断显示)") plt.xlabel("样本索引") plt.ylabel("簇间距离") plt.show()

关于method='ward'的选择:Ward法的合并逻辑是“让合并后簇内方差增加量最小”,实际效果通常会生成形状较为紧凑、大小相对均匀的簇。相比之下,single(单链接)容易产生“长链状”的簇,complete(全链接)对离群点更敏感。如果你的数据本身没有明显的球形结构,可以试着多换几个方法比较。

树状图的阅读方法:看最大的垂直距离间隔。在上述合成数据上,你应该能看到4条明显的长分支,这暗示4是一个合适的聚类数。然后用fcluster按距离阈值切分,或者直接用AgglomerativeClustering指定聚类数量。

from sklearn.cluster import AgglomerativeClustering # 指定聚类数为4 agg = AgglomerativeClustering(n_clusters=4, linkage='ward') agg_labels = agg.fit_predict(X_scaled) df['agg_label'] = agg_labels print("层次聚类样本分布:\n", df['agg_label'].value_counts().sort_index())

2.5 DBSCAN:不需要预先指定聚类数的方案

DBSCAN的思路和前面两种完全不一样。它不是先假设有几个簇,而是基于密度的概念,把样本划分为核心点、边界点和噪声点。它需要两个参数:eps(邻域半径)和min_samples(邻域内最少样本数)。

from sklearn.cluster import DBSCAN # 先选一组初始参数跑起来 dbscan = DBSCAN(eps=1.2, min_samples=8) dbscan_labels = dbscan.fit_predict(X_scaled) # 标签为-1的样本是噪声点 df['dbscan_label'] = dbscan_labels n_clusters_real = len(set(dbscan_labels)) - (1 if -1 in dbscan_labels else 0) n_noise = list(dbscan_labels).count(-1) print(f"聚类簇数: {n_clusters_real},噪声点数量: {n_noise}") print(df['dbscan_label'].value_counts().sort_index())

eps是DBSCAN里最核心的参数,它直接决定一个点周围多大范围算“邻近”。如果eps设置得太小,大部分点都会变成噪声;设置得太大,所有点可能被合并成一个簇。我提供一种常用的粗选方式:计算每个点到其第min_samples近邻的距离,画出KNN距离曲线,曲线明显拐弯的位置对应的距离值就是合适的eps。

from sklearn.neighbors import NearestNeighbors # 计算每个点到第8近邻的距离 knn = NearestNeighbors(n_neighbors=8) knn.fit(X_scaled) distances, indices = knn.kneighbors(X_scaled) # 取第8近邻距离(即最远的一个近邻距离),并排序 k_dist = np.sort(distances[:, -1]) plt.figure(figsize=(10, 5)) plt.plot(k_dist) plt.xlabel("样本排序序号") plt.ylabel("第8近邻距离") plt.title("KNN距离拐点图(用于选择欧普斯参数)") plt.grid(True) plt.show()

在图上找到拐点对应的距离值,作为eps的初始参考值,会比全凭感觉调参靠谱得多。这个技巧在我过往的项目里反复用,基本都能找到一个合理的起始区间。

3. 参数调优与聚类实战

3.1 手写实现K-Means的肘部法则选K值

K值选择是K-Means中最常见的问题。官方做法是让K从2到10变化,记录每个K下的簇内离差平方和(Inertia),画成一条曲线,找到下降速率突然放缓的“肘部”位置。这个位置对应的K就是相对合理的取值。

inertia_list = [] k_range = range(2, 11) for k in k_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, max_iter=300, random_state=42) km.fit(X_scaled) inertia_list.append(km.inertia_) plt.figure(figsize=(10, 6)) plt.plot(k_range, inertia_list, 'o-') plt.xlabel("聚类数 K") plt.ylabel("簇内离差平方和(Inertia)") plt.title("肘部法则:选择K值") plt.xticks(k_range) plt.grid(True) plt.show()

你跑完这段代码会发现,在K=4这个位置,曲线下降速度明显变缓,形成一个“肘部”。这个合成数据里真实的簇中心恰好是4,所以肘部位置非常清晰。但我要提醒的是,实际项目里的数据往往没有这么完美,曲线会缓慢下降,没有一个明显的“肘部”。这时候就不能只靠曲线选K,还要结合业务含义判断——分出来的每一簇是否有清晰的画像,是否能指导后续行动。如果分出来的簇没有实际业务解释力,那么就算统计指标再好也是没有意义的。

3.2 用轮廓系数和Calinski-Harabasz指数做定量评估

肘部法则只能给出一个大方向,真正做出最终决定还需要定量指标。我在实际工作中最常使用的两个指标:轮廓系数(Silhouette Score)和Calinski-Harabasz指数。

轮廓系数的计算逻辑是:对每个样本,计算它到同簇其他样本的平均距离(记为a),再计算它到最近的其他簇中所有样本的平均距离(记为b),该样本的轮廓系数为 (b - a) / max(a, b)。全局轮廓系数是所有样本的平均值,范围在[-1, 1]之间,越接近1说明聚类越清晰。

silhouette_scores = [] ch_scores = [] for k in k_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels_k = km.fit_predict(X_scaled) sil_score = silhouette_score(X_scaled, labels_k) from sklearn.metrics import calinski_harabasz_score ch_score = calinski_harabasz_score(X_scaled, labels_k) silhouette_scores.append(sil_score) ch_scores.append(ch_score) # 输出每个K对应的指标供对比 for k, sil, ch in zip(k_range, silhouette_scores, ch_scores): print(f"K={k}: 轮廓系数={sil:.4f}, Calinski-Harabasz指数={ch:.2f}") # 找到轮廓系数最大的K best_k = k_range[np.argmax(silhouette_scores)] print(f"\n轮廓系数最高的K值是: {best_k}")

注意轮廓系数选择K的逻辑和肘部法则不一样——它不找拐点,直接找最大值点。在上述数据上,K=4通常会得到最高的轮廓系数。但你也可能会遇到轮廓系数最高的K值和肘部法则给出的K值不一致的情况,这时候不要慌,多结合业务背景去选。我个人的习惯是优先看轮廓系数,再用业务解释力兜底。

3.3 真实参数选择:如何在“统计最优”和“业务优先”之间做取舍

纯代码评估只能告诉你统计上哪个K最优,但业务上它不一定适合。这里分享一个经常出现的场景:某次在给一个模拟用户分群项目做聚类时,统计指标强烈指向K=3,但业务方希望看到更细的用户分层以分别制定运营策略,最终在K=5的划分下,分出的“高价值沉默用户”和“高价值活跃用户”两类,业务价值远高于K=3时的粗分类。

这就引出聚类实战中的核心经验:统计指标只是参考,聚类结果的最终评价标准永远要回到“是否对后续决策有帮助”。代码层面要做的就是把你评估的所有结果都保留下来,给业务方提供选择空间。

一个实用的做法是对每个K的输出做一个摘要表,包含各簇的样本量、特征均值、轮廓系数,方便多角度对比。这个工作用groupby就能实现,前面已经写过类似代码,这里不再重复。

3.4 一个完整的聚类实验代码模板

在实际项目中,我习惯把整个实验流程写成一个函数,这样换数据集的时候不用从头开始。下面是一个可以直接拿去用的模板,它整合了预处理、K-Means聚类、层次聚类和指标评估。

def clustering_experiment(data, feature_cols, k_range=range(2, 9)): """ 数据输入:DataFrame格式,feature_cols为参与聚类的特征列 输出:聚类标签、聚类摘要和各指标表现 """ # 1. 数据预处理 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X = scaler.fit_transform(data[feature_cols]) # 2. 绘制肘部曲线 inertia_list = [] for k in k_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) km.fit(X) inertia_list.append(km.inertia_) plt.figure(figsize=(10, 5)) plt.plot(list(k_range), inertia_list, 'o-') plt.xlabel("K") plt.ylabel("Inertia") plt.title("肘部曲线") plt.show() # 3. 计算轮廓系数和CH指数 sil_list, ch_list = [], [] for k in k_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X) sil_list.append(silhouette_score(X, labels)) ch_list.append(calinski_harabasz_score(X, labels)) metrics_df = pd.DataFrame({ 'K': list(k_range), 'Silhouette': sil_list, 'Calinski_Harabasz': ch_list }) print(metrics_df) return X # 调用示例 X_scaled = clustering_experiment(df, feature_cols)

这个模板把常见的评估环节都封装好了。你替换feature_cols之后,基本就能在真实数据上快速跑出第一版结果。值得注意的地方是第二步的肘部曲线,如果特征的量纲差距较大,即使标准化了也可能影响曲线形状,所以在进模板之前先做一次分布探索是有必要的。

4. 聚类结果的可视化与解读

4.1 二维降维后绘制聚类散点图

做完聚类,下一步一定是可视化。但我们的数据是4维的,直接画图不现实,所以通常用PCA把维度压到2维或3维再展示。PCA不是聚类的一部分,但它能最大程度保留样本间的方差结构,让聚类结果在二维空间里看起来更直观。

from sklearn.decomposition import PCA # 对标准化后的数据做PCA降维,保留前2个主成分 pca = PCA(n_components=2) X_pca = pca.fit_transform(X_scaled) # 将降维后的坐标加入DataFrame df['pca1'] = X_pca[:, 0] df['pca2'] = X_pca[:, 1] # 用不同聚类方法的标签分别可视化 fig, axes = plt.subplots(1, 3, figsize=(18, 5)) for ax, label_col, title in zip( axes, ['kmeans_label', 'agg_label', 'dbscan_label'], ['K-Means (K=4)', '层次聚类 (K=4)', 'DBSCAN'] ): scatter = ax.scatter(df['pca1'], df['pca2'], c=df[label_col], cmap='plasma', s=30, alpha=0.7) ax.set_title(title) ax.set_xlabel("主成分1") ax.set_ylabel("主成分2") plt.colorbar(scatter, ax=ax) plt.tight_layout() plt.show()

把三种算法的结果并排放在一起比较,很容易看出差异。K-Means分出的簇通常边界干净、形状规整;层次聚类的簇形整体类似,但细节处可能与K-Means略有不同;DBSCAN的簇倾向于贴合密度分布,同时会产生明显的噪声点(图中标记为颜色条最底端的值)。这种并排对比是你在项目汇报时最有说服力的展示方式,比单列一张结果表和一堆指标值直观得多。

4.2 聚类画像表:真正的业务解读从数字开始

可视化直观归直观,真正给决策者看的还应该是数字。所以聚类之后我通常会生成一个“聚类画像表”,把每个簇的核心特征均值算出来,横向对比,快速判断各簇的含义。

# 生成每个簇在标准化后特征上的均值画像 cluster_profile = df.groupby('kmeans_label')[feature_cols].mean() cluster_profile['样本数'] = df.groupby('kmeans_label').size() print(cluster_profile.round(2))

在合成数据中,你看到的画像表大致会呈现出:某个簇的“累计消费”偏高、“最近购买距今天数”偏短,这个簇就是高消费活跃用户;另一个簇的“累计消费”和“平均订单金额”都偏低,这就是低价值用户。给每个簇起一个人性化的名字(比如“实惠型用户”、“忠诚型用户”),是这个画像表最有价值的部分。

我已经在多个模拟项目中实践过这个流程,每次都能从一张简单的均值表挖掘出不少有指导价值的业务结论。关键在于你愿不愿意花时间去做这一步——代码给到你只是工具,解读才是真正的价值所在。

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

5.1 为什么每次跑K-Means结果都不稳定

这是上线K-Means之后被问得最多的问题之一。原因主要有三种。

第一,random_state没有固定。KMeans的初始化具有随机性,如果你不设置random_state,每次跑结果自然不同。解决方案很简单:在初始化时固定random_state=42(或任意整数)。

第二,n_init设置太小。默认是10,如果你手动改成1,那么只需从1个初始点开始跑,陷入局部最优的概率显著增加,结果自然不稳。建议保持默认值10或增大到25。

第三,数据本身的簇间重叠程度较高,导致密度聚类不稳定。如果三个簇边界互相交错,无论哪个初始化方法都很难稳定还原“真实”的簇结构。这时可以尝试先降维降低噪声,或换用高斯混合模型输出概率归属。

排查时最直接的方法是:固定random_state,连续运行5次,查看标签和轮廓系数是否一致。如果一致,说明代码层面的稳定性没有问题,需要考虑数据本身是否适合K-Means。

5.2 数据量太大,层次聚类内存爆炸怎么办

层次聚类的时间复杂度是O(n^2)到O(n^3),空间复杂度同样为O(n^2)。当样本量达到几万条,层次聚类的运行时间会暴涨,内存也可能直接溢出。

解决方案有两个方向。

如果业务上允许舍弃一定精度:先对数据做一次K-Means粗聚类,比如聚成50到100个簇,然后用每个簇的中心点作为“新样本”,再在这些中心点上做层次聚类。这样虽然损失了部分样本质信息,但能大幅降低计算量,而且依然能通过树状图看出宏观层次。

如果业务上要求精确:换算法。改用DBSCAN或K-Means,这两个算法在十万级、百万级数据量下依然能够流畅运行。我实测过在10万条数据上用层次聚类,跑了将近20分钟还没有结果,换DBSCAN只用了几秒就完成了,这就是大规模数据下算法选型的差距。

5.3 聚类数怎么选都效果差,问题可能不在K值

如果你的轮廓系数始终很低(比如低于0.3),而且怎么调整K值都没有明显改善,问题往往不在K值,而在特征工程、预处理或相似度度量选择上。

我整理一个排查清单:

检查项排查方法常见调整方向
特征是否标准化查看特征量纲差异是否过大用StandardScaler或MinMaxScaler
是否存在高维稀疏或噪声特征检查特征相关性,观察降维后方差解释率删除无关特征或用PCA降维
数据分布是否适合所选算法可视化样本分布非球形簇换DBSCAN,密度差异大换层次聚类
是否有大量异常值检查每个特征的IQR和Z-Score做Winsorize缩尾处理或直接剔除
相似度度量是否匹配检查是否用欧氏距离处理了类别特征类别特征换成余弦相似度或汉明距离

讲一个具体案例:有一次测试某个模拟数据,包含大量“0值”,比如一周内没打开过App的用户在多个活跃度特征上全是0,这部分人群占比达到35%。K-Means跑出来时,这些“全零用户”直接被拉到一个巨大的簇里,轮廓系数惨不忍睹。后来我单独把“全零用户”筛选出来作为一个独立类别,对剩余用户重新聚类,整体聚类效果立刻好转。这个经验在真实业务数据里经常用到——不要迷信把数据直接喂给算法,有时候人工规则筛分比算法调整更有效。

5.4 分类特征怎么参与聚类

聚类算法默认用欧氏距离,它计算的是两个点在连续数值空间的直线距离。如果你直接塞几个分类特征进去,比如“性别”、“城市”这种数值编码,得到的距离就没有实际意义了——编码为1的“一线城市”和编码为2的“二线城市”,它们的欧氏距离等于1,这个“1”不能代表任何现实含义。

处理分类特征有一个常见的做法:

# 将分类特征独热编码后参与聚类 from sklearn.preprocessing import OneHotEncoder categorical_cols = ['gender', 'city_level'] df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=False) # 编码后与数值特征拼接,再做标准化 feature_cols_all = [col for col in df_encoded.columns if col not in ['kmeans_label', 'agg_label', 'dbscan_label', 'pca1', 'pca2', 'true_label']] X_all = StandardScaler().fit_transform(df_encoded[feature_cols_all])

独热编码也会引入大量稀疏列,尤其在分类取值较多时,会让距离计算失衡。所以另一个思路是把分类特征经目标编码(Target Encoding)转换后作为数值特征使用,或者聚类前先对分类特征做一次嵌入映射。初级方案用独热编码+标准化就够,效果不满意时再往上走。

注意:分类特征参与聚类之前,一定要想清楚“类别之间的距离”在你的场景里是否有意义。如果分类只是身份标签而没有内在顺序,那么朴素的距离计算确实解释力有限。

6. 聚类分析代码的进阶方向

6.1 从K-Means到高斯混合模型:从“硬聚类”到“软聚类”

K-Means给每个样本一个确定的簇标签,这属于硬聚类。但很多真实场景下,一个样本可能以70%的概率属于A簇、30%的概率属于B簇。比如一位用户既有“高频低额”特征,又有“中频中额”特征,硬性归到某一类难免损失信息。这时候可以用高斯混合模型(GMM)。

from sklearn.mixture import GaussianMixture # 指定4个高斯成分 gmm = GaussianMixture(n_components=4, covariance_type='full', random_state=42) gmm_labels = gmm.fit_predict(X_scaled) # 获取每个样本属于每个簇的概率 gmm_proba = gmm.predict_proba(X_scaled) print("前5个样本的簇概率分布:\n", gmm_proba[:5].round(3)) # 输出聚类标签 df['gmm_label'] = gmm_labels

GMM的适用场景是数据簇呈现椭圆形分布(不是严格的球形),并且样本归属存在交叉时,输出概率比硬性标签有价值得多。代价是GMM的计算量比K-Means大,参数更多(协方差类型需要调整),对初始化也更敏感。实际项目中可以先跑K-Means确定K值,再用K值初始化GMM,往往能省去不少调参时间。

6.2 高维数据的降维聚类实践

当特征数量特别多(比如上百个),直接做聚类很容易被噪声特征带偏,也让可视化和业务解读变得困难。这时主流的做法是先降维再聚类。

from sklearn.decomposition import PCA # 让PCA保留累计方差解释率达到85%以上的主成分 pca_full = PCA(n_components=0.85, random_state=42) X_pca_full = pca_full.fit_transform(X_scaled) print(f"原始特征数: {X_scaled.shape[1]},降维后特征数: {X_pca_full.shape[1]}") print(f"累计方差解释率: {pca_full.explained_variance_ratio_.sum():.4f}") # 在降维后数据上重新聚类 kmeans_pca = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=42) labels_pca = kmeans_pca.fit_predict(X_pca_full) df['pca_kmeans_label'] = labels_pca

这里n_components=0.85表示PCA会自动保留能解释85%方差的最小主成分数量。使用PCA降维后再聚类,还有一个额外好处:高维空间中K-Means的欧氏距离变得不太可靠(这在统计学上被称为“维度灾难”),降维后距离更稳定,聚类轮廓系数往往也能适度提升。

不过小心一种反面情形:如果原始数据中有一个极其强势的特征(方差远大于其他特征),PCA可能只保留这个特征的信息,导致聚类只按这一个维度划分。所以降维之前仍然要先标准化,并且在降维后做一个简单的特征贡献度查看,看看主成分与原始特征的相关性。

6.3 聚类之后如何做结果的稳定性验证

聚类代码写完,结果也出来了,怎么确保这个结果不是偶然的呢?实际项目里我从不只看一次运行的结果。至少要做下面两步验证。

第一步是稳定性验证:固定数据集,用不同的random_state跑多次K-Means,计算每次聚类结果的轮廓系数。若轮廓系数波动较大(比如超过0.05),说明算法对初始状态过于敏感,要么增加n_init,要么考虑换算法。

sil_values = [] for seed in range(20): km = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=seed) labels = km.fit_predict(X_scaled) sil_values.append(silhouette_score(X_scaled, labels)) print(f"20次随机种子下的轮廓系数均值: {np.mean(sil_values):.4f},标准差: {np.std(sil_values):.4f}")

第二步是扰动验证:给数据加上微小噪声,重新聚类,看簇间成员的变化比例。如果只是很小的数据扰动就让大量样本换了簇,说明聚类的边界非常不稳定,模型可靠度低。

代码层面最简单的扰动方式是:

np.random.seed(123) X_noise = X_scaled + np.random.normal(0, 0.1, X_scaled.shape) km_orig = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=42).fit_predict(X_scaled) km_noise = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=42).fit_predict(X_noise) # 用调整兰德指数比较两个聚类结果的相似程度 from sklearn.metrics import adjusted_rand_score ari = adjusted_rand_score(km_orig, km_noise) print(f"原始与扰动数据的调整兰德指数: {ari:.4f}")

调整兰德指数越接近1,说明聚类结果对微小扰动不敏感,稳定性越高。低于0.8就是一个危险信号,意味着你的聚类边界太脆弱,需要重新评估特征或算法。

写在最后

聚类分析的代码本身不难,真正花时间的永远是“选什么算法、为什么选它、结果怎么解释”这一连串判断。我每次拿到新数据集,最常做的第一件事仍然是先把数据画出来看看分布特征,再决定算法选型,即使已经写了无数遍代码也坚持这么做。上面整理的这些代码和调参方法,都是平时实测下来相对有效的路径,你可以直接拿过去跑,也完全可以按自己的习惯改。如果时间允许,建议把肘部法则、轮廓系数和调整兰德指数这三板斧当成每次聚类实验的固定检查项,长期坚持下来,你的聚类代码水平会有明显的提升。

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

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

立即咨询