简介:基于层次聚类(HC)的数据聚类可视化MATLAB代码,配套Excel格式示例数据,适合机器学习入门读者、数据挖掘课程设计者以及需要快速验证聚类效果的算法工程师。资源围绕“读入Excel数据—执行层次聚类—可视化树状图与簇划分”这一主线,便于理解不同距离度量与连接准则对聚类结果的影响,也能直接替换为自己的数据表进行实验。压缩包约338KB,以MATLAB脚本和Excel数据文件为主,未单列文件总数,包体轻量、结构简明,便于按需修改;目前已有120人学习/下载。对刚接触聚类分析的同学而言,这份代码省去了从零搭建流程、处理数据读写与绘图细节的麻烦,可作为课程作业、论文复现或小型项目的基线方案,帮助快速输出可解释的聚类结果。
1. 层次聚类可视化:这份Matlab工程把Excel数据变成可直接解读的谱系图
层次聚类(HC)在数据分析里常常被当成“听过但没用过”的算法:K-Means要提前拍脑袋定K、对初始点敏感,而层次聚类不需要预设类别数,跑完一张谱系图就能看清数据的内在结构。这份基于Matlab的层次聚类可视化工程,把Excel表格作为唯一数据入口,走完“读取、标准化、距离计算、聚类、谱系图、回写标签”的完整链路,输出物是一张带大簇颜色标注的聚类树和一份每个样本对应的分组编号。适合三类人:做用户或商品分群的业务分析人员、需要快速验证样本可分性的研究者、想在最小Matlab工程上做二次开发的初学者。
2. 项目结构与技术选型:为什么是层次聚类+Matlab+Excel
2.1 这份资源到底解决什么问题
先把这个工程的位置摆清楚。它不是一个从零实现层次聚类算法的教学代码,而是一个把Matlab统计工具箱里现成的pdist、linkage、dendrogram、cluster四个函数串成完整分析链路的落地工程。对使用者来说,最直接的价值是:你只需要准备一份格式正确的Excel,改一行文件路径,就能看到聚类结果。我对这个工程做完拆解之后的评价是,它更像一个分析“骨架”——所有核心步骤都有代码实现,但具体参数(聚类方法、簇数、颜色阈值)都留给你根据实际数据调整。
举一个实际场景。某公司在给用户做分群,原始特征是Excel里的消费金额、访问次数、停留时长和转化率,一共800多条记录。如果用K-Means,第一步就得定K,而且改一次K要重跑一次;换成层次聚类的方案,跑完直接看谱系图,整棵树有两个明显的高度断层,对应三类用户,于是把Cutoff设成断层高度,三类用户自然就出来了。这就是这份资源的核心定位:让聚类从“凭经验猜K”变成“看图定K”。
对于还在纠结“这份代码跑出来到底长什么样”的人,提前给个预期:控制台会打印出样本数、特征数和cophenet相关系数;图形窗口会弹出一棵从下往上合并的树状图,同一大簇的分支用同一颜色;result目录里会多出一张谱系图和一份带分组标签的CSV文件。这些东西加起来,就是一次完整的层次聚类分析交付物。
2.2 目录结构与关键文件职责
整个工程的文件布局非常紧凑,拿到手里一眼能看明白分工:
hc-cluster-visualization/ ├── main.m % 唯一入口:配置参数,按步骤执行聚类链路 ├── load_data.m % 读取Excel,检查列类型,执行标准化 ├── plot_dendrogram.m % 绘制谱系图,支持自定义颜色阈值 ├── cluster_assign.m % 将聚类树转换为每个样本的类别标签 ├── data/ │ └── sample_data.xlsx % 示例数据:样本ID + 多个数值特征列 └── result/ └── dendrogram.fig % 运行后生成的谱系图我实际跑工程时,只在main.m一个文件里做调整。路径、sheet名、聚类方法、是否强制指定簇数,都在文件开头的配置区修改。其余三个函数文件,除了改图和改标签颜色外基本不动。这种“单入口配置、少量函数分工”的结构,对二次开发很友好:你想换数据格式,只改load_data.m;你想改可视化风格,只改plot_dendrogram.m;其他部分完全不用碰。
各文件的分工在工程里是明确分离的:main.m负责流程编排,它不关心算法细节;load_data.m把外部数据变成内存里的数值矩阵,屏蔽了Excel格式差异;cluster_assign.m把linkage生成的层次树翻译成人能读懂的类别标签。这种各司其职的划分,让代码既好调试也好扩展。如果某天你想把入口换成CSV或者数据库,只需要替换load_data.m一个函数即可。
2.3 技术选型的三点理由
第一,算法层面为什么用层次聚类而不是K-Means。K-Means要求预先给定K值,且对初始中心敏感,同一份数据跑两次可能有不同结果;层次聚类是确定性算法,同一份数据用同样的距离和连接方法,跑多少次结构都一样。更关键的是,层次聚类会生成完整的合并树,你可以执行完聚类之后再决定从哪个高度切分,这个特性在探索性数据分析里价值极高。Excel里几十个特征列互相之间的远近关系,用谱系图一次看全,这是其他聚类方法很难给到的体验。
第二,工具层面为什么选Matlab。在聚类这个细分场景,Matlab统计与机器学习工具箱提供了一套完整的函数链:pdist负责两两距离,linkage负责构建层次树,dendrogram负责绘图,cluster负责按数量或高度切树——四个函数恰好覆盖一次聚类分析的全部环节。相比Python用SciPy时需要自己处理压缩距离矩阵和谱系图对象,Matlab的默认参数更少、报错更直接,对刚接触聚类的人来说门槛更低。这也是很多教学场景和业务快速验证都选择Matlab的原因。
第三,数据层面为什么锁定Excel。业务方手里的大多数数据最终都会汇总成Excel,readtable可以零配置读取xlsx/xls文件,不需要建数据库、写SQL,把文件丢进data/目录就能跑。对于那些一天要试多批数据的场景,Excel几乎是你能想到的最顺手的入口。这里也可以补充一句:这份工程本质上是Matlab聚类能力的一个轻量“插件化”封装,它不替代Matlab本身,而是把常用的聚类流程固定成一套可复用步骤——你先装好Matlab这个“软件”,再把这套工程当成“插件”来使,这正是它好用的原因。
单从层次聚类的实现难度来看,从零写一个需要处理“簇间距离更新、树结构保存、谱系图坐标计算”三个难题,没有几百行代码下不来。这份工程聪明的地方在于把这些难题全部交给了Matlab内置函数,保留下来的是数据分析本身:数据怎么读入、特征怎么标准化、结果怎么解读。不重复造轮子的思路,对今天做数据工作的开发者来说其实更受用——你的精力应该花在业务问题上,而不是重新实现一遍已有算法。
3. 核心代码拆解:从Excel读入到聚类谱系图输出的完整链路
3.1 数据读取:从Excel到干净的数值矩阵
load_data.m承担了整个工程的数据入口逻辑。它要做两件事:把Excel中参与聚类的数值列提取出来,同时保留每行对应的样本名称。来看核心实现:
function [X, labels, var_names] = load_data(excel_path, sheet_name) % 读取Excel指定Sheet为表格对象 raw_table = readtable(excel_path, 'Sheet', sheet_name); % 第1列约定为样本名称(字符串),其余列作为聚类特征 labels = raw_table{:, 1}; X = raw_table{:, 2:end}; % 逐列做标准化,避免量纲差异主导距离计算 X = zscore(X); % 返回数值特征列名,方便后续绘图标注横纵轴 var_names = raw_table.Properties.VariableNames(2:end); end这段代码里的readtable会智能识别每列类型:文本列自动变成cell数组,数值列自动变成double数组。raw_table{:, 2:end}取出的是一个数值矩阵,这里隐含一个约定:Excel的第二列到最后一列必须全部是数值型。如果中间混入文本,X的类型就不是double,下一步pdist会直接报错。
zscore按列把均值归零、标准差归一,这是层次聚类里几乎必做的一步,否则“销售额”这种大数值列会压过“转化率”这种小数值列。多说一句,zscore默认按列计算,和我们希望的“每个特征各自标准化”完全一致。如果拿到一份量纲差异很大的Excel,这行代码就是决定聚类结果是否合理的关键。常见做法是先把X备份一份原始值,因为标准化之后的矩阵已经丢失了业务单位,后续画散点图如果用原始值会更好解释。
3.2 聚类核心链路:距离矩阵、层次树与谱系图
这是工程里最关键的十几行,全部集中在main.m中。逻辑上分三步:先求样本两两距离,再按距离构建层次聚类树,最后把树画成谱系图。
% 计算样本间距离,返回压缩距离向量 D = pdist(X, 'euclidean'); % 使用Ward方差最小化策略构建层次聚类树 Z = linkage(D, 'ward'); % 绘制谱系图,并让大簇自动上色 figure('Position', [100, 100, 900, 600]); dendrogram(Z, 'Labels', labels, 'ColorThreshold', 'default');pdist返回的不是完整的N阶矩阵,而是只保存三角形下三角部分的压缩向量,这在样本量上千时能省下一大块内存。linkage的第二个参数'ward'是最常用的合并策略,每次合并使簇内离差平方和的增量最小,生成紧凑的簇;如果你发现数据有明显长尾,可以改成'average'。dendrogram的ColorThreshold指定了树干着色的高度阈值,设成'default'时Matlab会按树的最大高度自动分级上色,不同大簇一眼就能区分。
有一个容易被忽略的点:dendrogram默认最多显示30个叶节点。如果你的数据只有十几个样本,全部显示没问题;数据到了50个以上,整个图会非常挤,标签全部叠在一起。工程里的标准处理方式是控制显示数量:
N = size(X, 1); if N > 30 dendrogram(Z, 30, 'Labels', labels, 'ColorThreshold', 'default'); else dendrogram(Z, 'Labels', labels, 'ColorThreshold', 'default'); end第二个参数30的含义是“只显示树中最新的30个叶节点”,其余节点被折叠。这不会改变聚类树本身,只是让图更可读。如果要把聚类分析结果用于汇报,传统做法是导出一张高分辨率的PNG。新版Matlab里用exportgraphics更干净:
exportgraphics(gcf, 'result/dendrogram.png', 'Resolution', 300);这条指令在R2020a以后的版本都能用,替代了过去容易出问题的print加-dpng组合。300dpi的输出可以直接放进报告或PPT里,比.fig文件通用得多。
3.3 从树到标签:聚类结果如何回写样本
谱系图给人看,cluster函数则负责把树变成机器可读的标签。仍以main.m中的代码为例:
% 方式一:指定最多类别数 k = 3; category = cluster(Z, 'MaxClust', k); % 方式二:按树的高度阈值切分 cutoff = 0.6 * max(Z(:, 3)); category = cluster(Z, 'Cutoff', cutoff); % 评估:计算树对原始距离的保留程度 c = cophenet(Z, D); fprintf('cophenetic correlation coefficient: %.3f\n', c);MaxClust和Cutoff这两种切法语义差异很大。MaxClust, 3表示从树顶部向下剪,最终类别数不超过3;如果树顶层的两个大簇距离过远,函数会自动把类别数降到2,这往往让习惯了K-Means的人困惑。Cutoff则是按距离阈值剪枝——所有距离大于阈值的分支被拆开,得到的簇数是数据决定的而不是人决定的。实际业务里如果已经通过谱系图看到了清晰的断层高度,我会把Cutoff设成断层附近的数值,这样得到的结果最符合数据本身的分布。
cophenet系数的作用是检验距离矩阵被树结构保留了多少,通常0.6以上可以认为聚类树可信,低于这个值说明数据本身没有明显的层次结构,继续分簇意义不大。最后把标签写回并落盘,这一步在工程里由cluster_assign.m完成:
result_table = table(labels, category, ... 'VariableNames', {'SampleID', 'Cluster'}); writetable(result_table, 'result/cluster_result.csv'); % 用前两个特征画散点着色图 figure; gscatter(X(:,1), X(:,2), category); xlabel(var_names{1}); ylabel(var_names{2}); grid on;gscatter按分类变量自动上色,不需要手动写循环。注意此时X已经被zscore标准化,散点图的横纵轴数值已经没有原来的业务单位,解释时要说明这是标准化后的分布。如果希望散点图显示原始数值,需要在标准化之前备份一份原始矩阵。整个回写过程的优势在于,它把可视化结果和数据分析无缝衔接了起来——谱系图告诉你看结构,CSV告诉你可以继续往下做统计建模。
4. 跑通全流程:Excel格式确认、运行步骤与参数微调
4.1 Excel数据格式约定
这一节把数据格式拆得细一点,你可以直接照着准备。Excel文件的要求有三条:A1单元格开始,第一行是列名;第一列是样本标识,文本形式,比如用户ID、商品SKU、城市名;第二列到最后一列必须是数值型特征。一个可以正常读入的示例:
SampleID Amount(万元) Visits Conversion(%) U001 12.5 45 3.2 U002 8.3 22 1.8 U003 20.1 67 5.1这个表有一个容易翻车的地方:列名里的括号和百分号不影响readtable读取,但如果某列有空单元格,readtable会自动填入NaN。NaN对zscore不致命,但pdist会直接报错。所以拿到Excel之后,先看一眼有没有空行或空列。我一般会在load_data.m里临时加一行检查:
fprintf('样本数: %d, 特征数: %d, 含NaN单元格数: %d\n', ... size(X,1), size(X,2), sum(isnan(X(:))));如果含NaN单元格数不为零,就要回Excel补数或删除对应行。另一个常见问题是表头占了两行,比如第一行是标题、第二行才是列名。解决方案是readtable增加'NumHeaderLines', 2参数,或者在Excel里先合并单元格、整理成单行表头再跑。
4.2 运行全流程的五步操作
整个跑通流程只需要五步。
第一步,把Excel文件放进data/目录,文件名不要带中文和空格,避免老版本Matlab对路径解析出错。第二步,打开main.m,修改开头的配置参数:
excel_path = 'data/sample_data.xlsx'; sheet_name = 'Sheet1'; linkage_method = 'ward'; % ward/average/complete/single distance_metric = 'euclidean'; color_threshold = 'default'; % 或者填写数值作为着色阈值第三步,直接运行整个脚本。如果一切正常,命令行会打印出样本数、特征数和cophenet系数,Figure窗口弹出谱系图。第四步,查看result/目录,确认生成了dendrogram.fig和cluster_result.csv。第五步,用表格软件打开cluster_result.csv,核对每行样本所属的簇编号是否和谱系图里的大簇目测一致。
这五步看着简单,但有一个容易忽略的点:运行前确认当前目录是工程根目录。很多人直接在编辑器里打开main.m,忘记把工作目录切到工程文件夹,导致data/sample_data.xlsx这个相对路径找不到,报错“文件或目录不存在”。这类问题排查起来很费时间,运行前看一眼工作目录是最有效的预防方式。另外,脚本运行中如果报错中断,多半是数据格式问题,优先检查第4.1节提到的两类情况。
4.3 聚类参数选择与结果验证
这份资源留出来的可调参数不多,但每个参数的改动都会明显改变结果。下面按经验整理了一张参数表:
| 参数位置 | 可选值 | 说明 |
|---|---|---|
pdist距离 | 'euclidean'/'cityblock'/'cosine' | 连续数据默认欧氏;含奇异值用曼哈顿;文本或方向类特征用余弦 |
linkage方法 | 'ward'/'average'/'complete'/'single' | ward适合紧凑簇;average抗异常值;single慎用,易链式合并 |
ColorThreshold | 'default'/ 具体高度值 | 控制谱系图大簇着色,值越小颜色区分越碎 |
cluster切法 | 'MaxClust'/'Cutoff' | MaxClust指定最多簇数;Cutoff按树高切分,簇数由数据决定 |
至于结果怎么验证,推荐两种手边就能做的办法。第一种是看cophenet系数,低于0.6时考虑换linkage方法。第二种是对照业务常识检查簇的特点:把cluster_result.csv按簇号分组,计算每个簇的特征均值,看是否满足你对“这几类样本应该有差异”的预期。比如用户分群里,第1簇的消费金额均值明显高于其他簇,那这个分簇结果才有业务解释意义。如果某簇在全部特征上几乎和另一簇完全重合,说明聚类过度切分了,需要调低切分数,或把Cutoff阈值调高让树合并更彻底。
4.4 从谱系图读取“断层”并确定最佳聚类数
谱系图的作用不只是展示,更是一个切割决策工具。树的高度差异在图上呈现出“长枝”和“短枝”的交界,也就是断层。实际操作中,我会在谱系图上找到最长的那几根竖线,把明显低于它们的分叉全部剪掉,剩下的分支数量就是合理的聚类数。这个操作和cluster函数的Cutoff参数是对应的:Cutoff, h把横线画在高度h处,与横向分支相交的数量就是聚类数。能自动计算这个数量,代码里可以这样表达:
% 按某个高度切树并统计簇数 tmp_cluster = cluster(Z, 'Cutoff', cutoff); num_groups = length(unique(tmp_cluster)); fprintf('Cutoff=%.2f时得到%d个簇\n', cutoff, num_groups);这个方法在探索性分析里几乎百试百灵。相比K-Means需要画肘部图找拐点,基于谱系图断层的判断对异常值更稳。还有一个更简单的经验:把cutoff从大到小扫一遍,记录每次的簇数,画一张“cutoff-簇数”折线图,平缓段的转折点就是比较合适的切树位置。当你在报告里写“本数据集分成三类”,别人问你怎么确定的,你直接说“谱系图断层高度,配合阈值扫描”,专业感完全不一样。
5. 踩坑与排查:Matlab层次聚类实战高频问题记录
5.1 pdist报错:数据里有文本或NaN
现象:运行pdist(X, 'euclidean')时命令行抛出错误,提示输入不是数值矩阵或包含NaN。
原因:Excel里第二列之后混入了文本单元格(比如填了“暂无”两个字),或者存在空单元格被readtable读成了NaN。zscore遇到NaN会原样保留,但pdist在算距离时无法处理缺失值,直接中断。
解决:在load_data.m加入剔除非数值列的检查:
numeric_cols = varfun(@isnumeric, raw_table, 'OutputFormat', 'uniform'); X = raw_table{:, numeric_cols(2:end)}; if sum(~numeric_cols) > 1 warning('已自动剔除非数值列:%s', ... strjoin(raw_table.Properties.VariableNames(~numeric_cols), ', ')); end这样脚本遇到坏数据不会直接崩,而是把能用的数值列挑出来继续跑。需要说明的是,这个处理会在控制台打印警告信息,跑通后记得回Excel把文本单元格改成数字,或者确认这些列本来就该剔除。
5.2 聚类结果被某个量纲大的特征牵着走
现象:聚类后每一簇的差异主要体现在“销售额”这一类数值很大的特征上,其他特征在簇间几乎一样,谱系图看起来两个大簇被一条长支硬生生隔开。
原因:没有做标准化,欧氏距离里大数值列占据绝对主导地位。比如“销售额”单位是万元,数值是几十;“转化率”是百分比,数值是个位数,两列对距离的贡献不在一个数量级上。
解决:在进入pdist之前对X做zscore。这份工程自带的load_data.m已经做了标准化,但如果你自己改写读取逻辑,很容易漏掉这一行。注意zscore默认按列操作,和“每个特征单独归一化”的目标正好匹配,不需要额外加参数。
5.3 谱系图标签重叠成一团
现象:样本超过40个后,谱系图底部标签横七竖八叠在一起,没法读清某个样本属于哪条分支。
原因:所有样本名称被强制画在x轴,叶节点密度高导致标签互相遮盖。
解决:用dendrogram(Z, 30)限制显示最近合并的30个叶节点,或者把标签旋转45度:set(gca, 'XTickLabelRotation', 45)。还有一种情况是标签本身太长,可以在readtable之后对labels做一次截断处理。业务汇报场景我一般直接限制叶节点数量,因为多数情况下只需要展示大簇结构,不需要把每个样本名都露出来;如果真要展示全部,配合导出PNG比在屏幕上拖动窗口体验好得多。
5.4 MaxClust返回的类别数比指定值少
现象:设置了'MaxClust', 3,但category里只有2类,和预期不符。
原因:MaxClust是上限而不是目标值。当树顶层两个大簇的距离远大于内部距离时,Matlab认为按3类切会产生只有一两个样本的孤立簇,于是自动减少类别数。这其实是算法的合理行为,但容易让不熟悉层次聚类的人误以为代码有bug。
解决:想要严格三类,用clusterdata(X, 3)这种强制指定方式,或者在谱系图上找到三类对应的两个Cutoff高度,直接按高度切。要明确一个边界:强制切出三类不代表结果有业务意义,如果树的顶层大类本身就该是两类,硬分成三类只是自欺欺人。看谱系图判断才是正路。
5.5 别人打开.fig报缺函数
现象:把生成的dendrogram.fig发给同事,对方Matlab没有安装统计与机器学习工具箱,打开报错或者图形一片空白。
原因:fig文件保存的是图形对象,dendrogram生成的对象依赖工具箱的类定义,缺工具箱时无法还原图形。
解决:在运行结束后额外导出通用图片:
exportgraphics(gcf, 'result/dendrogram.png', 'Resolution', 300);PNG一张图就能解决所有沟通问题。这也是我推荐把“导出PNG”写进工作流的原因:图比fig文件通用得多,发到群里、贴进报告、投到PPT里都无障碍。如果对方确实需要可编辑的图,再让他装工具箱或者把导出格式换成svg。
5.6 linkage的single方法聚类结果像链条
现象:数据在中等数量下,single方法生成的谱系图像一个不断连点的链条,看不出明显的簇状结构。
原因:single也就是最近邻法,只关注两个簇之间最近的点,容易被离群点“串”起来,形成链式效应。这在层次聚类的课本里专门有提到,实际数据里几乎一踩一个准。
解决:换成ward或average。判断哪个方法适合当前数据,可以对比不同方法的cophenet系数,取最高者。需要注意,cophenet系数也不是越高越好,它衡量的是“树”和“原始距离”的一致性,但一致性高不等于簇之间有实际业务区分度,最终还是要结合业务解释来定。
6. 二次开发方向:把单次可视化脚本改造成可复用的聚类分析入口
从拿到这份代码到实际落地使用,最值得做的改动就是把它从一个固定路径的脚本改造成参数化入口。我建议先把main.m里所有读路径、聚类别数的硬编码抽成函数入参,变成类似下面这样一个接口:
function result = run_hc_analysis(excel_path, sheet_name, varargin) % 输入Excel路径、sheet名,可选参数: 'Method', 'Cutoff', 'MaxClust' p = inputParser; addRequired(p, 'excel_path', @ischar); addRequired(p, 'sheet_name', @ischar); addParameter(p, 'Method', 'ward', ... @(x) ismember(x, {'ward', 'average', 'complete', 'single'})); addParameter(p, 'MaxClust', [], @isnumeric); parse(p, excel_path, sheet_name, varargin{:}); % 后续接原有聚类流程 end用inputParser做参数校验是Matlab里比较工程化的写法,能为后续脚本批量化留出空间。改完之后,我通常还会加一个export_report函数,把每次跑的聚类方法、样本数、cophenet系数和得到的簇数写进一张汇总表,这样多次调参的过程就有迹可循:
T = table({excel_path}, k, c, num_groups, ... 'VariableNames', {'DataSource', 'MaxClust', 'Cophenet', 'NumGroups'}); writetable(T, 'result/run_log.xlsx', 'WriteMode', 'append');从那以后,我每次拿一份新数据跑聚类,都强制走一遍这个入口:先读数据看一眼列名,再跑默认参数,最后对着谱系图的断层高度微调Cutoff。整个流程从五分钟压缩到两分钟,而且每次跑的参数和结果都留在日志里,回溯问题时再也不用靠脑子记。这个改造本身不复杂,但把“一次性脚本”变成“可复用工具”的体验,确实值得你花半小时做一次。希望帮到你。
本文还有配套的精品资源,点击获取