第一次听说k-medoids这个词时,我也是一头雾水。后来在实际项目中用了几次才发现,这其实就是一种更"接地气"的聚类方法。想象一下你要在小区里开几家便利店,k-medoids就是帮你找出最适合开店的位置——这些位置必须是小区里真实存在的楼栋(我们称之为medoid),而不能是随便在地图上画个点。
和常见的k-means算法相比,k-medoids最大的特点就是它选的中心点必须是数据集中的真实样本。这就好比k-means可以在小区任何位置画圈开店,哪怕这个点在池塘中央;而k-medoids则必须选择现有楼栋作为店铺位置。这个特性让k-medoids对异常值特别"淡定",不会因为几个离群点就乱跑中心位置。
我去年处理过一个传感器网络数据,里面有不少噪声。用k-means时,聚类中心总被几个异常读数带偏;换成k-medoids后,效果立刻稳定多了。这也是为什么在现实数据往往不够"干净"的情况下,很多工程师更偏爱这个方法。
让我们拆解下k-medoids是怎么工作的。假设你要把100个客户分成5个群组:
这个过程中最耗计算的就是第3步。我做过测试,在1000个数据点时,这一步要计算近百万次距离!所以实际使用时,对小数据集很友好,但数据量大时就得考虑优化了。
很多人容易混淆k-medoids和k-means,我整理了个对比表格:
| 特性 | k-means | k-medoids |
|---|---|---|
| 中心点性质 | 可以是虚拟点 | 必须是真实数据点 |
| 对异常值的敏感性 | 非常敏感 | 相对鲁棒 |
| 计算复杂度 | O(n) | O(n²) |
| 适用距离度量 | 通常用欧式距离 | 可用任意距离度量 |
| 最佳使用场景 | 大数据量、分布均匀的数据 | 小数据集、含噪声的数据 |
去年我给一家电商做用户分群时,发现他们的交易数据里有不少异常大额订单。用k-means时,这些异常值把整个聚类都带偏了;换成k-medoids后,分群结果立即合理很多。
下面这个MATLAB函数是我在多个项目中打磨出来的实用版本,关键位置都加了注释:
matlab复制function [clusters, medoids] = my_kmedoids(data, k, max_iter)
% 输入检查
if nargin < 3
max_iter = 100; % 默认最大迭代次数
end
[n_samples, ~] = size(data);
medoids = datasample(data, k, 'Replace', false); % 随机选择初始medoids
for iter = 1:max_iter
% 计算所有点到medoids的距离
distances = pdist2(data, medoids);
% 分配每个点到最近的medoid
[~, labels] = min(distances, [], 2);
new_medoids = zeros(k, size(data, 2));
for i = 1:k
cluster_points = data(labels == i, :);
% 计算簇内所有点间的距离和
intra_distances = sum(pdist2(cluster_points, cluster_points), 2);
[~, min_idx] = min(intra_distances);
new_medoids(i, :) = cluster_points(min_idx, :);
end
% 检查是否收敛
if isequal(medoids, new_medoids)
break;
end
medoids = new_medoids;
end
% 整理输出结果
clusters = cell(k, 1);
for i = 1:k
clusters{i} = data(labels == i, :);
end
end
这个实现有几个优化点:
pdist2计算距离矩阵,比手动实现快很多实际使用时,有三个参数需要特别注意:
matlab复制k_range = 1:10;
inertia = zeros(size(k_range));
for i = 1:length(k_range)
[~, medoids] = my_kmedoids(data, k_range(i));
distances = pdist2(data, medoids);
inertia(i) = sum(min(distances, [], 2));
end
plot(k_range, inertia, '-o');
xlabel('Number of clusters (k)');
ylabel('Within-cluster sum of distances');
跑这段代码时要注意,k值越大计算量越大。我一般先用子采样数据跑个大概范围,再在全量数据上精细调整。
matlab复制distances = pdist2(data, medoids, 'hamming');
matlab复制best_inertia = inf;
for trial = 1:10
[temp_clusters, temp_medoids] = my_kmedoids(data, k);
distances = pdist2(data, temp_medoids);
current_inertia = sum(min(distances, [], 2));
if current_inertia < best_inertia
best_inertia = current_inertia;
clusters = temp_clusters;
medoids = temp_medoids;
end
end
去年我用k-medoids帮一个零售客户做过用户分群,这里分享下具体过程。
原始数据包含10,000名用户的:
首先进行标准化处理:
matlab复制raw_data = csvread('customer_data.csv');
normalized_data = zscore(raw_data); % z-score标准化
然后检查是否有异常值:
matlab复制[coeff, score] = pca(normalized_data);
plot(score(:,1), score(:,2), 'o');
xlabel('PC1');
ylabel('PC2');
我发现有约2%的极端值,但考虑到这些可能是高价值客户,决定保留它们,这正是选择k-medoids的原因。
确定k=5个分群后运行算法:
matlab复制k = 5;
[clusters, medoids] = my_kmedoids(normalized_data, k);
为了可视化高维结果,我用PCA降维:
matlab复制[~, score] = pca(normalized_data);
colors = ['r','g','b','c','m'];
figure;
hold on;
for i = 1:k
scatter(score(labels==i,1), score(labels==i,2), 36, colors(i), 'filled');
plot(score(medoid_indices(i),1), score(medoid_indices(i),2), 'kx', 'MarkerSize', 15, 'LineWidth', 3);
end
legend('Cluster 1','Cluster 2','Cluster 3','Cluster 4','Cluster 5','Medoids');
hold off;
最终识别出5个典型客户群体:
针对不同群体,我们制定了差异化的营销策略。例如对潜在流失客户,我们推出了专属优惠券;而对高价值客户,则提供VIP服务和个性化推荐。
这个案例让我深刻体会到,k-medoids在处理真实商业数据时的优势——它不会被少数极端客户带偏方向,能稳定地找出真正有代表性的客户群体。