ML-For-Beginners 第5周第1课实战:聚类前先用散点图、相关热力图与 KDE 分布图看清数据结构
本篇技术指南围绕 ML-For-Beginners 仓库中「5-Clustering(聚类)」模块的开篇课程《Introduction to clustering》及其配套作业 assignment.md 展开:聚类是无监督学习,算法本身不依赖标签,但在选算法、定参数之前,你必须先"看懂"数据的结构。本文完整复现了课程在 Nigerian 音乐数据集(530 首歌、16 个字段)上的全部探索步骤——数据加载与完整性校验、柱状图筛选流派、相关系数热力图、KDE 联合分布与 FacetGrid 散点图——并解释每个可视化结论如何指向后续的 K-Means 聚类选型。读完后,你将能独立完成作业所要求的"用散点图研究聚类数据"任务:在一个 notebook 中产出至少 5 张有注释的散点图,并说明你从中发现了什么。
为什么聚类之前必须先做可视化
课程开篇给出聚类的定义:聚类属于无监督学习,它假定数据集没有标签、或输入不与预定义输出配对,算法通过发现数据中的模式来给出分组。与有监督任务不同,聚类没有标准答案可以对照,因此"数据探索阶段"承担了绝大部分判断责任:
- 点与点之间是松散聚集还是彼此独立?——决定"是否值得聚类";
- 数据是否落在近似平面(欧氏几何)上?——决定距离度量与算法类型;
- 密度是否均匀?——决定 K-Means 类算法是否会失效。
课程给出了聚类的三类典型生产场景:市场细分(例如判断哪些年龄段购买哪些商品)、异常检测(例如从信用卡交易中发现欺诈)、医学影像中的肿瘤定位。课程还提到一个容易被忽视的用途:为数据点分配 cluster id 之后,可以只引用 cluster id 而不暴露更具辨识度的原始字段,从而在保留数据价值的前提下保护隐私。
课程附注:cluster analysis(聚类分析)最早起源于 20 世纪 30 年代的人类学与社会心理学领域。
本课在整个仓库中的位置是 12 周、26 节课路线图的第 5 周(第 5 章"Clustering")第 1 课,配套材料包括:
- 主练习文件:notebook.ipynb(标题为 "Nigerian Music scraped from Spotify - an analysis",需要自行填充代码);
- 完成后的参考解答:solution/notebook.ipynb,同目录下还有
Julia与R子目录提供其他语言版本的解答; - 作业说明:assignment.md;
- 数据集:nigerian-songs.csv(530 行歌曲数据)。
聚类算法速览:本课可视化要回答的选型问题
课程正文附了一张源自 Scikit-learn 文档的算法与适用场景对照表,完整保留如下——它是"为什么先可视化"的直接答案:只有先看清数据的簇数、密度、几何形态,才能对号入座:
| 方法 | 适用场景 |
|---|---|
| K-Means | 通用、归纳式(inductive) |
| Affinity propagation | 数量多、大小不均的簇、归纳式 |
| Mean-shift | 数量多、大小不均的簇、归纳式 |
| Spectral clustering | 数量少、大小均等的簇、传递式(transductive) |
| Ward hierarchical clustering | 数量多、受约束的簇、传递式 |
| Agglomerative clustering | 数量多、受约束、非欧氏距离、传递式 |
| DBSCAN | 非平坦几何、大小不均的簇、传递式 |
| OPTICS | 非平坦几何、密度可变的不均簇、传递式 |
| Gaussian mixtures | 平坦几何、归纳式 |
| BIRCH | 含异常值的大数据集、归纳式 |
课程用四组术语解释了上表中的关键列,理解它们才能读懂"适用场景":
- Transductive(传递式)vs. Inductive(归纳式):传递式推理是从观测到的训练样本映射到特定的测试样本;归纳式推理先从训练样本学出一般规则,再套用到测试样本。课程给的例子是补全部分打标的音乐数据:归纳法会先训出"records/cds"两类再贴标签,遇到实际上是"cassettes"的数据就会出错;传递法则先把相似项聚成组再整体贴标签,对未知类别更稳健。
- Non-flat(非平坦)vs. flat(平坦)几何:flat 指欧氏几何(中学里的"平面几何"),两点距离是线段长度;non-flat 指非欧氏几何,距离沿曲线度量。如果你的数据可视化之后"不存在平面之上",就需要专门处理非欧氏结构的算法。
- Distances(距离):簇由其距离矩阵定义。欧氏簇以质心(centroid,中心点)为参考,距离即点到质心的距离;非欧氏情形使用 clustroid(簇内离其他点最近的那个点)作为参考。
- Constrained(受约束聚类):把"半监督"引入无监督方法,用 must-link / cannot-link 规则约束点的配对关系。课程举例:给算法附加"该物品必须由塑料制成""必须能发出音乐"这类规则后,它就不会把"饼干"也分进"音乐器材"簇里。
- Density(密度):噪声多的数据密度不均匀,同一算法在不同密度区域表现差异很大,需要匹配密度型方法(如 DBSCAN、OPTICS、Mean-shift)。
课程随后将主流算法归为五大家族,这也是作业 Challenge 要求你整理成图的骨架:
- Hierarchical clustering(层次聚类):对象按"与邻近对象而非远处对象的距离"归类,簇由成员间的距离自底向上合并而成。Scikit-learn 的 agglomerative clustering 属于此族。
- Centroid clustering(质心聚类):需要先给定簇数 k,算法确定每个簇的中心点并围绕其收集数据;K-Means 是最流行的版本(取"最近均值"得名,最小化点到簇的平方距离)。
- Distribution-based clustering(基于分布的聚类):以统计建模为中心,计算数据点属于某个簇的概率并据此分配,Gaussian mixture 属于此族。
- Density-based clustering(基于密度的聚类):按点与点的密度聚集情况分配簇,远离群体的点视为离群值或噪声;DBSCAN、Mean-shift、OPTICS 属于此族。
- Grid-based clustering(基于网格的聚类):对多维数据集建立网格,数据被划分到网格单元中,单元即簇。
环境与数据准备
练习的第一步是准备环境并加载数据。课程给出的安装与加载代码如下(注意数据文件位于 5-Clustering/data/ 目录下,README 中写的 ../data/nigerian-songs.csv 是相对于 notebook 所在目录的路径):
!pip install seaborn
import matplotlib.pyplot as plt
import pandas as pd
df = pd.read_csv("../data/nigerian-songs.csv")
df.head()
df.head() 的前 5 行如下(来自 nigerian-songs.csv,共 16 列):
| name | album | artist | artist_top_genre | release_date | length | popularity | danceability | acousticness | energy | instrumentalness | liveness | loudness | speechiness | tempo | time_signature | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Sparky | Mandy & The Jungle | Cruel Santino | alternative r&b | 2019 | 144000 | 48 | 0.666 | 0.851 | 0.42 | 0.534 | 0.11 | -6.699 | 0.0829 | 133.015 | 5 |
| 1 | shuga rush | EVERYTHING YOU HEARD IS TRUE | Odunsi (The Engine) | afropop | 2020 | 89488 | 30 | 0.71 | 0.0822 | 0.683 | 0.000169 | 0.101 | -5.64 | 0.36 | 129.993 | 3 |
| 2 | LITT! | LITT! | AYLØ | indie r&b | 2018 | 207758 | 40 | 0.836 | 0.272 | 0.564 | 0.000537 | 0.11 | -7.127 | 0.0424 | 130.005 | 4 |
| 3 | Confident / Feeling Cool | Enjoy Your Life | Lady Donli | nigerian pop | 2019 | 175135 | 14 | 0.894 | 0.798 | 0.611 | 0.000187 | 0.0964 | -4.961 | 0.113 | 111.087 | 4 |
| 4 | wanted you | rare. | Odunsi (The Engine) | afropop | 2018 | 152049 | 25 | 0.702 | 0.116 | 0.833 | 0.91 | 0.348 | -6.044 | 0.0447 | 105.115 | 4 |
其中 length 单位是毫秒,popularity 是 Spotify 给出的 0–100 热度分,danceability、energy 等音频特征均为 0–1 区间的连续值,loudness 为分贝(可为负),tempo 是 BPM。
三步数据体检:info、isnull、describe
课程要求按固定顺序做三项完整性检查。第一步 df.info() 确认行列结构与类型:
df.info()
预期输出(530 行 × 16 列,float64 占 8 列、int64 占 4 列、object 占 4 列):
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 530 entries, 0 to 529
Data columns (total 16 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 name 530 non-null object
1 album 530 non-null object
2 artist 530 non-null object
3 artist_top_genre 530 non-null object
4 release_date 530 non-null int64
5 length 530 non-null int64
6 popularity 530 non-null int64
7 danceability 530 non-null float64
8 acousticness 530 non-null float64
9 energy 530 non-null float64
10 instrumentalness 530 non-null float64
11 liveness 530 non-null float64
12 loudness 530 non-null float64
13 speechiness 530 non-null float64
14 tempo 530 non-null float64
15 time_signature 530 non-null int64
dtypes: float64(8), int64(4), object(4)
memory usage: 66.4+ KB
第二步 df.isnull().sum() 确认每列空值数均为 0(课程原文结论:"Looking good"):
df.isnull().sum()
name 0
album 0
artist 0
artist_top_genre 0
release_date 0
length 0
popularity 0
danceability 0
acousticness 0
energy 0
instrumentalness 0
liveness 0
loudness 0
speechiness 0
tempo 0
time_signature 0
dtype: int64
第三步 df.describe() 给出 12 个数值列的统计画像,是后续"挑哪些列做散点图"的直接依据:
df.describe()
| release_date | length | popularity | danceability | acousticness | energy | instrumentalness | liveness | loudness | speechiness | tempo | time_signature | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 530 | 530 | 530 | 530 | 530 | 530 | 530 | 530 | 530 | 530 | 530 | 530 |
| mean | 2015.390566 | 222298.1698 | 17.507547 | 0.741619 | 0.265412 | 0.760623 | 0.016305 | 0.147308 | -4.953011 | 0.130748 | 116.487864 | 3.986792 |
| std | 3.131688 | 39696.82226 | 18.992212 | 0.117522 | 0.208342 | 0.148533 | 0.090321 | 0.123588 | 2.464186 | 0.092939 | 23.518601 | 0.333701 |
| min | 1998 | 89488 | 0 | 0.255 | 0.000665 | 0.111 | 0 | 0.0283 | -19.362 | 0.0278 | 61.695 | 3 |
| 25% | 2014 | 199305 | 0 | 0.681 | 0.089525 | 0.669 | 0 | 0.07565 | -6.29875 | 0.0591 | 102.96125 | 4 |
| 50% | 2016 | 218509 | 13 | 0.761 | 0.2205 | 0.7845 | 0.000004 | 0.1035 | -4.5585 | 0.09795 | 112.7145 | 4 |
| 75% | 2017 | 242098.5 | 31 | 0.8295 | 0.403 | 0.87575 | 0.000234 | 0.164 | -3.331 | 0.177 | 125.03925 | 4 |
| max | 2020 | 511738 | 73 | 0.966 | 0.954 | 0.995 | 0.91 | 0.811 | 0.582 | 0.514 | 206.007 | 5 |
这里有一个值得注意的细节:popularity 的 25% 分位数为 0,意味着约四分之一的歌曲没有任何热度排名。课程的处理策略是把这类记录视为噪声,后面统一过滤。
课程的思考题:既然聚类是无监督方法、不需要标签,为什么数据里还带着
artist、artist_top_genre这些"标签"?答案是——数据探索阶段它们非常有用,但聚类算法本身并不依赖它们;你完全可以删掉表头、只用列号引用数据。
用柱状图确定分析范围:流派过滤与噪声剔除
第一步可视化是"哪个流派最多"。课程代码如下:
import seaborn as sns
top = df['artist_top_genre'].value_counts()
plt.figure(figsize=(10,7))
sns.barplot(x=top[:5].index,y=top[:5].values)
plt.xticks(rotation=45)
plt.title('Top genres',color = 'blue')
柱状图显示存在一个 Missing 流派——这是 Spotify 未能分类的记录。课程的处理是直接过滤掉,然后重新画全流派柱状图:
df = df[df['artist_top_genre'] != 'Missing']
top = df['artist_top_genre'].value_counts()
plt.figure(figsize=(10,7))
sns.barplot(x=top.index,y=top.values)
plt.xticks(rotation=45)
plt.title('Top genres',color = 'blue')
过滤后结论很清晰:afro dancehall、afropop、nigerian pop 三大流派占据绝大多数样本。课程据此把数据集收敛到这三大流派,同时剔除 popularity == 0(无热度排名、视为噪声)的记录:
df = df[(df['artist_top_genre'] == 'afro dancehall') | (df['artist_top_genre'] == 'afropop') | (df['artist_top_genre'] == 'nigerian pop')]
df = df[(df['popularity'] > 0)]
top = df['artist_top_genre'].value_counts()
plt.figure(figsize=(10,7))
sns.barplot(x=top.index,y=top.values)
plt.xticks(rotation=45)
plt.title('Top genres',color = 'blue')
这一步的意义在于"降维前的定界":把分析锁定在数据量足够、语义一致的子集上,后续的散点图与聚类才不会被长尾流派稀释。
相关系数热力图:弱相关数据对聚类意味着什么
定界之后,课程用一个快速测试检查数据中是否存在强线性相关:
corrmat = df.corr(numeric_only=True)
f, ax = plt.subplots(figsize=(12, 9))
sns.heatmap(corrmat, vmax=.8, square=True)
课程对热力图的判读是:唯一强的相关是 energy 与 loudness 之间——音乐响往往能量高,这并不意外;除此之外各特征间相关性都相当弱。这个结论直接影响下一步:如果特征间几乎没有线性结构,"哪个特征能预测哪个"的故事讲不通,数据只能交给聚类算法去发现组内结构——而弱相关、松散对齐的数据恰恰是聚类难度最高的情形。课程同时提示:相关不等于因果,这里只有相关性的证据。
KDE 联合分布:三大流派真的不同吗
课程的第二个探索练习(Exercise - data distribution)要回答的问题是:这三大流派在"流行度 × 舞动性"的感知上是否显著不同?使用的是 Seaborn 的 jointplot,kind="kde" 会叠加核密度估计(KDE,Kernel Density Estimate)曲面,用连续概率密度曲线来刻画分布,便于在多个分布并存时做解读:
sns.set_theme(style="ticks")
g = sns.jointplot(
data=df,
x="popularity", y="danceability", hue="artist_top_genre",
kind="kde",
)
图中可以看到三个流派都绕着一个共同的收敛点画出一圈圈等高线——三大流派在流行度与舞动性上只是"松散对齐",而非各自占据不同的角落。课程的判断是:在这种松散对齐的数据上确定簇,将是一个挑战。
FacetGrid 散点图:聚类的"侦察兵"
KDE 图给出的是密度视图,作业要求的武器则是散点图本身。课程用 FacetGrid 在同一坐标轴上按流派着色绘制:
sns.FacetGrid(df, hue="artist_top_genre", height=5) \
.map(plt.scatter, "popularity", "danceability") \
.add_legend()
同一坐标轴上的散点图呈现出与 KDE 图一致的收敛模式:所有流派向同一区域聚拢,而不是分成三个可分离的团块。课程在此点明本课的方法论主旨:
对聚类而言,散点图可以展示数据的簇结构,因此熟练掌握这类可视化非常有价值。 下一课将把这份过滤后的数据交给 K-Means 聚类,去发现数据中那些以有趣方式相互重叠的群体。
这句话也解释了为什么"散点图"是散点图库(matplotlib 的 scatter/plot、Seaborn 的 scatterplot/FacetGrid、以及其他第三方绘图库)值得系统调研的原因——它是聚类前侦察的主力图形,也是聚类结果复核的主力图形。
作业要求与评分标准(完整继承)
assignment.md 布置了一项研究性任务,核心指令是:
本节课你使用了几种可视化技术来把握"为聚类而绘图"的方法。散点图对发现对象群体尤其有用。请研究创建散点图的不同方式与不同绘图库,并在一个 notebook 中记录你的工作。数据可以用本课的、其他课的、或你自己来源的数据(但须在 notebook 中注明出处)。用散点图绘制一些数据,并解释你的发现。
作业附带的评分标准(Rubric)完整如下,可作为自查清单:
| 评价维度 | 优秀(Exemplary) | 合格(Adequate) | 需改进(Needs Improvement) |
|---|---|---|---|
| (评分项) | 提交一个包含五张注释完善散点图的 notebook | 散点图少于五张,且注释不够完善 | 提交的 notebook 不完整 |
对照本课内容,一份"优秀"档作业可以按下面这张图形式谱来组织(全部有课程与仓库数据支撑,可直接落进 notebook.ipynb):
| 图形 | 代码要点 | 在聚类工作流中的角色 |
|---|---|---|
| 流派柱状图(过滤前) | sns.barplot + value_counts() |
识别长尾与 Missing 类别,界定分析范围 |
| 流派柱状图(过滤后) | 过滤 Missing、三大流派、popularity > 0 后重绘 |
剔除噪声,确认聚类子集 |
| 相关系数热力图 | df.corr(numeric_only=True) + sns.heatmap |
评估特征线性结构,预判聚类难度 |
| KDE 联合分布 | sns.jointplot(..., kind="kde") |
用连续密度曲线观察多分布重叠 |
| FacetGrid 散点图 | sns.FacetGrid + plt.scatter |
直接侦察簇的收敛/分离形态 |
若想让散点图更"可解释",可参考课程提示:换用 energy、loudness、speechiness 等列或更换流派组合,并结合 df.describe() 中各列的 spread 判断哪些维度值得上图。另外,作业允许的"其他课数据"在本仓库中现成可用,例如 2-Regression/data/US-pumpkins.csv、4-Classification/data/cuisines.csv、7-TimeSeries/data/energy.csv,均可复用上述散点图模板。
本课挑战与通向 K-Means 的路径
课程末尾的 🚀Challenge 为下一课做铺垫:
为下一节课做准备:把你可能在生产环境中发现并使用的各种聚类算法画成一张图。这些聚类分别试图解决什么问题?
结合本课的算法速览表,这张图可以直接按"簇数已知/未知、密度均匀/不均、几何平坦/非平坦、是否含异常值"四个维度对 K-Means、DBSCAN、OPTICS、Mean-shift、Gaussian mixtures、BIRCH 等方法分箱。
至此,本课的可视化工作已经为 2-K-Means 一课铺平了道路:带着三大流派、popularity > 0 的干净子集,以及对"数据松散对齐、相关弱"的预判进入 K-Means,你才能判断它给出的簇究竟是数据结构的反映,还是对噪声的强行拟合。若需要其他语言的完整解答,可参考 solution/notebook.ipynb 及同目录下的 Julia、R 版本;想检验学习效果,仓库根目录提供了配套的测验应用 quiz-app。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00





