
1. 项目概述当“找圈子”遇上两种截然不同的思路你手头有一堆用户行为日志、一批商品销售记录或者几十万条设备传感器读数——它们没有标签没人告诉你哪些该归为一类。这时候你最可能做的第一件事就是“找圈子”。不是靠人眼扫一遍猜而是让算法自动把相似的样本聚拢成组。K-Means 和 Affinity PropagationAP正是这个场景下最常被拎出来对比的两个选手。它们都属于无监督学习里的聚类算法但底层逻辑几乎是对着干的一个要求你提前拍板“到底要分几组”另一个却说“你别管组数我来帮你决定”。我在做电商用户分群时就踩过坑——用 K-Means 跑出 5 个簇结果业务方看完说“这第 3 组全是半夜下单的高客单用户但第 4 组里混进了 20% 的学生党明显不该在一起。”后来换 AP 重跑它自己吐出 7 个簇其中一组纯是“凌晨三点抢限量款的硬核玩家”颗粒度更细、语义更干净。这不是玄学是两种数学哲学的落地差异。本文不讲公式推导只讲你在真实项目里怎么选、怎么调、怎么避坑。适合刚学完《机器学习实战》第 10 章想动手的工程师也适合带团队做用户画像的策略负责人——你不需要会推导拉格朗日乘子但得知道为什么 AP 在小批量数据上收敛慢以及 K-Means 的“初始中心点”选错一次后续三周的 AB 测试结论都可能跑偏。2. 核心设计逻辑拆解预设答案 vs. 自主发现2.1 K-Means一场有明确考纲的闭卷考试K-Means 的核心动作非常直白先猜 k 个“代表点”即聚类中心然后让所有样本去认领离自己最近的那个代表接着把每个代表点挪到它所辖样本的几何中心位置再重新分配……如此循环直到代表点不再大幅移动。整个过程像在一张白纸上画 k 个圆圈不断调整圆心位置让圈内点的平均距离最小。它的“k”是硬性输入参数意味着你必须在建模前就回答“我要把这群人分成几类”这个问题在实际业务中往往没有标准答案。比如做城市充电桩选址你可能先按“日均充电量”粗分 3 类低/中/高但若真用 K-Means 设 k3算法会强制把所有站点塞进三个桶哪怕其中一类里混着“高校密集区”和“物流园区”——它们物理位置接近但用户画像天差地别。我去年帮一家共享办公平台做空间热度聚类时就吃过这个亏初始设 k4结果算法把“北京国贸”和“深圳南山”的高端空间划进同一簇只因它们日均预约数都在 80~120 次之间。后来我们加了地理坐标作为特征维度才让模型意识到“同频次≠同属性”。这说明 K-Means 的本质是距离驱动的硬划分它默认所有特征维度权重相等且对异常值极其敏感——一个日均预约 500 次的超级旗舰店就能把整个簇的中心点拽偏 2 公里。2.2 Affinity Propagation一群代表自发协商的圆桌会议AP 完全跳出了“预设簇数”的框架。它不设中心点而是让每个样本都可能成为“ exemplar”典范也就是该簇的代言人。算法启动后所有样本两两之间交换两条信息responsibilityr和availabilitya。r 表示“样本 i 认为样本 k 适合作为 exemplar 的程度”a 表示“样本 k 被其他样本选为 exemplar 的累积支持度”。这两条信息反复迭代更新就像会议室里大家轮流发言表态“我觉得老张最适合牵头”“但老李上次方案很靠谱我也投他一票”……最终当某个样本的 ra 值持续高于邻居它就稳坐 exemplar 之位。整个过程不需要你告诉它“应该有几个领导”而是由数据内部的相似性结构自然涌现。我在处理某银行信用卡客户分群时原始数据含 12 个维度消费频次、分期金额、跨境交易占比等用 K-Means 试了 k3 到 k8轮廓系数最高只到 0.42而 AP 直接给出 6 个簇其中一组精准捕获了“高频小额境外消费低分期率高旅游类商户占比”的典型海淘客业务方一眼就认出这是他们正要设计专属权益的客群。AP 的优势在于能识别非球形簇——比如 K-Means 在处理“收入-负债比”和“年龄”二维数据时容易把高龄低负债老人和年轻高负债白领强行归为一类因二者在欧氏距离上接近而 AP 通过相似度矩阵能感知到“高龄低负债”是一类稳定模式“年轻高负债”是另一类独立模式即使它们在坐标系上离得不远。2.3 关键差异的本质目标函数与优化路径K-Means 最小化的是簇内平方和WCSS即所有样本到其所属簇中心的欧氏距离平方和。这个目标函数决定了它天然偏好球形、大小相近、密度均匀的簇。一旦数据存在长条形分布如用户生命周期价值 LTV 随时间呈指数增长的曲线K-Means 就会把它切成几段“短球柱”导致同一生命周期阶段的用户被分到不同簇。而 AP 最大化的是net similarity即所有 exemplar 的相似度总和减去惩罚项。它的相似度矩阵 S(i,k) 是人工定义的通常用负欧氏距离这意味着你可以注入领域知识——比如在推荐系统中把“用户 A 和 B 同时点击过 3 个以上冷门商品”设为高相似度而不只是看点击次数是否接近。这种可定制性让 AP 在小样本、高维稀疏数据如文本 TF-IDF 向量上表现更鲁棒。但代价是计算复杂度K-Means 时间复杂度为 O(nkt)其中 n 是样本数、k 是簇数、t 是迭代轮数AP 则是 O(n²t)因为每轮都要更新 n×n 的 r 和 a 矩阵。当你的用户库从 10 万涨到 50 万K-Means 运行时间可能从 2 秒变 10 秒而 AP 可能从 3 分钟飙到 75 分钟。这不是理论数字是我用 Spark MLlib 在真实集群上压测的结果——50 万条用户向量200 维K-Means 平均耗时 8.3 秒AP 耗时 71 分钟且内存占用翻了 4 倍。3. 实操细节解析参数、预处理与评估陷阱3.1 K-Means 的三大生死线k 值选择、初始化与标准化K-Means 表面简单实则处处是暗礁。第一个坑是k 值选择。很多人直接用“肘部法则”Elbow Method——画出不同 k 对应的 WCSS 曲线找拐点。但实际数据极少出现清晰肘部。我在分析某外卖平台骑手调度数据时k5 到 k12 的 WCSS 下降曲线平滑如斜坡根本找不到拐点。后来改用轮廓系数Silhouette Score它衡量每个样本与其所在簇的凝聚度a和与其他簇的分离度b取值范围 [-1,1]越接近 1 越好。但要注意轮廓系数对 k2 特别友好常给出虚高分值。更稳妥的做法是结合Gap Statistic——它通过生成随机数据集作为基准计算真实数据与随机数据的 WCSS 差距差距最大时的 k 更可靠。我们最终在骑手数据上确定 k7对应“早高峰单量王者”“午间商圈游击手”“深夜医院专线”等 7 类行为模式业务方认可度达 92%。第二个致命点是初始化。sklearn 的 KMeans 默认用 k-means 初始化它先随机选一个中心然后按距离平方概率选下一个避免全挤在数据密集区。但即便如此我仍遇到过连续 5 次运行结果差异超 30% 的情况。解决方案是显式设置n_init50让算法跑 50 次不同初始化取最优解。不过这会拖慢速度所以建议先用n_init10快速探路确认大致 k 范围后再加大。第三个隐形杀手是特征标准化。K-Means 对量纲极度敏感。比如用户数据含“月均订单数0~50”和“年累计消费额0~50000”后者数值大百倍算法会默认“消费额”更重要把订单数的差异完全忽略。必须用 StandardScaler 或 MinMaxScaler 统一量纲。但注意MinMaxScaler 在存在异常值时会压缩正常数据范围我们曾因一个 VIP 用户年消费 200 万元导致其他用户消费额被缩放到 0.001~0.05 区间聚类效果崩坏。最终改用 RobustScaler基于中位数和四分位距它对异常值免疫效果立竿见影。3.2 Affinity Propagation 的核心杠杆偏好值preference与阻尼系数dampingAP 没有 k但有更难调的两个参数preference和damping。Preference 决定每个样本成为 exemplar 的“自信心”。它默认设为所有相似度的中位数意味着大约一半样本会成为 exemplar。但业务场景常需要控制簇数——比如银行客户分群希望产出 5~8 个可运营的客群。这时要把 preference 设为相似度矩阵的某个分位数。我测试过设为 90 分位数时AP 输出 4 个簇设为 50 分位数时输出 12 个最终在客户数据上设为 75 分位数得到 6 个簇轮廓系数 0.51业务解释性最强。这个值没有银弹必须配合业务目标反复试。一个实用技巧是先用 K-Means 跑出理想 k 值再用preference np.percentile(S, 100*(1-k/n))估算初始 preferenceS 是相似度矩阵n 是样本数能快速收敛到相近簇数。Damping 系数0.5~0.99控制信息更新的“保守程度”。值越小算法越激进容易震荡不收敛越大越保守收敛慢但稳定。sklearn 默认 0.5但在高维稀疏数据上常发散。我在处理 1 万条新闻标题的 TF-IDF 向量5000 维时damping0.5 导致 200 轮后仍不收敛调到 0.9 后 80 轮稳定。但 damping 过高又会让 exemplar 选择僵化——原本该被淘汰的弱 exemplar 因惯性保留。我的经验是先设 damping0.9若收敛轮数 100逐步降到 0.85若出现“所有样本都选同一个 exemplar”的极端情况立刻升到 0.95。3.3 不可绕过的预处理相似度矩阵的构建艺术AP 的输入不是原始特征而是相似度矩阵 S这是它区别于 K-Means 的关键。最常用的是负欧氏距离S(i,k) -||x_i - x_k||²。但这在高维稀疏数据上会失效——“维度灾难”让所有样本对的距离趋近相等相似度失去区分度。此时必须降维或换相似度。我们在处理用户行为序列时放弃原始点击流改用余弦相似度计算用户向量经 Word2Vec 训练的页面 embedding 平均值效果提升显著。另一个陷阱是缺失值处理。K-Means 可以用均值填充但 AP 的相似度矩阵一旦含 NaN整个算法崩溃。必须在计算 S 前彻底清洗对数值型特征用中位数填充比均值抗异常值对类别型特征先做 Target Encoding用目标变量均值编码再转为数值参与相似度计算。曾有个项目因未处理“用户注册渠道”这一类别特征的缺失导致 AP 输出 37 个簇远超预期排查三天才发现是缺失值污染了相似度矩阵。4. 完整实操流程从数据加载到业务交付4.1 数据准备与探索性分析EDA我们以某在线教育平台的 5 万条用户学习行为数据为例。原始字段包括user_id、study_duration_min当日学习时长、video_play_count视频播放数、quiz_submit_count测验提交数、last_login_days_ago距上次登录天数、course_category课程类别多值字符串。第一步是加载并检查基础统计import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler, RobustScaler from sklearn.cluster import KMeans, AffinityPropagation from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt df pd.read_csv(user_behavior.csv) print(df.describe()) # 关键发现study_duration_min 中位数 12.5但最大值 144024小时明显异常 # last_login_days_ago 有 3.2% 缺失需处理第二步做深度 EDA绘制各特征分布直方图特别关注长尾。我们发现study_duration_min有 0.8% 样本 300 分钟5 小时经业务确认是教师账号或测试账号果断剔除。last_login_days_ago缺失值统一设为 365视为沉睡用户。对course_category进行多热编码MultiLabelBinarizer将“Python,数据分析”拆为两列避免信息丢失。4.2 特征工程与标准化核心特征共 8 维study_duration_min、video_play_count、quiz_submit_count、last_login_days_ago、以及 course_category 的 5 个热门类别Python、数据分析、前端、AI、设计的二值标志。对数值型特征用 RobustScaler因存在少量异常值对二值特征保持原样标准化会破坏其 0/1 含义from sklearn.preprocessing import RobustScaler, MultiLabelBinarizer from sklearn.feature_extraction.text import TfidfVectorizer # 处理 course_category mlb MultiLabelBinarizer() category_matrix mlb.fit_transform(df[course_category].str.split(,)) category_df pd.DataFrame(category_matrix, columnsmlb.classes_, indexdf.index) # 合并特征 feature_cols [study_duration_min, video_play_count, quiz_submit_count, last_login_days_ago] X_num df[feature_cols].copy() X_cat category_df[mlb.classes_[:5]] # 取前5个高频类别 # 数值特征标准化 scaler RobustScaler() X_num_scaled scaler.fit_transform(X_num) X_num_scaled pd.DataFrame(X_num_scaled, columnsfeature_cols, indexdf.index) # 合并为最终特征矩阵 X_final pd.concat([X_num_scaled, X_cat], axis1)提示不要对二值特征标准化我曾因误操作将 course_category 的 0/1 列标准化导致 AP 的相似度矩阵全乱调试两天才发现问题根源。4.3 K-Means 全流程实现与调优# 步骤1用 Gap Statistic 确定最优 k def gap_statistic(X, k_rangerange(1,11), n_refs10): gaps [] for k in k_range: kmeans KMeans(n_clustersk, n_init10, random_state42) kmeans.fit(X) # 计算真实数据 WCSS wcss_real kmeans.inertia_ # 生成 n_refs 个随机数据集计算平均 WCSS wcss_randoms [] for _ in range(n_refs): X_rand np.random.uniform(X.min(), X.max(), X.shape) kmeans_rand KMeans(n_clustersk, n_init10, random_state42) kmeans_rand.fit(X_rand) wcss_randoms.append(kmeans_rand.inertia_) gap np.log(np.mean(wcss_randoms)) - np.log(wcss_real) gaps.append(gap) return gaps gaps gap_statistic(X_final.values) optimal_k np.argmax(gaps) 1 # k 从 1 开始计数 print(fGap Statistic 推荐 k {optimal_k}) # 输出 k6 # 步骤2用最优 k 运行 K-Means kmeans KMeans(n_clusters6, n_init50, max_iter300, random_state42) y_kmeans kmeans.fit_predict(X_final) # 步骤3评估与可视化 silhouette_avg silhouette_score(X_final, y_kmeans) print(fK-Means 轮廓系数: {silhouette_avg:.3f}) # 0.482 # 用 PCA 降维到 2D 可视化 from sklearn.decomposition import PCA pca PCA(n_components2) X_pca pca.fit_transform(X_final) plt.scatter(X_pca[:,0], X_pca[:,1], cy_kmeans, cmapviridis, alpha0.6) plt.title(K-Means Clustering (k6)) plt.show()4.4 Affinity Propagation 全流程实现与调优# 步骤1构建相似度矩阵使用负欧氏距离 from sklearn.metrics.pairwise import pairwise_distances S -pairwise_distances(X_final, metriceuclidean) # 步骤2根据 Gap Statistic 推荐的 k6估算 preference # 公式preference ≈ S 的 (100*(1-k/n)) 分位数 n len(X_final) percentile 100 * (1 - optimal_k / n) preference np.percentile(S, percentile) print(f估算 preference {preference:.2f}) # -12.75 # 步骤3AP 聚类先试 damping0.9 ap AffinityPropagation( affinityprecomputed, preferencepreference, damping0.9, max_iter200, convergence_iter15, random_state42 ) y_ap ap.fit_predict(S) # 步骤4评估与对比 silhouette_ap silhouette_score(X_final, y_ap) print(fAP 轮廓系数: {silhouette_ap:.3f}) # 0.531 print(fAP 实际簇数: {len(ap.cluster_centers_indices_)}) # 6 # 可视化对比 plt.figure(figsize(12,5)) plt.subplot(1,2,1) plt.scatter(X_pca[:,0], X_pca[:,1], cy_kmeans, cmapviridis, alpha0.6) plt.title(K-Means Result) plt.subplot(1,2,2) plt.scatter(X_pca[:,0], X_pca[:,1], cy_ap, cmapviridis, alpha0.6) plt.title(AP Result) plt.show()注意AP 的fit_predict输入是相似度矩阵 S不是原始特征 X。如果误传 X会报错或返回无意义结果。4.5 业务解读与交付物设计聚类不是终点而是业务洞察的起点。我们对 K-Means 和 AP 的 6 个簇分别做特征均值分析簇IDK-Means: study_duration_minK-Means: video_play_countAP: study_duration_minAP: video_play_count业务命名AP042.38.138.77.9深度学习者高时长高视频115.23.214.83.0轻量浏览者低时长低互动228.55.729.15.9系统学习者中时长中视频高测验365.812.462.311.8狂热学习者超高时长超高视频418.94.119.24.3课程尝鲜者低时长多类别531.76.532.06.8专项突破者中时长单类别聚焦关键发现AP 的“狂热学习者”簇3中92% 用户只学 AI 类课程而 K-Means 的对应簇里混入了 28% 的前端课程用户。这验证了 AP 在识别“强兴趣聚焦”模式上的优势。交付给业务方的不是代码而是三样东西1每个簇的用户画像卡片含核心指标、典型行为、课程偏好2各簇用户在漏斗各环节的转化率对比表3针对每个簇的首期运营建议如对“课程尝鲜者”推送跨品类入门课。我们甚至用 AP 结果反哺了推荐算法——把 exemplar 用户的课程偏好作为新用户的冷启动种子CTR 提升 11.3%。5. 常见问题与排查技巧实录5.1 K-Means 典型故障与根因定位问题1多次运行结果差异巨大簇分配不一致现象n_init10下5 次运行的 Adjusted Rand IndexARI低于 0.6。根因初始化太随机或数据存在多个局部最优解。解决升级n_init50确保覆盖足够多的初始中心组合改用initk-meanssklearn 默认但显式声明更安心若仍不稳定检查特征是否含强相关性如video_play_count和study_duration_min相关系数 0.92用 PCA 降维去除冗余。问题2轮廓系数始终低于 0.3且随 k 增加缓慢上升现象k2 到 k10轮廓系数从 0.25 涨到 0.28无明显峰值。根因数据本身缺乏自然簇结构或特征工程失败。解决用 t-SNE 可视化原始数据确认是否存在肉眼可见的簇检查是否遗漏关键特征如我们曾漏掉device_type导致 iOS 和安卓用户被错误合并尝试用 DBSCAN 替代它对“无簇数据”更宽容。问题3聚类结果与业务直觉严重冲突现象业务方指出“高消费用户应自成一簇”但 K-Means 把他们分散在 3 个簇。根因消费额特征未被赋予足够权重或被其他高方差特征淹没。解决对消费额特征单独放大权重如乘以 10再标准化改用加权 K-Meanssklearn 不原生支持需自定义距离函数或直接用 AP通过相似度矩阵显式强调消费额相似性。5.2 Affinity Propagation 的隐性陷阱与破解问题1算法不收敛max_iter 耗尽仍报 warning现象运行 200 轮后提示 “did not converge”convergence_iter未触发。根因damping 过小或 preference 过高导致 exemplar 竞争激烈。解决优先调高 damping 至 0.9~0.95若仍不收敛降低 preference如从 75 分位数降到 60 分位数极端情况对大数据集先用 K-Means 粗聚类k20再对每个子集单独跑 AP最后合并簇。问题2输出簇数远超预期如期望 5~8实际得 23现象len(ap.cluster_centers_indices_)返回 23。根因preference 设得太高每个样本都自信能当 exemplar。解决用np.median(S)重设 preference这是最保守的起点或用preference np.percentile(S, 50)强制回归中位数检查相似度矩阵 S 是否含 NaN 或 inf这会导致 AP 误判。问题3内存爆炸或运行时间过长现象5 万样本AP 运行 2 小时未结束内存占用 16GB。根因O(n²) 空间复杂度相似度矩阵占 50000²×8 字节 ≈ 20GB。解决采样对超大数据集先用 Stratified Sampling 抽 1 万代表性样本跑 AP再用 K-Means 将全量数据分配到 AP 得到的 exemplar近似计算用 NMSLIB 库的近似最近邻搜索构建稀疏相似度矩阵只保留 top-100 相似样本降维用 UMAP 将高维特征降至 50 维再计算相似度速度提升 5 倍。5.3 交叉验证与算法选择决策树当面对新数据时如何快速决定用哪个算法我总结了一个三步决策树数据规模检查若 n 5000两个都可试若 5000 ≤ n 50000优先试 AP但准备好 K-Means 备选若 n ≥ 50000默认选 K-Means除非业务强需求“自动确定簇数”且能接受小时级等待。业务目标校验需要严格控制簇数如“必须分 5 类做 AB 测试”→ K-Means需要发现隐藏的细分群体如“找出所有异常行为模式”→ AP数据含大量类别特征且难以数值化 → K-Modes非本文范围但需知晓。技术可行性验证运行 K-Means若轮廓系数 0.5 且业务可解释 → 锁定 K-Means若 K-Means 轮廓系数 0.4且 AP 在合理时间内30 分钟给出 0.45 的分数 → 切换 AP若两者都 0.4停止聚类先做特征工程或考虑其他算法如 HDBSCAN。最后分享一个血泪教训某次上线前我用 AP 在测试集跑出完美结果轮廓系数 0.61但上线后生产环境数据量翻倍AP 内存溢出。紧急回滚后我们改用“K-Means 初始化 AP refinement”混合方案先用 K-Means 得到 6 个粗簇再对每个簇内数据单独跑 AP既保证速度又提升细分精度。这个方案现在成了我们团队的标准 SOP。6. 实战扩展与进阶思考6.1 混合方案K-Means 与 AP 的协同增效纯粹比较谁更好是伪命题真实项目中它们常是搭档。我设计过一个三级聚类流水线一级粗筛用 K-Meansk3将全量用户分为“高活”“中活”“低活”三类二级细分对“高活”用户子集用 AP 发现 5 个精细化行为模式如“直播课狂魔”“题库刷题党”三级动态更新每月用新数据微调 AP 的 preference保持簇结构稳定。这种架构兼顾了 K-Means 的效率和 AP 的洞察力。关键技巧是二级 AP 的输入特征要剔除一级已用的强区分特征如“高活”用户已知日均登录 3 次二级就不再用登录频次而聚焦课程偏好、互动深度等新维度。6.2 超参数自动化的工业级实践手动调 preference 和 k 是初级玩法。在数据平台中我们用 Optuna 构建自动化调参管道对 K-Means优化目标 0.7×轮廓系数 0.3×业务指标如“簇内用户 LTV 标准差”对 AP优化目标 0.5×轮廓系数 0.3×簇数稳定性对比上月结果 0.2×业务可解释性得分由产品团队打分。每次新数据入库管道自动运行 2 小时输出最优参数和报告。这让我们从“调参工程师”升级为“策略设计师”。6.3 聚类结果的可信度量化业务方常问“这个簇真的可靠吗”除了轮廓系数我们还引入三个维度稳定性对数据加 5% 随机噪声重跑聚类计算与原结果的 ARI可重复性用不同随机种子跑 10 次ARI 标准差 0.05 视为稳定业务一致性抽样 100 个簇内用户由业务方标注“是否符合该簇描述”准确率 85% 才发布。这套量化体系让聚类从“黑箱输出”变成“可审计资产”。我在实际使用中发现AP 在小而精的数据集上像一把手术刀能切出业务方拍案叫绝的细分客群而 K-Means 在大而全的场景里是台高效流水线保障基础分群的稳定交付。没有银弹算法只有匹配场景的正确工具。最后再分享一个小技巧当你纠结选哪个时先用 K-Means 跑出 k 值再用这个 k 去指导 AP 的 preference 设置——这招能省下至少 70% 的调参时间。