You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python中如何基于听众年龄分布对离散音乐流派聚类?

方案结论

你不需要直接硬套默认欧氏距离的6维点KMeans聚类,你的数据本质是有序分箱的概率分布数据,用适配分布特性+年龄有序性的自定义距离做聚类,结果会合理很多。100个左右流派的样本量很小,实现起来没有难度。

核心思路

1. 数据预处理

首先把每个流派的听众占比整理成标准格式:

  • 每个流派对应长度为6的一维数组,索引0到5分别对应A1(0-10岁)到A6(61岁及以上)的听众占比
  • 修正你示例里的标签笔误(比如Pop条目里重复标注的A3、Rock条目缺失的占比项),确保每个数组所有元素加和为1,无占比的年龄段填0即可
  • 不要做标准化/归一化操作:所有维度的量纲统一为占比,尺度完全一致,标准化会破坏分布的相对比例关系。

2. 距离度量选择(最影响聚类效果的环节)

不要直接用默认欧氏距离:欧氏距离会把6个年龄段当成完全独立的维度,既不考虑数据是概率分布的特性,也不考虑年龄段的顺序关系——比如「受众集中在A2」和「受众集中在A3」的流派,实际受众年龄差只有10岁,欧氏距离算出来的差异和「受众集中在A2」「受众集中在A6(差50岁)」的差异一样,完全不符合业务逻辑。
推荐按优先级选下面的距离:

  • 第一选择:Wasserstein距离(推土机距离/EMD):专门衡量两个分布之间的差异,且支持自定义分箱之间的距离成本——你可以把相邻年龄段的移动成本设为1,隔一个设为2,以此类推,完全匹配年龄分段有序的特性,算出来的相似性最符合常识。
  • 第二选择:Jensen-Shannon(JS)散度:是KL散度的对称平滑版本,取值范围0-1,专门衡量概率分布的重叠度,0代表分布完全一致,缺点是默认不考虑年龄段的顺序关系,适合你不需要考虑年龄相邻性的场景。
  • 不推荐用余弦距离、曼哈顿距离这类通用距离,适配性都不如上面两个。

3. 聚类算法选择

因为要用自定义距离,不要选原生只支持欧氏距离的KMeans,选下面几个适配小样本+自定义距离的算法即可:

  • 层次聚类(Agglomerative Clustering):最推荐,100个样本跑起来速度极快,支持预计算自定义距离矩阵,还可以画树状图直观查看不同聚类粒度的结果,方便你决定分多少个簇。
  • DBSCAN:不需要提前指定簇数,能自动把受众特征极特殊的离群流派(比如低龄向儿歌、老年向戏曲这类和主流流派受众差异极大的内容)标记为噪声,不会强行归到某个簇里。
  • KMedoids(中心点聚类):如果你需要固定簇的数量,用这个替代KMeans,它的簇中心是实际存在的流派样本而非虚拟均值点,对异常值更鲁棒,完美支持自定义距离。
Python实现代码

首先安装依赖:
pip install numpy scipy scikit-learn matplotlib

完整可运行示例(你把模拟数据替换成自己的真实数据即可):

import numpy as np
from scipy.spatial.distance import pdist
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
import matplotlib.pyplot as plt

# ---------------------- 1. 加载数据,替换成你自己的真实数据即可 ----------------------
# 每行对应一个流派,列依次是A1~A6的听众占比
genre_names = ["Pop", "Rock", "Children's Songs", "Opera", "HipHop", "Classical"]
genre_dist = np.array([
    [0.05, 0.3, 0.35, 0.2, 0.05, 0.05],  # 修正笔误后的Pop分布
    [0.05, 0.2, 0.3, 0.2, 0.15, 0.1],    # 修正补全后的Rock分布
    [0.9, 0.08, 0.02, 0, 0, 0],          # 儿歌示例
    [0, 0.02, 0.03, 0.1, 0.25, 0.6],     # 戏曲示例
    [0.02, 0.7, 0.23, 0.04, 0.01, 0],    # 嘻哈示例
    [0.01, 0.05, 0.1, 0.24, 0.35, 0.25]  # 古典乐示例
])

# ---------------------- 2. 计算自定义距离矩阵 ----------------------
# 方法1:计算Wasserstein距离(推荐),等距年龄分箱的一维EMD可直接用CDF差求和实现
def wasserstein_dist(p, q):
    cdf_p = np.cumsum(p)
    cdf_q = np.cumsum(q)
    return np.sum(np.abs(cdf_p - cdf_q))

dist_matrix = pdist(genre_dist, metric=wasserstein_dist)
# 如果用JS散度可以替换成下面的实现
# def js_dist(p,q):
#     m = (p+q)/2
#     kl_pm = np.sum(p * np.log(p/m + 1e-10))
#     kl_qm = np.sum(q * np.log(q/m + 1e-10))
#     return np.sqrt(0.5*(kl_pm + kl_qm))
# dist_matrix = pdist(genre_dist, metric=js_dist)

# ---------------------- 3. 层次聚类+结果输出 ----------------------
Z = linkage(dist_matrix, method='average')  # 平均链接法,对分布类数据效果稳定

# 画树状图看聚类效果
plt.figure(figsize=(10,6))
dendrogram(Z, labels=genre_names)
plt.title("Genre Clustering Dendrogram by Age Distribution")
plt.show()

# 设定距离阈值切分簇,阈值可以根据树状图的跳变点调整
cluster_labels = fcluster(Z, t=1.0, criterion='distance')
# 输出聚类结果
for label in np.unique(cluster_labels):
    print(f"簇{label}: {[genre_names[i] for i in range(len(genre_names)) if cluster_labels[i]==label]}")
调优提示
  • 聚类的簇数没有固定标准答案,你可以通过树状图观察流派合并的距离差,选距离跳变最大的位置切分即可。
  • 如果你发现聚类结果太散,可以调大切分距离阈值;如果想把特殊流派单独分出去,可以换DBSCAN,调整邻域距离参数和最小样本数即可。
  • 如果你的数据里很多流派某个年龄段占比为0,用JS散度的时候记得加极小值(代码里的1e-10)避免对数计算报错。

内容的提问来源于stack exchange,提问作者user11369844

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 07:27:21