简介:这份资源面向需要快速上手聚类分析的数据分析初学者与科研人员,提供一套基于层次聚类(HC)的数据聚类可视化方案,用Matlab代码实现,配套数据以Excel格式存放,便于直接替换成自己的数据集进行实验。压缩包为zip格式,整体约338KB,体量轻巧,适合课堂演示、课程作业或论文中的聚类结果展示。资源围绕层次聚类展开,涵盖从数据读取、距离计算到聚类树与分类结果可视化的完整流程,读者可据此理解HC的合并策略与树状图解读方式,并在此基础上调整参数、更换数据以适配不同场景。目前已有120人学习下载,可作为入门聚类算法、验证实验思路的实用参考,帮助节省从零搭建代码框架的时间。
1. 从一张 Excel 表到一张聚类树:这套 HC 可视化代码到底能省掉什么
手里有一份 Excel 数据表,几十行到几百行样本,每行几个数值型指标,老板或者导师要你「把相似的样本归归类,画个图看看」。这个场景下,很多人第一反应是打开 Python 写scipy.cluster.hierarchy,但如果你日常主力环境就是 MATLAB,来回导数据、配环境反而更折腾。这套基于层次聚类(Hierarchical Clustering,HC)的 MATLAB 代码,解决的就是这个具体问题:读入 Excel 格式的数据,做层次聚类,输出聚类树状图和分类结果,全程不离开 MATLAB。
它适合谁?一类是做实验数据分析、问卷分群、样本归类,手头数据本来就是 Excel 的从业者和学生;另一类是想快速验证聚类效果、不想从零写距离矩阵和 linkage 逻辑的人。层次聚类本身不需要预先指定簇数量,会生成一棵完整的合并树,你可以在任意高度切一刀得到分类,这对「不知道该分几类」的探索性分析特别友好。代码把数据读取、标准化、距离计算、linkage、可视化串成了一条线,拿到就能跑,改改文件路径和参数就能套自己的数据。
2. 层次聚类的数学骨架:距离、linkage 与树状图怎么对上
2.1 从样本到距离矩阵:HC 到底在算什么
层次聚类的核心动作只有两步:先算样本两两之间的距离,再按某种规则把最近的样本或簇不断合并。第一步决定了「相似」怎么定义,第二步决定了「合并」的策略。假设你有 n 个样本,每个样本是 m 维特征向量,第一步会得到一个 n×n 的对称距离矩阵,对角线为 0。MATLAB 里最常用的是pdist函数,它把 n 个样本压成一个长度为 n(n-1)/2 的距离向量,而不是完整矩阵,这样省内存。
距离度量默认是欧氏距离(euclidean),但这不是唯一选择。如果你的特征是「城市街区距离」更合理,可以换成cityblock;如果特征维度高且关心方向而非绝对大小,cosine更合适。这一步选错,后面整棵树都会歪,所以别默认无脑用欧氏距离。
% 假设 data 是 n×m 的数值矩阵,每行一个样本 % 计算两两距离,返回压缩向量 Y Y = pdist(data, 'euclidean'); % 查看距离向量长度,应为 n*(n-1)/2 n = size(data, 1); fprintf('样本数 %d,距离向量长度 %d\n', n, length(Y));这段代码里pdist的第二个参数就是距离度量,改成'cityblock'、'cosine'、'correlation'都可以。Y不是方阵,是上三角展开的向量,后面linkage直接吃这个格式。如果你拿到的是已经算好的距离矩阵,用squareform转一下再喂给linkage。
2.2 linkage 的四种策略:single、complete、average、ward 怎么选
linkage决定簇与簇之间距离怎么算,这是层次聚类最容易踩坑的地方。四种常见策略:
| 策略 | 簇间距离定义 | 倾向 | 适用场景 |
|---|---|---|---|
| single | 两簇最近点距离 | 容易产生链状长簇 | 样本呈长条分布 |
| complete | 两簇最远点距离 | 倾向紧凑等大簇 | 簇大小相近 |
| average | 两簇所有点平均距离 | 折中,较稳健 | 通用默认 |
| ward | 合并后方差增量最小 | 倾向等方差紧凑簇 | 数值特征、希望簇紧凑 |
ward在数值型数据上通常表现最好,因为它最小化合并带来的组内平方和增量,倾向于生成大小接近的球形簇。但ward要求距离是欧氏距离,换成cityblock再用ward在数学上就不自洽了。我一般会先用average跑一版看整体结构,再用ward对比,如果两者树形差异很大,说明数据结构本身不清晰,得回头检查特征和标准化。
% 用 ward 策略做层次聚类 Z = linkage(Y, 'ward'); % Z 是 (n-1)×3 矩阵,每行记录一次合并: % 前两列是被合并的簇编号,第三列是合并时的距离 fprintf('linkage 矩阵大小:%d × %d\n', size(Z,1), size(Z,2));Z的第三列(合并距离)是画树状图纵轴的依据,也是你决定切几类的参考——相邻合并距离如果出现明显跳跃,那一刀通常切在跳跃前。
2.3 树状图与分类结果:dendrogram 和 cluster 的配合
dendrogram(Z)画出树状图,横轴是样本编号,纵轴是合并距离。看图时关注两件事:一是整体是否分成几个明显的大分支,二是纵轴高度在哪个位置出现「断层」。cluster(Z, 'maxclust', k)可以按最大簇数切,cluster(Z, 'cutoff', h)可以按距离阈值切。
% 画树状图 figure; dendrogram(Z); title('层次聚类树状图'); xlabel('样本编号'); ylabel('合并距离'); % 按最大簇数切分,比如切 3 类 k = 3; idx = cluster(Z, 'maxclust', k); % idx 是 n×1 的类别标签 disp('各类样本数:'); disp(histcounts(idx, 1:k+1));cluster的'maxclust'参数是「最多切 k 类」,实际可能少于 k;'cutoff'是按距离切,更贴近树状图上的视觉判断。两种方式结合用:先看树状图找断层高度,再用cutoff精确切,最后用maxclust验证类别数是否合理。
3. 把 Excel 数据喂进 MATLAB:读取、清洗与标准化
3.1 readtable 读 Excel:列名、缺失值和类型陷阱
MATLAB 读 Excel 现在首选readtable,它返回 table 类型,列名自动从首行读取,比老的xlsread干净得多。但有几个坑:如果 Excel 首行不是列名,readtable会把数据首行当列名;如果某列混了文本和数字,整列可能被读成 cell 或 string;空单元格默认读成NaN(数值列)或缺失字符串。
% 读取 Excel 文件,指定首行为变量名 filename = 'data.xlsx'; T = readtable(filename, 'VariableNamingRule', 'preserve'); % 查看前几行和列类型 head(T) summary(T)'VariableNamingRule', 'preserve'保留原始列名(包括中文和空格),不加这个参数 MATLAB 会把列名改成合法变量名,中文列名可能被改得面目全非。summary(T)能一眼看出每列的类型、缺失值数量和取值范围,读进来第一件事就该跑它。
3.2 缺失值处理与特征列筛选
真实 Excel 数据几乎不可能干净。缺失值处理没有万能方案,但常见做法是:缺失比例低于 5% 的样本直接删,高于 5% 的列考虑整列剔除或插补。插补用均值、中位数都行,但要在文档里记清楚,因为插补会人为缩小方差。
% 假设 T 是读进来的 table,只保留数值列做聚类 numericCols = varfun(@isnumeric, T, 'OutputFormat', 'uniform'); Tnum = T(:, numericCols); % 把 table 转成矩阵 data = table2array(Tnum); % 删除含 NaN 的行 data(any(isnan(data), 2), :) = []; % 或者用中位数插补 % data = fillmissing(data, 'constant', median(data, 'omitnan')); fprintf('清洗后样本数:%d,特征数:%d\n', size(data,1), size(data,2));varfun(@isnumeric, ...)筛出数值列,避免把 ID 列、备注列也塞进聚类。table2array转矩阵后any(isnan(data),2)找出任何含 NaN 的行并删除。注意:如果 ID 列是数值但无意义,聚类前一定要手动排除,否则 ID 的大小会污染距离计算。
3.3 zscore 标准化:为什么不做这一步结果会骗你
这是最容易被忽略、后果最严重的一步。假设你有两个特征:一个取值范围 0-1,另一个 0-10000。欧氏距离里第二个特征完全主导,第一个特征等于没参与。标准化把每个特征变成均值 0、标准差 1,让各特征等权。
% 对每列做 z-score 标准化 dataNorm = zscore(data); % 验证:标准化后每列均值约 0,标准差约 1 fprintf('标准化后均值:%s\n', mat2str(round(mean(dataNorm), 4))); fprintf('标准化后标准差:%s\n', mat2str(round(std(dataNorm), 4)));zscore默认按列操作,正好对应「每个特征独立标准化」。如果你的数据本身量纲统一(比如全是百分比),标准化不是必须,但做了也不会错。唯一例外是特征里有明确的「0 表示不存在」的稀疏结构,标准化会破坏这种语义,那种情况要谨慎。
4. 完整可复现流程:从 data.xlsx 到分类结果导出
4.1 主脚本骨架:一条线跑通读取到出图
把前面几节拼起来,一个能直接跑的主脚本长这样。我习惯把参数集中在开头,方便改。
%% 层次聚类主脚本 clear; clc; close all; % ===== 参数区 ===== filename = 'data.xlsx'; % Excel 文件路径 distMetric = 'euclidean'; % 距离度量 linkMethod = 'ward'; % linkage 策略 k = 3; % 切分类别数 doStandardize = true; % 是否标准化 % ===== 读取数据 ===== T = readtable(filename, 'VariableNamingRule', 'preserve'); numericCols = varfun(@isnumeric, T, 'OutputFormat', 'uniform'); data = table2array(T(:, numericCols)); data(any(isnan(data), 2), :) = []; % ===== 标准化 ===== if doStandardize data = zscore(data); end % ===== 聚类 ===== Y = pdist(data, distMetric); Z = linkage(Y, linkMethod); idx = cluster(Z, 'maxclust', k); % ===== 可视化 ===== figure('Position', [100 100 900 400]); subplot(1,2,1); dendrogram(Z); title(['树状图 (' linkMethod ')']); xlabel('样本编号'); ylabel('合并距离'); subplot(1,2,2); gscatter(data(:,1), data(:,2), idx); title(['前两特征散点 (k=' num2str(k) ')']); xlabel('特征1'); ylabel('特征2'); % ===== 导出结果 ===== Tresult = T; Tresult.Cluster = idx; writetable(Tresult, 'cluster_result.xlsx'); fprintf('结果已导出到 cluster_result.xlsx\n');参数区集中了所有可调项,改文件路径、换距离度量、换 linkage、改 k 都在这里。gscatter用前两个特征画散点并按类别着色,如果特征多于两个,这只是投影,不能完全代表聚类结构,但能快速看类别是否在某个平面上分离。
4.2 参数怎么调:距离、linkage、k 的联动
这三个参数不是独立的。ward只配欧氏距离;average和complete可以配多种距离;single对距离度量最不敏感但最容易出链状簇。k 的选择依赖树状图,不要拍脑袋定。
一个实用的调参流程:先用average+euclidean跑一版,看树状图整体形状;再换ward跑一版对比;如果两版树形差异大,说明数据结构不清晰,考虑换特征或降维。k 的确定看树状图纵轴,找合并距离跳跃最大的位置,跳跃前的高度对应的类别数就是候选 k。
% 对比不同 linkage 的树状图 methods = {'single', 'complete', 'average', 'ward'}; figure('Position', [100 100 1200 800]); for i = 1:4 subplot(2,2,i); Zi = linkage(Y, methods{i}); dendrogram(Zi); title(methods{i}); end四张图并排看,哪个策略的树状图分支最清晰、断层最明显,就用哪个。这一步花两分钟,能避免后面反复返工。
4.3 结果导出与复现:把类别标签写回 Excel
聚类结果最终要能交出去。最直接的做法是把类别标签作为新列加回原表,导出成 Excel。这样别人拿到文件,既能看到原始数据,也能看到每个样本被分到哪一类。
% 把类别标签加回原 table 并导出 Tresult = T; Tresult.Cluster = idx; writetable(Tresult, 'cluster_result.xlsx'); % 同时导出每类的统计摘要 for c = 1:k mask = idx == c; fprintf('第 %d 类:%d 个样本,特征均值 %s\n', ... c, sum(mask), mat2str(round(mean(data(mask,:), 1), 3))); endwritetable会自动处理列名和类型,中文列名也能正常写出。每类统计摘要能帮你快速判断分类是否合理——如果某一类只有一个样本,或者两类均值几乎一样,说明 k 选大了或特征区分度不够。
5. 避坑与排查:那些让树状图变成一团乱麻的原因
5.1 现象:树状图所有分支挤在底部,看不出结构
原因通常是没做标准化,某个大范围特征主导了距离,所有样本在这个特征上差异巨大,导致合并距离整体被拉高,小差异被压扁。解决:加zscore标准化,再重跑。如果标准化后仍然挤在一起,检查是否有常数特征(方差为 0),这种列对距离没贡献但会占维度,用std(data)==0找出来删掉。
5.2 现象:single linkage 出现一条长链,所有样本串成一串
这是 single linkage 的经典问题,叫链状效应。原因是它只看最近点距离,只要两个簇各有一个点靠近,就合并,慢慢串成一条链。解决:换average或ward。如果业务上确实需要 single 的敏感性,那就接受链状结构,但切类时要用cutoff而不是maxclust。
5.3 现象:readtable 读进来列名变成 Var1、Var2
原因是 Excel 首行不是列名,或者readtable默认的命名规则把非法字符替换了。解决:加'VariableNamingRule', 'preserve';如果首行确实是数据,用'ReadVariableNames', false让 MATLAB 自动生成列名,再手动赋值。
5.4 现象:cluster 切出来的类别数和 k 不一致
cluster(Z, 'maxclust', k)是「最多 k 类」,如果树状图在达到 k 类之前就出现了距离为 0 的合并(重复样本),实际类别数会少于 k。解决:先去重(unique(data, 'rows')),或者改用'cutoff'按距离切,切完用numel(unique(idx))确认实际类别数。
5.5 现象:换了台电脑跑,结果和之前不一样
层次聚类本身是确定性的,同样的数据和参数结果应该一致。不一致通常来自:MATLAB 版本差异导致linkage默认行为微调,或者数据读取时 Excel 里有隐藏行/列被读进来。解决:固定数据文件,用readtable后先size(T)确认行列数,再跑聚类。跨版本复现时,把Z矩阵存下来对比。
6. 进阶技巧:用 cophenet 验证聚类质量与多方案对比
跑完聚类不是终点,你得知道这棵树可不可信。MATLAB 提供了一个现成的指标:cophenet,它计算树状图对原始距离的拟合程度,值越接近 1 越好。这个指标能帮你判断哪种 linkage 策略更适合当前数据,而不是靠肉眼看树状图猜。
% 对比四种 linkage 的 cophenetic 相关系数 methods = {'single', 'complete', 'average', 'ward'}; fprintf('%-10s %-12s\n', 'Method', 'Cophenet'); for i = 1:4 Zi = linkage(Y, methods{i}); c = cophenet(Zi, Y); fprintf('%-10s %.4f\n', methods{i}, c); endcophenet(Zi, Y)的第二个参数必须是pdist输出的原始距离向量Y,不能是方阵。返回值在 0 到 1 之间,越高说明树状图越忠实地反映了原始距离结构。我一般会把这个值和树状图一起看:cophenet 高且树状图断层清晰,才认为聚类结果可靠。
除了 cophenet,另一个实用技巧是「多 k 值扫描」。不要只切一个 k,把 k 从 2 到 8 都切一遍,看每类的样本数分布和类内方差。
% 多 k 值扫描:看类内平方和随 k 的变化 maxK = 8; wss = zeros(maxK-1, 1); for kk = 2:maxK idxk = cluster(Z, 'maxclust', kk); s = 0; for c = 1:kk members = data(idxk == c, :); if size(members,1) > 1 s = s + sum(sum((members - mean(members,1)).^2)); end end wss(kk-1) = s; end plot(2:maxK, wss, '-o'); xlabel('类别数 k'); ylabel('类内平方和'); title('类内平方和随 k 变化');类内平方和随 k 增大单调下降,看的是「拐点」——下降速度突然变缓的位置,就是相对合理的 k。这个方法叫肘部法,配合树状图断层一起判断,比单看一个指标稳。
还有一个容易被忽略的点:特征选择。层次聚类对无关特征很敏感,塞进去一堆噪声列,距离结构就被稀释了。如果特征多,可以先做主成分分析(PCA),取累计方差贡献 85% 以上的前几个主成分再做聚类。MATLAB 里pca函数直接返回得分矩阵,拿来当输入就行。
% PCA 降维后再聚类 [coeff, score, latent] = pca(data); explained = cumsum(latent) / sum(latent); nPC = find(explained >= 0.85, 1); fprintf('保留 %d 个主成分,累计解释 %.1f%%\n', nPC, explained(nPC)*100); dataPCA = score(:, 1:nPC); Ypca = pdist(dataPCA, 'euclidean'); Zpca = linkage(Ypca, 'ward'); figure; dendrogram(Zpca); title('PCA 降维后层次聚类');PCA 之后聚类的一个副作用是:主成分是原始特征的线性组合,解释性变差,你没法直接说「第 3 类在原始特征 X 上偏高」。所以如果业务需要解释每个类的特征含义,PCA 要慎用,或者只在特征特别多时用来降维,聚类完再回到原始特征上做类间均值对比。
最后说个我自己的习惯:每次跑完聚类,不管结果看起来多好,都会把Z矩阵和idx一起存成.mat文件。因为调参过程中很容易改着改着就忘了哪版对应哪个结果,存下来才能回溯。从那以后我每次做聚类分析都强制走一遍「标准化 → 多 linkage 对比 → cophenet 验证 → 多 k 扫描 → 导出结果」这条线,少一步都可能在后面对不上数。希望帮到你。
本文还有配套的精品资源,点击获取