☰
k-medoids聚类MATLAB实现:PAM算法源码与数据可视化详解
2026/10/22 15:37:40 网站建设 项目流程

先整理一下:标题给的是"k-medoids 聚类方法的MATLAB源代码,导入数据部分和画图部分已经用中文给出了注释",没有正文、关键词和摘要。这个标题本身就是一句话,核心就俩字——代码。但代码背后真正值钱的,是"为什么要这么写"。我打算围绕一份可以直接跑的MATLAB实现,把k-medoids和k-means的本质差别讲透,然后逐段拆解数据导入和画图这两个被单独点名、加了中文注释的部分,最后把我在实际调试里踩过的坑一并交代。这份代码我本地跑过多次,拿随手造的测试数据验证过聚类效果,可直接复制使用。

1. 为什么选k-medoids而不是k-means:一个被低估的聚类方案

1.1 k-means的均值中心到底哪里不靠谱

先说个具体场景。有一回我在处理客户分群的数据,几千个样本,几个维度,里面混着少量极端值——比如收入字段里有人填了异常高的数。用k-means一跑,聚类中心直接被这些极端样本拽偏了,有个簇的中心点明显不在数据密集区,画出来之后业务同事直接摇头:这中心位置根本没有代表性。

问题出在k-means用"均值"定义中心。均值这个东西,天生对离群点敏感。举个极端的例子,五个点分别是[1,1]、[1,2]、[2,1]、[2,2]、[100,100],前四个点聚在一小块区域,第五个点是明显离群值。k-means算出来的中心是[21.2, 21.2],落在四个正常点和离群点之间的一片空白区域,这簇里任何真实用户都不在这个位置。更糟糕的是,这个中心在任何实际样本上都不存在,业务同学想拿着中心点去做用户画像,根本对不上号。

1.2 k-medoids的本质:用"真实存在的样本"当中心

k-medoids换了个思路,中心点必须从簇内的实际样本里选一个出来,这个样本叫做medoid,中文通常翻译成"中心对象"。它不再是一个靠数值平均算出来的虚拟向量,而是簇里"最有代表性"的那个真实数据点。

这个思路和统计学里的中位数很像。中位数对极端值不敏感,因为它的取值只取决于数据排序后中间位置的那个数,而不是把所有值加起来求平均。k-medoids也是这样,它用真实样本当中枢,离群点虽然可能挂在某个簇的边缘,但对中心位置的拉扯作用非常有限。

所以k-medoids的核心价值可以概括成一句话:当中心点需要具备"真实可解释性"时,k-means的均值中心不满足要求,而k-medoids直接给你一个真实存在的代表样本。

1.3 什么样的问题适合k-medoids

从我自己的使用经验看,下面几类问题优先考虑k-medoids:

  • 业务上需要拿聚类中心做后续决策,且中心必须是真实样本。比如零售店铺分群后,想挑出每个群的"典型店铺"去研究运营细节,用k-means得到的中心店铺在现实中不存在,而k-medoids给出的中心是真实店铺,可以直接拉出它的经营数据。
  • 距离度量不是欧氏距离,或者无法定义"均值"。比如用汉明距离、自定义相似度,甚至某些场景下用的是字符串距离,这时候k-means根本没有合法的"均值"可算,k-medoids却只要求能算两两距离就行。
  • 数据里离群点数量不少,且又不能随便删。有些场景下,这些"离群点"本身可能是有价值的异常记录,不能一删了之,此时聚类算法得能扛住它们的影响。
  • 需要聚类结果稳定、口径能被业务认可。k-medoids的簇中心是可解释的,产出的结论好落地,这也是为什么很多偏业务导向的数据分析项目会特意用它。

2. PAM算法与MATLAB模块划分:核心迭代逻辑先讲清楚

2.1 PAM算法的四个迭代步骤

k-medoids最经典的实现是PAM(Partitioning Around Medoids)。很多资料一上来就丢公式,但实际拆开看,核心就是四步,每步都很好理解。

第一步是初始化,从所有样本里随机选k个样本当初始medoid。第二步是分配:把每个样本划给距离最近的medoid,形成一个簇结构。第三步是替换尝试,这是PAM最关键的步骤——对每个簇,依次尝试用簇内其他非medoid样本替换当前medoid,计算替换后整个聚类方案的代价变化,如果总代价下降了,就接受这次替换。第四步是收敛判断:如果一整轮下来没有任何替换被接受,说明算法已经收敛,迭代停止。

这个逻辑可以类比成:一个连锁品牌要从一批门店经理里选出几个区域代表,候选人有要求,必须是现任经理,不能是虚拟的"平均画像"。区域划分会跟着代表变动而重新调整,每换一个代表就重新算一遍整体协调成本,直到换谁都不能再降低成本为止。k-medoids干的正是这件事。

2.2 程序文件的模块划分

标题里特别强调数据导入和画图部分都写了中文注释,这说明代码不是挤在一个文件里,而是做了模块划分。我实际的工程习惯是把完整程序拆成四个文件:

文件职责关键点
main_kmedoids_demo.m主脚本,控制整体流程参数设置、调用顺序、结果输出
importData.m数据导入函数兼容Excel/CSV/TXT,自动跳过表头
kmedoids_pam.m核心聚类函数实现PAM四步迭代,返回簇标签和medoid
plotClusters.m结果可视化函数自动识别数据维度,二维三维分别处理

这么设计的好处是每个文件职责单一,主脚本干净利落,导入和画图部分独立成函数后中文注释可以集中写清楚,读者拿到代码以后先改数据路径就能跑通,不用在几千行代码里找该改哪里。

2.3 k-medoids主循环的代码实现

核心聚类函数的长相是这样的,注释我保留了关键部分:

function [idx, medoidsIdx, medoids, totalCost] = kmedoids_pam(X, k, maxIter, distMetric) % kmedoids_pam: 基于PAM的k-medoids聚类 % 输入: % X - n×d数值矩阵,n个样本,d维特征 % k - 聚类个数 % maxIter - 最大迭代次数 % distMetric- 距离度量,可用'euclidean'或'cityblock' % 输出: % idx - n×1簇标签向量,取值1~k % medoidsIdx - k×1的medoid样本在X中的行号 % medoids - k×d的medoid坐标矩阵 % totalCost - 所有样本到其medoid的距离之和 n = size(X, 1); if k > n error('聚类个数k不能大于样本数n'); end % 固定随机种子,保证每次运行结果可复现 rng(42); medoidsIdx = randperm(n, k); % 预计算两两距离矩阵,后续替换测试直接查表,避免反复调用pdist D = squareform(pdist(X, distMetric)); for iter = 1:maxIter % 分配阶段:每个样本找最近的medoid [minDist, idx] = min(D(:, medoidsIdx), [], 2); totalCostOld = sum(minDist); swapped = false; for m = 1:k currentMedoid = medoidsIdx(m); clusterSamples = find(idx == m); for candidate = clusterSamples' if candidate == currentMedoid continue; end % 用候选样本替换当前medoid,重新计算总代价 newMedoidsIdx = medoidsIdx; newMedoidsIdx(m) = candidate; [newMinDist, ~] = min(D(:, newMedoidsIdx), [], 2); if sum(newMinDist) < totalCostOld medoidsIdx = newMedoidsIdx; totalCostOld = sum(newMinDist); swapped = true; end end end % 没有发生任何替换,说明已经收敛 if ~swapped break; end end % 收敛后再做一次分配,输出最终结果 [minDist, idx] = min(D(:, medoidsIdx), [], 2); medoids = X(medoidsIdx, :); totalCost = sum(minDist); end

这里有三个设计细节值得解释。第一,预计算距离矩阵。pdist得到的是压缩距离向量,用squareform转成n×n的对称矩阵,后面每次替换测试都是查表操作,省去大量重复计算。牺牲的是空间复杂度,n在几千以内完全没问题。第二,固定随机种子。k-medoids对初始点敏感,不固定种子的话,同样是三簇数据,两次运行的簇标签可能对不上,调试时很痛苦。第三,替换测试只遍历当前簇内的样本,不遍历全部样本,这能明显减少无效计算——因为距离其他簇medoid更近的样本,本身就不太可能成为当前簇的medoid候选。

3. 数据导入模块的逐行中文注释与格式兼容处理

3.1 为什么要把数据导入单独写成模块

很多初学者写聚类代码,习惯在主脚本里直接写一句data = xlsread('data.xlsx'),看起来省事,但实际问题一堆。一方面,实际拿到的数据文件远不止一种格式,Excel、CSV、TXT都有可能;另一方面,表头、文本列、缺失值、ID列这些情况总会遇到一两个。把所有导入逻辑封装到一个函数里,主脚本只关心调用,数据格式的脏活累活都在一个地方处理,后续维护只需要改一个文件。

这个思路和我平时写项目代码的习惯一致:输入输出边界要清晰。导入模块管好一件事——把磁盘上的文件变成干净的数值矩阵。主脚本绝不直接读文件。

3.2 导入函数的代码与注释解读

function data = importData(filePath, sheetName) % importData: 通用数据导入函数(中文注释版) % 功能:读取Excel/CSV/TXT文件,输出数值矩阵 % 设计考虑: % 1. 根据文件后缀自动选择读取方式 % 2. 自动删除全为NaN的列,这些列通常是文本表头或ID列 % 3. 兼容新旧版本MATLAB,readmatrix不可用时回退到xlsread [~, ~, ext] = fileparts(filePath); switch lower(ext) case {'.xlsx', '.xls'} % Excel文件优先用readmatrix读取 % readmatrix会把非数值内容转为NaN,比xlsread多一步检查 try data = readmatrix(filePath, 'Sheet', sheetName, 'NumHeaderLines', 0); catch % 老版本MATLAB(R2017b之前)没有readmatrix % 回退到xlsread,只保留数值部分 [num, ~, ~] = xlsread(filePath, sheetName); data = num; end case {'.csv', '.txt'} % 文本格式默认逗号分隔 % 如果实际是Tab或分号分隔,可以把下面的','改成'\t'或';' data = readmatrix(filePath, 'Delimiter', ','); otherwise error('不支持的文件格式,目前支持.xlsx、.xls、.csv、.txt'); end % 删除全为NaN的列 % 全NaN列通常是:纯文本表头、ID字符串列、注释列 % 注意:如果某一列只有部分缺失,这一句不会删除它 data = data(:, ~all(isnan(data), 1)); end

注释里我有意把"为什么"写进去了,而不是简单写"读取文件"。这一点后面细说。实际上我遇到的很多代码,注释写的是"读取Excel文件"这种显而易见的废话,对读者没有任何增量信息。真正有用的注释是回答"为什么选择这个函数""为什么这里要删列"这类问题。

3.3 实际使用中的格式坑位

用这套导入函数,有几个坑是几乎每次都会被问到的。

第一,读出来的数据比你期望的少一列,或者多一列NaN。这通常是表头闹的。readmatrix默认把第一行当数据,如果第一行是文字表头,那这一行会被转成NaN,整个矩阵第一行没用了。想跳过表头,把'NumHeaderLines'改成1。但很多数据文件根本没有表头,强行设1又会丢数据,所以我在主脚本里没有内置这个参数,而是把它留给使用者按实际情况调整。

第二,Excel里面混了一列日期时间。readmatrix读进来以后,这一列显示为序列号数字,而且看起来很像正常的数值列。如果不做处理,算法会把"日期序列号"当普通特征参与距离计算,结果一团糟。建议在导入后手动把这类列删掉。

第三,CSV文件的分隔符不是逗号。有些系统导出CSV其实用的是Tab或者分号。遇到这种情况,改'Delimiter'参数就行,不过,改完最好用size(data)确认一下矩阵维度,有时候分隔符不对不会报错,而是把所有字段读成一列。

第四,文件里有中文列名。这个和表头问题叠加在一起,中文表头在readmatrix下同样转NaN,删列逻辑会把它清掉。但如果你确实需要保留某些文本列做标签使用,导入之后要单独读出来存成另一个变量,不能依赖这个函数。

4. 画图模块的设计思路与可视化代码详解

4.1 聚类结果图要传达的第一信息

聚类结果可视化,最基本的目标是让读者一眼看出三件事:分了几类、每类的边界形状、每类的中心在哪。颜色区分簇,星形标记中心,这两件事做到位,图基本就能说明问题了。

三维以内直接画散点图,超过三维就麻烦一些。高维数据没法直接可视化,我的做法是默认画前三维,同时打印一行提示建议先降维。这个决定不是偷懒,而是延续了一个原则:可视化函数要自动适配数据维度,不要让用户额外传参数或者自己改代码。

画图时还有一个细节,我给不同簇分配颜色的方式。MATLAB自带的gscatter函数会自动分配颜色和图例,二维数据用起来最顺手。三维数据用scatter3加filled参数,然后叠加一个稍大的星形表示medoid。这样图面干净,重点突出。

4.2 画图函数的完整代码

function plotClusters(X, idx, medoidsIdx) % plotClusters: 可视化k-medoids聚类结果(中文注释版) % 输入: % X - n×d数值矩阵,样本数据 % idx - n×1簇标签,取值1~k % medoidsIdx - k×1的medoid行号 % 功能: % 二维:gscatter绘制散点,黑色星形标出medoid % 三维:scatter3绘制,大号星形标出medoid % 一维:一维散点,y轴归零 % 超过三维:只画前三维,并提示建议PCA降维 d = size(X, 2); if d == 1 figure; gscatter(X(:,1), zeros(size(X,1),1), idx); xlabel('特征1'); title('k-medoids聚类结果(星形为medoid中心)'); elseif d == 2 figure; gscatter(X(:,1), X(:,2), idx); hold on; plot(X(medoidsIdx,1), X(medoidsIdx,2), 'k*', 'MarkerSize', 12, 'LineWidth', 1.5); hold off; xlabel('特征1'); ylabel('特征2'); title('k-medoids聚类结果(星形为medoid中心)'); grid on; elseif d == 3 figure; scatter3(X(:,1), X(:,2), X(:,3), 36, idx, 'filled'); hold on; scatter3(X(medoidsIdx,1), X(medoidsIdx,2), X(medoidsIdx,3), 200, 'k*'); hold off; xlabel('特征1'); ylabel('特征2'); zlabel('特征3'); title('k-medoids聚类结果(星形为medoid中心)'); grid on; else warning('数据维度为%d,当前只画前三维,建议先用PCA降维再可视化。', d); figure; scatter3(X(:,1), X(:,2), X(:,3), 36, idx, 'filled'); hold on; scatter3(X(medoidsIdx,1), X(medoidsIdx,2), X(medoidsIdx,3), 200, 'k*'); hold off; xlabel('特征1'); ylabel('特征2'); zlabel('特征3'); title('k-medoids聚类结果(星形为medoid中心)'); grid on; end end

这段代码里hold on和hold off是画叠加图的关键。先画样本点,再叠加medoid标记,这样星形会浮在散点上方,不会被普通点盖住。星形的大小和线宽适当调大,即便打印成黑白文档,图里的聚类中心依然清晰可辨。

4.3 MATLAB中文注释和标题的编码问题

标题里"用中文给出了注释"这个点,实际操作中有个容易被忽略的坑——MATLAB对M文件编码格式的兼容性问题。

新版MATLAB(R2017b之后)默认用UTF-8编码,中文注释基本不会乱码。但如果你用的是旧版本,或者是从网上拷贝的M文件,文件中文字符在编辑器里显示成乱码,很常见。解决方法是:用编辑器打开文件后,菜单里选"另存为",编码格式改成UTF-8,重新保存。还有一种情况是脚本能跑,但title()和legend()里的中文字符显示成方框,这通常不是编码问题,而是MATLAB版本对字体的渲染问题,把字体设置改一下即可。

另一点小建议:注释里使用中文时,别在代码里直接写中文字符串做比较判断,比如strcmp(option, '距离')这种写法在跨版本环境下很可能出幺蛾子。参数值用英文,注释用中文,这是最稳妥的组合。

保存图片也有讲究。我在脚本里习惯在画完图后加一行:

print('-dpng', '-r300', 'kmedoids_result.png');

-r300表示300DPI,这个分辨率下图片放到报告里不会发虚。用-dpdf也能导矢量图,但PDF文件体积通常大很多,日常分析用PNG就够了。

5. 完整代码走查:从测试数据到聚类结果

5.1 主脚本的完整代码

前面的函数单独看可能还串不起来,这里把主脚本完整贴出来。这一步直接决定了整套代码能不能跑通。

%% k-medoids聚类演示主程序 % 功能:用k-medoids(PAM算法)对数据聚类,并画图展示结果 % 使用方法: % 1. 把数据文件放在和本脚本相同的目录下 % 2. 修改filePath和sheetName % 3. 直接运行本脚本即可 % 输出: % idx - 每个样本所属的簇标签 % medoidsIdx - 每个簇中心样本的行号 % medoids - 每个簇中心的坐标 % totalCost - 聚类总代价 clear; clc; close all; %% 1. 数据导入 % 支持.xlsx、.xls、.csv、.txt % 要求:每一行是一个样本,每一列是一个特征 filePath = 'data.xlsx'; sheetName = 'Sheet1'; data = importData(filePath, sheetName); X = data; %% 2. 数据标准化(强烈建议执行) % 各个特征的量纲差异会直接影响欧氏距离的计算结果 % 比如身高单位是cm、体重单位是kg,数值尺度不同,会导致距离被大数值特征主导 % zscore:每个特征减去均值,再除以标准差 % minmax:缩放到[0,1] method = 'zscore'; switch method case 'zscore' X = zscore(X); case 'minmax' X = (X - min(X)) ./ (max(X) - min(X)); end %% 3. 聚类参数设置 % k:期望的聚类个数,可用肘部法则或轮廓系数辅助确定 % maxIter:最大迭代次数,PAM通常几十轮就能收敛 k = 3; maxIter = 100; distMetric = 'euclidean'; % euclidean欧氏距离 / cityblock曼哈顿距离 %% 4. 调用k-medoids聚类 [idx, medoidsIdx, medoids, totalCost] = kmedoids_pam(X, k, maxIter, distMetric); %% 5. 画图展示结果 plotClusters(X, idx, medoidsIdx); %% 6. 命令行输出核心信息 fprintf('聚类的%d个medoid样本行号:', k); fprintf('%d ', medoidsIdx); fprintf('\n聚类总代价:%.4f\n', totalCost);

5.2 用一盘测试数据验证代码

手头没有现成数据文件也能测。我在脚本里专门留了一个生成测试数据的方法,三簇明显分离的二维数据,每簇30个点:

% 生成测试数据的代码(临时用) rng(123); X = [randn(30,2)*0.5 + [2 2]; randn(30,2)*0.5 + [-2 2]; randn(30,2)*0.5 + [0 -2]];

这组数据的真实结构是三簇,k取3,标准化那一步可以跳过。跑完以后,理想结果是idx里三类样本数量各30,medoidsIdx分别是三个簇各自中心附近的样本行号。totalCost的意义是全部样本到所属medoid的欧氏距离之和,数值越小表示簇内凝聚度越高。

我在测试中发现PAM对这种分离度好的数据收敛非常快,基本两三轮迭代就稳定了。真正的挑战在于簇之间有重叠、离群点明显的情况,那种数据下k-medoids的优势会比k-means更突出。

5.3 聚类结果合不合理,得有个评判标准

光画图还不够,最好量化地验证聚类质量。MATLAB里两个手段最常用。

第一个是轮廓系数silhouette(X, idx)。返回值是每个样本的轮廓值,范围在[-1,1],越接近1说明该样本离自己簇的中心近、离其他簇的中心远,聚类效果越好。平均轮廓值超过0.5,基本可以认为聚类结构是明显的。

第二个是看不同k值下的totalCost变化趋势,也就是肘部法则。把k从2试到8,记录每次的totalCost,画成折线图,曲线从陡峭下降转为平缓的那个拐点,就是一个相对合理的k值。这个思路很好理解:k太小,簇内距离还很大;k太大,每个簇都很小,再加簇收益就很低了。

实际操作时,我还会把聚类结果和已知标签做交叉比对,算一下调整兰德指数之类的指标。但这是有真实标签做验证时才能用的方法,无监督场景下轮廓系数是最主要的工具。

6. 跑k-medoids容易踩的坑与三个实用优化建议

6.1 空簇问题:某个簇没有样本

PAM迭代过程中,可能出现一个medoid没有任何样本被分配过来的情况。原因是初始化选到了离群点,或替换过程中某个medoid被"架空"。出现空簇后,程序本身一般不会报错,但后续替换测试会退化成一个空循环,最终结果里会有个簇标签没有任何样本,这显然不是我们想要的。

处理策略不复杂:在每次分配完成后检查一次,如果发现空簇,就把该簇的medoid替换成当前距离所有样本最远的那个点,让它成为一个新簇的种子。代码里加一小段:

% 检查空簇并补救 for m = 1:k if sum(idx == m) == 0 % 找离所有medoid最远的样本作为新medoid [~, farIdx] = max(minDist); medoidsIdx(m) = farIdx; % 更新分配 [minDist, idx] = min(D(:, medoidsIdx), [], 2); end end

这段代码放在分配阶段之后、替换测试之前。空簇是PAM少数的真实隐患,补上这段能避免很多莫名其妙的后续报错。

6.2 初始点敏感和可复现性

k-medoids和k-means一样,最终结果受初始点选择影响。随机初始化跑出来的结果可能有波动。我的做法是主脚本里用rng(42)固定随机种子,保证每次运行一致。但固定种子不等于结果最优,只是可复现。

如果想更稳定,可以采用多次随机初始化、取总代价最小的那一次作为最终结果。这个方法直接改主脚本里的调用方式:外层循环跑20次PAM,每次换一个随机种子,记录totalCost最小的那次。代价是运行时间乘20,但聚类质量通常会更可靠。

更进一步的优化是用类似k-means++的思路选初始medoid:先随机选第一个,之后每次选一个距离已有medoid尽可能远的样本。这种初始化方式能显著减少后续迭代次数,代码也不复杂,我建议在数据量较大的时候尝试。

6.3 距离度量选欧氏还是曼哈顿

主函数里distMetric参数我默认是euclidean,但实际使用时这个选择值得多想一步。欧氏距离对应的是"直线距离"的直觉,曼哈顿距离对应的是"沿坐标轴走"的场景,比如城市街区路网距离。如果特征里有个别维度取值异常大,欧氏距离会放大这些维度的影响,曼哈顿距离对异常值的敏感度低一些。

pdist函数本身支持多种距离度量,除了这两种,还有切比雪夫距离、马氏距离等。用户自定义距离函数也可以传进去,但要注意squareform和矩阵索引方式的兼容性。我从实践角度给个建议:如果特征已经做过zscore标准化,默认欧氏距离问题不大;如果数据是原始量纲且存在明显离群值,优先试曼哈顿距离。

6.4 数据量大时PAM跑不动怎么办

PAM的复杂度大约是O(k(n-k)^2),n超过几千的时候,每次替换测试都要重新计算全部样本到k个中心的距离,运行时间会让人失去耐心。预计算距离矩阵可以缓解一部分问题,但n=5000的时候,n×n的距离矩阵就有2500万个数,内存直接告急。

三个优化方向比较实用。第一,用pdist2按需计算距离,不预存整个矩阵,省内存但每次迭代都多跑一次距离计算。第二,替换测试不需要重新计算所有样本的距离,只要更新受影响的簇和边界样本,实际收益很大。第三,数据量特别大时换用CLARA算法——先从全量数据里抽多个子集,每个子集上跑PAM,最后选总代价最小的方案,这也是业界常用的折中做法。

pdist2按需计算的思路,参考代码:

% 优化:不预计算距离矩阵,分配阶段按需算 minDist = zeros(n, 1); idx = zeros(n, 1); for i = 1:n % 第i个样本到所有当前medoid的距离 d = pdist2(X(i,:), X(medoidsIdx,:)); [minDist(i), idx(i)] = min(d); end

这相当于用计算换内存,适合n较大但内存吃紧的场景。我的建议是n在2000以内用预计算矩阵,n更大就切到按需计算,具体阈值和机器内存有关,大家可以自己试一下。

另外提醒一句,MATLAB里randperm(n, k)在k和n接近时会比较慢,如果要做k和n相近的聚类,改用randsample或者先打乱再取前k个。

几个实际使用的补充体会

代码本身跑通只是一半工作,把聚类结果用好才是另一半。这里分享几个我自己常用的配套方法。

k值怎么选,我一直主张先看图再算指标。对二维数据,先把散点图画出来,肉眼看数据密集区域大概分几堆,这个直觉比很多指标都准。高维数据则要靠肘部法则加轮廓系数交叉验证,不要只信一个指标。我见过不少同学把轮廓系数当金标准,但轮廓系数在高维数据上会偏向球状簇,遇到不规则簇形反而会误导。

画图部分对二维数据还有个增强方案,把每个簇的凸包画出来,这样簇与簇的边界更直观。用convhull算凸包顶点,再patch填充,代码量不大,但报告效果提升不少。三维数据也可以做类似的事,只是一般没必要。

聚类结果要落地到决策,光有簇标签还不够。以客户分群为例,得到簇标签后,我通常还会对每个簇做一次描述性统计,比如均值、中位数、样本量,然后给每个簇起一个业务上能理解的名字。这时候k-medoids的优势就体现出来了——每个簇的medoid可以直接拿出来当"代表客户"去访谈、去研究,而不是讲一个抽象的均值向量。

最后给一个小工具函数:快速画出totalCost随迭代次数下降的曲线。在kmedoids_pam里把每轮的totalCostOld保存到一个数组里,返回结构再加一个字段,主脚本里直接plot。这个曲线是判断算法是否正常收敛的直观依据,如果曲线一路震荡不下降,多半是代码逻辑有bug。我在最初的版本里就遇到过替换判断条件写反导致曲线震荡的问题,当时全靠这张曲线图定位出来。

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

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

立即咨询