☰
KMeans聚类算法+代码:从欧氏距离陷阱到可解释客户分群
2026/10/13 2:51:08 网站建设 项目流程

简介:本资源是一份面向机器学习初学者与实践者的KMeans聚类算法入门实践包,聚焦无监督学习核心场景,如用户分群、图像分割与文本主题发现。内容涵盖算法原理精讲、sklearn标准实现全流程(含数据标准化、模型训练、标签预测与质心可视化),并提供可直接运行的Python代码及配套说明文档。压缩包共3个文件:1个核心脚本KMeans.py(含完整可执行代码与注释)、2个文本文件(hello.txt为简要提示,ReadMe.txt含环境依赖与运行指引),整体仅355KB,轻量易用。已有4907人学习下载,适合零基础快速上手聚类分析、理解K值选择与结果评估逻辑,尤其便于在Jupyter或PyCharm中边学边调、验证算法收敛过程与簇划分效果。

1. KMeans聚类算法+代码:不是调个sklearn.fit就完事,而是得懂它为什么在客户分群里总把高净值用户和沉默用户分进同一簇

你手上有3万条用户行为日志——登录频次、页面停留时长、加购次数、最近一次下单时间。老板说:“把用户分三类,我要做精准运营。”你秒开Jupyter,from sklearn.cluster import KMeans,kmeans.fit(X),跑完画个轮廓系数图,挑了个k=3,导出标签交差。结果市场部反馈:“A类用户里既有月消费2万的VIP,也有半年没登录的僵尸号,推送优惠券全发错了。”这不是模型不行,是KMeans在你没察觉时,已经用欧氏距离悄悄抹平了“行为稀疏性”和“量纲差异”的致命伤。KMeans聚类算法+代码,本质是一套对数据分布极度敏感、对预处理零容忍、对业务语义完全失语的硬核工具。它不关心“活跃用户”定义是什么,只认坐标系里的点距;它不理解“最近一次下单”和“总浏览时长”单位不同,直接拿原始数值算距离。本文不讲公式推导,只拆解一线工程师从拿到原始表到交付可解释分群标签的完整链路:怎么选k、为什么必须标准化、如何诊断簇内离散度是否合理、哪些字段绝对不能直接喂进去、以及当轮廓系数卡在0.45不动时,你该先查哪三行代码。适合正在写数据分析报告、准备模型上线文档、或被业务方追问“这个分群逻辑能讲清楚吗”的实战派。


2. KMeans聚类算法+代码:从数学直觉到工程落地,为什么必须亲手实现一次核心迭代逻辑

KMeans聚类算法+代码,常被误认为“调包即用”,但真正卡住项目进度的,永远是黑匣子外的环节:k值怎么定才不被业务质疑?初始中心点选错会导致收敛到局部最优,而sklearn默认的k-means++策略在稀疏数据上可能失效;更隐蔽的是,当你的特征包含“用户等级(1-10)”和“近30天GMV(0-500000)”时,后者数值大10万倍,会直接主导距离计算——算法根本没看到“等级”的业务权重。所以,动手写一次核心迭代,不是为了造轮子,而是为了建立对收敛过程的肌肉记忆。下面这段代码,就是我每次新项目必跑的“诊断级”最小实现,它暴露了所有sklearn封装里你看不见的中间态。

2.1 手写KMeans核心迭代:看清每一轮质心漂移与样本重分配

import numpy as np from sklearn.preprocessing import StandardScaler def kmeans_manual(X, k, max_iters=100, tol=1e-4): """ 手写KMeans核心逻辑,返回每轮质心坐标、簇分配、SSE变化 X: (n_samples, n_features) 数值型矩阵,已标准化 k: 目标簇数 tol: 连续两轮质心移动距离均值小于该值则停止 """ n_samples, n_features = X.shape # 随机初始化质心(实际项目中改用k-means++) centroids = X[np.random.choice(n_samples, k, replace=False)] history = {'centroids': [centroids.copy()], 'labels': [], 'sse': []} for i in range(max_iters): # 步骤1:计算每个样本到各质心的欧氏距离,分配到最近簇 distances = np.sqrt(((X - centroids[:, np.newaxis])**2).sum(axis=2)) labels = np.argmin(distances, axis=0) # shape: (n_samples,) # 步骤2:重新计算每个簇的质心(均值) new_centroids = np.array([ X[labels == j].mean(axis=0) if np.any(labels == j) else centroids[j] for j in range(k) ]) # 步骤3:计算本轮SSE(簇内平方和)和质心移动距离 sse = ((X - centroids[labels])**2).sum() shift = np.sqrt(((centroids - new_centroids)**2).sum(axis=1)).mean() history['labels'].append(labels) history['sse'].append(sse) history['centroids'].append(new_centroids.copy()) # 判断收敛 if shift < tol: print(f"KMeans收敛于第{i+1}轮,最终SSE={sse:.2f},质心平均移动{shift:.6f}") break centroids = new_centroids return history # 示例:用iris数据验证(注意:此处仅演示逻辑,真实项目不用iris) from sklearn.datasets import load_iris iris = load_iris() X_raw = iris.data # (150, 4) # 关键!必须标准化——这是KMeans聚类算法+代码里最常被跳过的生死线 scaler = StandardScaler() X_scaled = scaler.fit_transform(X_raw) history = kmeans_manual(X_scaled, k=3)

提示:这段代码的价值不在运行结果,而在history字典里存的每轮centroids和labels。
当业务方问“为什么用户A和B分到同一簇”,你不能只说“距离近”,而要打开history['centroids'][5]看第5轮质心坐标,再查X_scaled[A]和X_scaled[B]到该轮三个质心的距离向量——这才是可追溯、可答辩的聚类依据。sse列表则帮你判断:如果SSE下降曲线在第10轮后变得平缓,说明继续迭代无意义;若第1轮SSE降了80%但后续停滞,大概率是初始质心选得太差。

2.2 为什么标准化不是可选项,而是KMeans聚类算法+代码的启动开关

KMeans聚类算法+代码的数学基础是欧氏距离:$d(x_i, c_j) = \sqrt{\sum_{f=1}^F (x_{i,f} - c_{j,f})^2}$。问题在于,当特征量纲差异巨大时,距离计算会被大数值特征垄断。假设你有两列特征:

  • age: 范围 18-80,标准差约15
  • annual_income: 范围 20000-2000000,标准差约300000

那么 $ (income_i - income_j)^2 $ 的量级是 $ (age_i - age_j)^2 $ 的400万倍。算法根本“看不见”年龄差异,只按收入分簇。标准化(Z-score)强制所有特征均值为0、标准差为1,让每维贡献平等。但注意:标准化必须在训练集上拟合,在测试集/全量数据上transform,且绝不能对类别型变量(如用户城市)做标准化——这是新手翻车第一现场。

# 错误示范:对混合类型数据统一标准化 df_mixed = pd.DataFrame({ 'city': ['Beijing', 'Shanghai', 'Guangzhou'], 'age': [25, 32, 28], 'income': [15000, 22000, 18000] }) scaler.fit(df_mixed) # 报错!字符串无法标准化 # 正确流程:分离数值与类别特征 numeric_cols = ['age', 'income'] categorical_cols = ['city'] X_numeric = df_mixed[numeric_cols] X_cat = df_mixed[categorical_cols] # 仅对数值列标准化 scaler = StandardScaler() X_numeric_scaled = scaler.fit_transform(X_numeric) # fit_transform只用于训练集 # 后续需将X_numeric_scaled与one-hot编码后的X_cat拼接

2.3 K值选择:别信肘部法则,用轮廓系数+业务校验双锁死

“KMeans聚类算法+代码”里最玄学的环节,是确定k值。肘部法则(Elbow Method)看SSE随k增大下降的拐点,但实际数据中拐点常不明显;信息准则(如BIC)在非高斯分布上失效。我的血泪经验是:轮廓系数(Silhouette Score)必须配合业务规则交叉验证。轮廓系数s(i)定义为:
$$ s(i) = \frac{b(i) - a(i)}{\max{a(i), b(i)}} $$
其中a(i)是样本i到同簇其他点的平均距离,b(i)是i到最近异簇所有点的平均距离。s(i)∈[-1,1],越接近1越好。但注意:轮廓系数高≠业务合理。例如k=5时轮廓系数0.65,但其中一簇全是“近30天登录1次、停留<10秒、未加购”的用户——这在业务上就是“无效流量”,不该单独成簇。

from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt # 计算不同k值的轮廓系数 k_range = range(2, 10) sil_scores = [] for k in k_range: kmeans = KMeans(n_clusters=k, random_state=42, n_init=10) labels = kmeans.fit_predict(X_scaled) sil_avg = silhouette_score(X_scaled, labels) sil_scores.append(sil_avg) print(f"k={k}, 平均轮廓系数={sil_avg:.3f}") # 绘图找峰值 plt.plot(k_range, sil_scores, 'bo-') plt.xlabel('簇数量 k') plt.ylabel('平均轮廓系数') plt.title('轮廓系数 vs k值') plt.grid(True) plt.show() # 关键动作:对候选k值(如k=3,4,5)分别分析每簇的业务指标分布 for k_candidate in [3, 4, 5]: kmeans = KMeans(n_clusters=k_candidate, random_state=42, n_init=10) labels = kmeans.fit_predict(X_scaled) # 假设原始数据df有'login_count_30d', 'avg_stay_time', 'last_order_days_ago'列 df_with_label = df_original.copy() df_with_label['cluster'] = labels # 按簇统计核心业务指标(这才是业务方要看的) cluster_summary = df_with_label.groupby('cluster').agg({ 'login_count_30d': ['mean', 'std'], 'avg_stay_time': 'mean', 'last_order_days_ago': 'median', 'user_id': 'count' # 簇大小 }).round(2) print(f"\n=== k={k_candidate} 时各簇业务画像 ===") print(cluster_summary)

注意:轮廓系数计算耗时随样本量指数增长。对10万以上样本,建议抽样(如用np.random.choice取1万样本)计算,再用全量数据拟合最终模型。否则等半小时只为了看一个数字,不值得。


3. KMeans聚类算法+代码:特征工程生死线——哪些字段能进,哪些必须砍,哪些要变形

KMeans聚类算法+代码的成败,70%取决于输入特征的质量。它不像树模型能自动处理缺失、容忍异常值,也不像神经网络能学习特征交互。它是一台精密的几何测量仪,输入歪了,输出必然歪。我见过太多项目因特征处理不当,导致分群结果完全违背业务常识:比如把“高复购用户”和“新注册用户”分到同一簇,只因为两者“近7天登录次数”都为0——而算法根本不懂“0次登录”对新用户是正常,“0次登录”对老用户是流失预警。

3.1 绝对禁止直接输入的三类特征

特征类型问题本质具体案例正确处理方式
ID类字段无业务语义的纯编号,距离无意义user_id,order_id彻底删除,或转为计数特征(如用户历史订单数)
强偏态连续变量均值被极值扭曲,质心失去代表性total_gmv(90%用户<500,1%用户>10万)取对数:np.log1p(x);或分箱后one-hot;或用RobustScaler(基于中位数和四分位距)
时间戳原始值绝对时间无聚类价值,且数值巨大register_time(1609459200)提取周期性特征:day_of_week,hour_of_day,is_weekend;或计算相对时间:days_since_register
# 错误:直接用原始注册时间戳 df['register_timestamp'] = pd.to_datetime(df['register_time']).astype(int) // 10**9 # 转为秒级时间戳 # 这会让2020年注册用户(~1577836800)和2024年用户(~1704067200)距离达400万秒,远超其他特征尺度 # 正确:提取业务相关的时间模式 df['register_date'] = pd.to_datetime(df['register_time']) df['register_dayofweek'] = df['register_date'].dt.dayofweek # 0=周一,6=周日 df['register_is_weekend'] = (df['register_date'].dt.dayofweek >= 5).astype(int) df['days_since_register'] = (pd.Timestamp.now() - df['register_date']).dt.days # 再对'days_since_register'做log1p处理(避免0取log) df['days_since_register_log'] = np.log1p(df['days_since_register'])

3.2 必须构造的两类高价值特征

KMeans聚类算法+代码需要“业务感知型”特征,而非原始埋点。以下两类特征在我经手的12个客户分群项目中,显著提升轮廓系数0.1以上:

  1. 行为密度特征:解决“稀疏性”问题。原始行为字段(如page_view_count_7d)大量为0,导致距离计算失效。改为密度比:
    view_density = page_view_count_7d / (active_days_7d + 1)
    分母加1防除零,分子分母同为正整数,结果反映“活跃日均浏览量”。

  2. 状态组合特征:捕捉业务逻辑。单字段无法表达复杂状态,组合后产生新语义:
    is_high_value_churn_risk = (total_gmv > 50000) & (last_order_days_ago > 90)
    这个布尔特征(转为0/1)比单独的total_gmv或last_order_days_ago更能区分“高价值流失风险用户”。

# 构造行为密度特征示例 df['active_days_7d'] = df[['login_day1', 'login_day2', ..., 'login_day7']].sum(axis=1) # 假设有每日登录标记列 df['view_density_7d'] = df['page_view_count_7d'] / (df['active_days_7d'] + 1) # 构造状态组合特征(需业务定义阈值) df['is_vip'] = (df['total_gmv'] >= 100000).astype(int) df['is_recent_active'] = (df['last_order_days_ago'] <= 30).astype(int) df['vip_active_status'] = df['is_vip'] * 10 + df['is_recent_active'] # 生成0,1,10,11四个状态码 # 注意:状态码是有序分类,可直接用于KMeans(因其数值代表状态优先级)

3.3 类别型特征的嵌入陷阱:One-Hot不是万能解药

KMeans聚类算法+代码要求所有输入为数值向量。对类别型特征(如user_city,device_type),常见做法是One-Hot编码。但问题在于:One-Hot会爆炸式增加维度,且使距离计算失效。例如user_city有1000个值,One-Hot后增加1000维,其中999维为0,1维为1。此时任意两个不同城市的距离恒为$\sqrt{2}$,相同城市距离为0——算法只认“是否同城”,丢失了城市间的地理/经济相似性。

替代方案:Target Encoding(目标编码)
用该类别下目标变量的均值替代原始值。例如以city分组,计算各城市用户的平均30天复购率,用该均值作为city的数值表示。这样北京(复购率0.35)和上海(0.32)距离小,北京和县城(0.12)距离大,符合业务直觉。

# Target Encoding示例:用城市复购率编码 target_col = 'rebuy_rate_30d' # 业务定义的目标变量 df['city_rebuy_mean'] = df.groupby('user_city')[target_col].transform('mean') # 处理未见城市(测试集可能出现新城市):用全局均值填充 global_mean = df[target_col].mean() df['city_rebuy_mean'] = df['city_rebuy_mean'].fillna(global_mean) # 验证编码合理性:检查编码值分布是否与业务认知一致 print(df.groupby('user_city')['city_rebuy_mean'].agg(['mean', 'count']).sort_values('mean', ascending=False).head(10))

避坑:Target Encoding必须用训练集统计量,且需添加平滑防止过拟合。
公式:$ \text{encoded}c = \frac{\sum{i \in c} y_i + \alpha \cdot \mu_{\text{global}}}{\text{count}_c + \alpha} $,其中α是平滑参数(通常取5-10),count_c是类别c的样本数,μ_global是全局均值。直接用groupby().mean()在训练集上计算,再map到测试集,是新手最常犯的泄露错误。


4. KMeans聚类算法+代码:避坑指南——那些让模型上线前夜崩溃的5个具体问题

KMeans聚类算法+代码的落地,90%的失败不在算法本身,而在工程细节。以下是我亲身踩过的坑,按发生频率排序,每条都附带现象、根因和可立即执行的解决方案。它们不会出现在任何教程里,但足以让你在需求评审会上被当场质疑。

4.1 现象:轮廓系数突然从0.5暴跌到0.1,且k=2时反而最高

原因:数据中存在大量完全相同的样本(如新注册用户未产生任何行为,所有字段为0)。KMeans在计算距离时,0向量到质心距离恒为质心模长,导致大量样本被随机分配到同一簇,簇内离散度暴增。
解决:

# 在标准化前,先检测并处理重复样本 duplicates = df.duplicated() print(f"发现{duplicates.sum()}条完全重复记录") # 方案1:删除重复(若业务允许) df_clean = df.drop_duplicates() # 方案2:对重复样本添加微小噪声(更稳妥) if duplicates.sum() > 0: noise = np.random.normal(0, 1e-8, size=df[duplicates].shape) df.loc[duplicates] = df.loc[duplicates] + noise

4.2 现象:KMeans.fit()运行10分钟无响应,CPU占用100%

原因:n_init参数默认为10,即尝试10次不同初始质心并选SSE最小者。当样本量>5万且特征维数>20时,单次迭代耗时剧增,10次就是地狱。
解决:

# 降低n_init,用k-means++保证初始质量 kmeans = KMeans( n_clusters=k, init='k-means++', # 比random更优的初始化 n_init=1, # 改为1,信任k-means++的稳定性 max_iter=300, # 默认300足够,无需增加 random_state=42 ) # 或对大数据用mini-batch KMeans(牺牲精度换速度) from sklearn.cluster import MiniBatchKMeans kmeans = MiniBatchKMeans(n_clusters=k, batch_size=1000, random_state=42)

4.3 现象:同一份数据,今天跑结果和昨天跑结果标签顺序不同(如昨天A簇是高活用户,今天变成低活)

原因:KMeans不保证簇标签的语义一致性。算法只输出数字标签0,1,2...,其对应业务含义由质心位置决定。而质心坐标受随机种子影响,不同次运行质心位置微变,导致标签映射错乱。
解决:

# 固定random_state,并按质心某业务特征排序重映射标签 kmeans = KMeans(n_clusters=k, random_state=42, n_init=10) labels = kmeans.fit_predict(X_scaled) centroids = kmeans.cluster_centers_ # 假设第2维特征是'avg_stay_time'(已标准化),我们按此维质心值升序排列簇 centroid_order = np.argsort(centroids[:, 1]) # 获取按第2维升序的索引 # 创建旧标签→新标签的映射 label_mapping = {old: new for new, old in enumerate(centroid_order)} new_labels = np.array([label_mapping[l] for l in labels]) # 验证:新标签0对应质心第2维最小(即平均停留时间最短的簇) print("重映射后各簇平均停留时间(标准化后):", centroids[centroid_order][:, 1])

4.4 现象:导出分群结果给业务方,他们说“第1簇用户购买力最强,但里面有很多学生用户,不合理”

原因:特征中混入了与购买力无关甚至负相关的字段,如student_tag(1=学生,0=非学生)。由于学生群体GMV普遍偏低,该字段与total_gmv强负相关,拉低了整体距离计算的业务一致性。
解决:

# 特征重要性诊断:用PCA看各原始特征对主成分的贡献 from sklearn.decomposition import PCA pca = PCA(n_components=0.95) # 保留95%方差 X_pca = pca.fit_transform(X_scaled) # 查看各原始特征在前3个主成分上的载荷(绝对值越大,影响越大) loadings = pca.components_.T * np.sqrt(pca.explained_variance_) feature_names = ['age', 'income', 'login_freq', 'student_tag'] # 替换为你的特征名 for i in range(min(3, len(feature_names))): print(f"PC{i+1}载荷:") for j, feat in enumerate(feature_names): print(f" {feat}: {loadings[j, i]:.3f}") # 若'student_tag'在PC1载荷绝对值最大,说明它主导了聚类方向,应删除或重构

4.5 现象:线上服务调用kmeans.predict()报错ValueError: Expected 2D array, got 1D array instead

原因:predict()要求输入是二维数组(即使只预测1个样本),而新手常传入一维向量。
解决:

# 错误写法 single_sample = X_scaled[0] # shape: (4,) kmeans.predict(single_sample) # 报错 # 正确写法:reshape为(1, -1) single_sample_2d = single_sample.reshape(1, -1) # shape: (1, 4) cluster_id = kmeans.predict(single_sample_2d)[0] # 取第一个(也是唯一)结果 # 更鲁棒的封装函数 def predict_cluster(model, scaler, raw_sample): """安全预测单样本簇标签""" if not isinstance(raw_sample, np.ndarray): raw_sample = np.array(raw_sample) if raw_sample.ndim == 1: raw_sample = raw_sample.reshape(1, -1) scaled = scaler.transform(raw_sample) return model.predict(scaled)[0]

5. KMeans聚类算法+代码:用轮廓系数热力图定位坏簇,以及我坚持写的3行后处理代码

KMeans聚类算法+代码交付前,最后一道防线不是看平均轮廓系数,而是逐簇诊断。平均值掩盖了问题:可能4个簇里3个轮廓系数0.7,1个只有0.2——那个0.2的坏簇,就是业务方投诉“分群不准”的根源。我习惯用热力图可视化每个样本的轮廓系数,一眼锁定异常簇。

5.1 轮廓系数热力图:找到那个拖后腿的“坏簇”

from sklearn.metrics import silhouette_samples import seaborn as sns def plot_silhouette_heatmap(X, labels, kmeans_model, figsize=(10, 6)): """ 绘制轮廓系数热力图,横轴为样本索引(按簇排序),纵轴为轮廓系数值 """ # 计算每个样本的轮廓系数 silhouette_vals = silhouette_samples(X, labels) # 按簇分组并排序(每簇内按轮廓系数降序) y_lower, y_upper = 0, 0 plt.figure(figsize=figsize) for i in range(kmeans_model.n_clusters): # 获取第i簇的轮廓系数 ith_cluster_silhouette_vals = silhouette_vals[labels == i] ith_cluster_silhouette_vals.sort() size_cluster_i = ith_cluster_silhouette_vals.shape[0] y_upper += size_cluster_i # 绘制热力条(实际是水平条形图) plt.fill_betweenx( np.arange(y_lower, y_upper), 0, ith_cluster_silhouette_vals, alpha=0.7 ) # 标注簇中心线 plt.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i)) y_lower += size_cluster_i # 添加平均轮廓系数线 avg_silhouette = np.mean(silhouette_vals) plt.axvline(x=avg_silhouette, color="red", linestyle="--", label=f'Avg: {avg_silhouette:.3f}') plt.xlabel('轮廓系数值') plt.ylabel('簇与样本') plt.title(f'KMeans聚类轮廓系数热力图 (k={kmeans_model.n_clusters})') plt.legend() plt.yticks([]) # 隐藏y轴刻度 plt.show() # 使用示例 plot_silhouette_heatmap(X_scaled, labels, kmeans)

这张图里,每一横条代表一个簇,条的长度是该簇样本数,颜色深浅是轮廓系数值(越红越好)。如果某个簇的条大部分是浅黄/白色(系数<0.2),说明该簇内样本差异大、边界模糊——这就是要重点分析的“坏簇”。下一步,提取该簇所有样本,查看其原始业务指标分布,往往能发现数据质量问题(如该簇集中了所有缺失值填充样本)或业务定义冲突(如“高消费但低活跃”用户被强行归为一类)。

5.2 我坚持写的3行后处理代码:让分群结果真正可用

KMeans聚类算法+代码输出的只是数字标签,但业务系统需要的是可解释、可操作的标签。我每次交付必加这3行,它们让模型从“技术输出”变成“业务资产”:

# 第1行:为每个簇生成业务描述性名称(基于质心业务指标) cluster_desc = {} for i in range(k): # 取该簇在关键业务指标上的均值(用原始未标准化数据) cluster_data = df_original[labels == i] desc = f"簇{i}: " desc += f"均龄{cluster_data['age'].mean():.0f}岁, " desc += f"月均GMV¥{cluster_data['monthly_gmv'].mean():.0f}, " desc += f"近7天登录{cluster_data['login_count_7d'].mean():.1f}次" cluster_desc[i] = desc # 第2行:计算每个簇的稳定性(用Bootstrap评估标签鲁棒性) def bootstrap_stability(X, labels, n_bootstrap=100): """计算每个簇的标签稳定性:在bootstrap样本上重复聚类,统计原标签一致率""" stability = np.zeros(len(labels)) for _ in range(n_bootstrap): idx_boot = np.random.choice(len(X), len(X), replace=True) X_boot = X[idx_boot] labels_boot = KMeans(n_clusters=len(np.unique(labels)), random_state=42, n_init=1).fit_predict(X_boot) # 映射boot标签到原始标签空间(用匈牙利算法,此处简化为最大匹配) from scipy.optimize import linear_sum_assignment cost_matrix = np.zeros((len(np.unique(labels)), len(np.unique(labels)))) for i, l1 in enumerate(np.unique(labels)): for j, l2 in enumerate(np.unique(labels_boot)): cost_matrix[i, j] = -np.sum((labels == l1) & (labels_boot == l2)) row_ind, col_ind = linear_sum_assignment(cost_matrix) boot_to_orig = {col_ind[i]: row_ind[i] for i in range(len(row_ind))} labels_mapped = np.array([boot_to_orig.get(l, -1) for l in labels_boot]) stability += (labels == labels_mapped) return stability / n_bootstrap stability_scores = bootstrap_stability(X_scaled, labels) df_original['cluster_stability'] = stability_scores # 第3行:输出每个簇的Top3特征贡献(用质心差分分析) centroids_scaled = kmeans.cluster_centers_ # 计算各簇质心与全局质心的差值,取绝对值最大的3个维度 global_centroid = X_scaled.mean(axis=0) for i in range(k): diff = np.abs(centroids_scaled[i] - global_centroid) top3_idx = np.argsort(diff)[-3:][::-1] print(f"簇{i}最具区分性的3个特征(标准化后):") for idx in top3_idx: print(f" {feature_names[idx]}: {diff[idx]:.3f}")

这3行代码背后,是我在5个项目里被业务方反复追问后形成的习惯:

  • 第1行把冷冰冰的“簇0”变成“高净值低活跃用户群”,让市场部能直接写推送文案;
  • 第2行的稳定性分数,让我能自信地说“这个簇的标签在95%的抽样中保持一致”,而不是靠运气;
  • 第3行的特征贡献,回答了“为什么把用户分到这个簇”的终极问题,把算法黑箱变成可解释的业务逻辑。

KMeans聚类算法+代码,从来不是终点,而是业务洞察的起点。它逼你深挖数据质量、厘清业务定义、验证假设逻辑。每次调参、每次画图、每次看热力图,都不是在调试模型,是在校准你对业务的理解。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询