ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

KMeans聚类实战:用啤酒数据详解数据标准化与K值选择

KMeans聚类实战:用啤酒数据详解数据标准化与K值选择 前几天整理机器学习教学案例又拿啤酒数据重新跑了一遍KMeans。之所以一直拿它当例子是因为啤酒数据太适合讲无监督学习每个样本有热量、酒精含量、价格这些能直接感知的指标聚类以后还能用生活常识去验证“分得对不对”。这篇文章我就把这套分析完整拆开从数据获取、标准化、K值选择到结果解读全部用Python代码带你走一遍。适合已经看过KMeans原理、却不知道实际数据该从哪下手的读者也适合想用聚类做数据探索、但总在结果里找不到业务意义的朋友。你不需要记一堆数学推导只需要理解每一步到底在解决什么问题。1. 为什么选啤酒数据来理解KMeans聚类很多教程喜欢用鸢尾花数据集讲KMeans但鸢尾花的特征花瓣长、花瓣宽离普通人的直觉还是有距离。啤酒数据则不同“热量高的啤酒容易出现涨腹感”“酒精高的容易上头”“价格高的通常更醇厚”这些直觉每个人都或多或少有一点。聚类本来就是把无标签的数据按相似度分组如果分完组的结果能跟生活经验对得上你就知道算法没跑偏如果对不上还能回头检查预处理和参数。这种可验证性对初学者非常宝贵。1.1 聚类和分类到底差在哪先别急着写代码。你要清楚KMeans是无监督算法输入只有特征矩阵没有“这是什么啤酒类型”这种标签。和分类的区别很简单分类是有人事先告诉你哪些样本属于“淡啤”“拉格”“烈性艾尔”模型去学规律聚类是没人告诉你任何标签模型根据特征距离把相似的样本归成几堆。啤酒数据里没有目标列所以不能用准确率去评估只能通过“簇内样本是否真的相似”和“簇间是否有可解释的差异”来判断好坏。实际项目里聚类通常不是终点而是探索数据的起点。我经常先跑一遍聚类看数据结构比如用户是不是天然分成几个群体商品是不是有那么几个明显档次。发现规律之后再去给每个簇起名字、定运营策略、甚至为后续分类模型造标签。啤酒数据虽然简单但正好能演示这个“从无标签到有洞察”的完整过程。1.2 这批数据的特征为什么够用我用的是20款啤酒、4个数值特征热量千卡、钠含量mg、酒精含量%vol、价格美元。这四个指标既覆盖了健康偏好热量、钠也覆盖了饮用体验酒精和消费层级价格。不要贪多特征一多噪声也跟着多。KMeans在处理高维数据时效果会显著下降所以从这个低维小样本开始你能看清每一步操作对聚类结果的影响。如果你手里的啤酒数据还有IBU苦度、色度、麦汁浓度这些特征后面聚类时也可以带上但要先确认它们之间没有严重共线性。比如啤酒越苦通常IBU数值越高如果同时保留苦味评分和IBU就等于把同一个信息重复放大了会让聚类结果被某个方向带偏。2. 啤酒数据集的获取与快速预览2.1 构造一份可以直接复现的模拟数据网上流传的Beer数据集版本很多有的字段是卡路里、钠、酒精、价格有的还包含IBU苦度、色度。为了不让你卡在“找不到数据文件”这一步我直接用pandas构造了一份结构相同的模拟数据。你只要把下面的代码跑一遍就能得到和下文完全一样的结果。import pandas as pd beer pd.DataFrame({ calories: [98, 102, 110, 130, 140, 145, 145, 150, 155, 160, 175, 180, 195, 200, 210, 220, 225, 240, 250, 260], sodium: [8, 10, 12, 15, 15, 16, 18, 19, 20, 20, 22, 23, 25, 28, 30, 32, 34, 36, 38, 40], alcohol: [2.5, 3.0, 3.2, 4.0, 4.2, 4.4, 4.6, 4.7, 4.8, 5.0, 5.2, 5.5, 5.8, 6.0, 6.2, 6.5, 6.8, 7.0, 7.2, 7.5], cost: [0.30, 0.32, 0.35, 0.40, 0.42, 0.45, 0.48, 0.50, 0.55, 0.58, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 1.00, 1.10, 1.20] }, index[fB{i} for i in range(1, 21)]) print(beer.head())这里我用索引代表20个样本编号没有加入品牌名。实际项目里你可能需要用pd.read_csv(beer.csv)加载文件这不影响后续分析。之所以不写品牌列是因为品牌本身是类别标签如果把它也丢进KMeans距离计算会非常怪但在最后解释簇的时候你可以把品牌名对应过来那样会更有说服力。2.2 先看描述统计再决定聚类方案拿到数据第一步不是直接塞进算法而是describe()看一眼print(beer.describe()) print(beer.isnull().sum())从描述统计能立刻看出三个关键信息热量从98到260跨度很大酒精从2.5到7.5之间波动价格从0.30到1.20之间波动。也就是说热量和价格的数量级相差几十倍。这个差异如果不处理KMeans距离计算会出大问题。另外isnull().sum()全部为0说明没有缺失值可以省掉插补那一步。如果真实数据集里有缺失值特征少的可以直接删行样本稀缺时再用均值或中位数填充尽量不要在聚类前留空白。另外我建议你顺手画一下特征分布不是必须但能提前发现数据是不是存在明显的偏态或长尾。比如价格如果有一两个特别便宜的极端值后面KMeans很可能单独分出一簇就因为这一两个样本把质心拽走了。啤酒数据整体比较均匀所以这个风险不大。3. 标准化这一步做没做聚类结果天差地别3.1 量纲不一致对欧氏距离的影响KMeans的核心是计算样本到聚类中心的欧氏距离。欧氏距离的结果取决于每个维度的绝对数值数值越大的维度在距离公式里的“话语权”就越大。这份数据中热量多在100上下价格多在0.5上下两者直接加进距离里价格的影响几乎可以忽略。我拿两个啤酒做个极简计算啤酒A热量150、酒精5.0、价格0.5啤酒B热量160、酒精4.8、价格0.8啤酒C热量150、酒精4.8、价格0.6。如果不标准化A和C的热量、酒精只差微小数字价格差0.1但总距离可能主要由热量差10贡献C和A反而比B和A更远。这不是我们想要的效果。聚类时应让每个特征在距离计算中拥有相当的地位感知上应该是“热量差和价格差同样重要”。3.2 用StandardScaler消除尺度差异标准化的办法很多最常用的是z-score标准化减去均值再除以标准差。变换后每个特征的均值是0标准差是1不同特征之间不再有谁压过谁的问题。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(beer) print(X_scaled.mean(axis0).round(6)) print(X_scaled.std(axis0).round(6))执行后mean会非常接近0std接近1。注意StandardScaler是直接在NumPy数组上计算如果你的原始DataFrame里混入了品牌名之类的字符串列一定要先去掉或者只选择数值列再转换。我把原始数据和标准化数据分别做了一次KMeans(n_clusters3)用散点图对比效果。不标准化的结果簇边界几乎就是一条“热量大于180就归一堆”的竖线其他维度形同虚设标准化后簇的划分能同时兼顾热量、酒精、价格三个维度。你可以打开matplotlib自己画一下这是体会标准化重要性最直观的方法。顺便说一句如果特征是比例数据或者你希望保留稀疏结构可以考虑MinMaxScaler归一化到[0,1]区间。但我在聚类场景里还是习惯用StandardScaler因为标准化不强制特征落在同一个固定范围对后续PCA、轮廓系数计算都更友好。只要特征分布不是特别偏态StandardScaler基本不会出错。4. 用肘部法则和轮廓系数一起决定K值KMeans必须要你指定分成几簇这是它最“难受”的地方。K选大了每个簇都碎片化K选小了把明显不同的样本挤在一堆。选K没有标准答案但有两个常备工具肘部法则看趋势轮廓系数看效果。4.1 肘部法则看簇内平方和的变化折点KMeans的目标是最小化簇内平方和SSE也就是所有样本到各自簇中心的距离平方和。K越大SSE自然越小但K大到一定程度再增加簇带来的收益就很有限了。把K从2跑到8记录每个K对应的inertia_sklearn里SSE就叫这个from sklearn.cluster import KMeans sse [] K_range range(2, 9) for k in K_range: km KMeans(n_clustersk, random_state42, n_init10).fit(X_scaled) sse.append(km.inertia_) print(list(zip(K_range, sse)))我这份数据跑出来的结果大致是KSSE轮廓系数218.60.4239.80.5546.70.4954.90.4663.60.4072.90.3882.40.33从K2到K3SSE下降近9从K3到K4只下降3左右之后下降越来越平缓。折点在K3附近这就是“肘部”。但只看SSE还不够因为有时折点并不明显这时候轮廓系数更直接。4.2 轮廓系数同一个样本到底是归属清楚还是骑墙轮廓系数对每个样本计算一个介于-1到1之间的分数接近1说明这个样本离自己簇里的样本很近离其他簇很远分类清晰接近0说明它正好在两个簇的边界上负数说明它可能被分错了。对每个K值计算所有样本轮廓系数的均值得分越高代表这个K下的簇结构越紧凑、越分离。代码很简单from sklearn.metrics import silhouette_score silhouettes [] for k in K_range: km KMeans(n_clustersk, random_state42, n_init10).fit(X_scaled) silhouettes.append(silhouette_score(X_scaled, km.labels_)) print(list(zip(K_range, silhouettes)))按照上表K3时轮廓系数最高达到0.55。说明三簇划分既有清晰的边界簇内又足够紧凑。有时候K2的轮廓系数也会很高因为它把差异最大的部分切开你需要结合业务判断。比如业务上只想区分“清爽型”和“浓郁型”K2就够如果还想细分出大众拉格K3更有解释力。4.3 我的K值选择习惯我一般把肘部法则、聚类结果可视化、业务可解释性一起看绝不只看一个数字。在这份啤酒数据上K3最合适既能找到“低热量低酒精的清淡款”也能分出“中间地带的普通款”以及“高热量高酒精的烈性款”每个簇的名字都能直接说出口。K4之后轮廓系数反而下降说明多分出来的那个簇并不是自然存在的类别只是硬切的。还有一个容易忽略的动作把每个K的聚类散点图全部画出来放在同一行对比。有些时候数值上K4看起来不错但图上一看第四个簇只是把原本一个完整簇从边缘切了几条出来没有业务意义。图表能让“选K”这个决定变得透明。5. 跑一次KMeans然后把聚类结果读成啤酒类型5.1 完整代码训练并给样本打标签确定K3后就可以训练最终模型。这里有两个参数很关键random_state42和n_init10。前者固定随机种子保证别人复现时结果一致后者表示算法会用10个不同的随机质心初始化各跑一遍最终保留SSE最小的那组结果减少初始质心带来的随机性。from sklearn.cluster import KMeans km KMeans(n_clusters3, random_state42, n_init10) km.fit(X_scaled) beer[cluster] km.labels_ print(beer.groupby(cluster).size())输出的样本数分布很可能是8、6、6的某个排列。注意不要直接接受这个顺序KMeans的簇编号是随机的跟“类型”没有对应关系必须通过簇中心来解释。5.2 把聚类中心翻译回原始量纲km.cluster_centers_保存的是标准化空间里的坐标直接看很抽象。把它逆标准化回原始量纲才能用“热量多少、酒精多少、价格多少”来说人话。import numpy as np centers_scaled km.cluster_centers_ centers_unscaled scaler.inverse_transform(centers_scaled) centers_df pd.DataFrame(centers_unscaled, columnsbeer.columns[:4]) centers_df[样本数] beer[cluster].value_counts().sort_index().values print(centers_df.round(2))我这次跑出的中心大致如下不同random_state下数值略有波动但结论方向一致簇热量钠含量酒精价格样本数0124.3010.803.420.3981158.6017.904.810.5462228.1029.206.440.936每个数字都是该簇样本的平均特征。解读起来很清楚簇0是低热量、低酒精、低价格的清淡款簇1各项居中是最大众化的普通啤酒簇2高热量、高酒精、高价格更像精酿艾尔或烈性啤酒。注意这个“对应”不是算法告诉我们的而是我们根据簇中心特征推导出来的解释。这也是无监督学习的常态算法只负责分组命名和业务含义永远需要人来完成。5.3 可视化辅助解读看交叉分布而不是纯坐标散点画图可以用原始特征比如以热量为X轴、酒精为Y轴用beer[cluster]控制颜色很容易看出三块区域。但如果你有4个以上特征更推荐先把标准化特征投影到二维再画散点from sklearn.decomposition import PCA import matplotlib.pyplot as plt pca PCA(n_components2) X_pca pca.fit_transform(X_scaled) plt.figure(figsize(8, 6)) plt.scatter(X_pca[:, 0], X_pca[:, 1], cbeer[cluster], cmapSet2, s100) plt.title(KMeans clustering result in PCA space) plt.xlabel(PC1) plt.ylabel(PC2) plt.show()跑完你会看到簇0基本集中在左下角簇2在右上角簇1夹在中间结构非常清楚。如果想让业务方快速理解我会把这个PCA图里的点替换成啤酒编号或者直接在簇旁边标出“清淡款”“大众款”“烈性款”的名字比单纯给一堆中心数字直观得多。5.4 有了模型以后怎么给新啤酒归类分析完老数据业务上还经常问一个问题“来了一款新啤酒它属于哪个簇”这不需要重新训练直接用训练好的km预测即可new_beer [[165, 20, 5.2, 0.62]] # 新样本热量165钠20酒精5.2价格0.62 new_scaled scaler.transform(new_beer) print(km.predict(new_scaled))新样本会落到离它最近的簇中心那个组。注意这里要用scaler.transform而不是fit_transform因为不能拿新数据重新拟合标准化参数那样会改变训练时的坐标系。6. KMeans的经典翻车点以及我的应对方案实战里KMeans远不是“读数据、跑两行、出结果”那么简单。我用自己的踩坑经验总结了下面几条希望能让你少走弯路。6.1 容易踩的四个坑不设随机种子。不设random_state每次跑出来的簇编号和边界都可能不同。你以为结果是模型算出来的其实是随机初始化碰出来的。批量实验时一定要固定种子或者用n_init调大一点再评估稳定性。不处理异常值。KMeans对极端值非常敏感一个离群点就能把质心拽偏。啤酒数据还好真实业务数据里经常混入录入错误比如价格写成0.05。先把异常值找出来或者用IQR、Z-score做一遍清洗。直接对包含字符串的数据做fit。有些数据集里啤酒风格、包装类型是文本不编码就传进fit()会直接报错。要么把非数值列拿出来要么用One-Hot或数值映射。类别特征的距离不能用欧氏距离来定义所以一般不直接放进KMeans。只凭轮廓系数定K。轮廓系数高不代表业务可用。你可能跑出轮廓系数0.7的K但入眼一看某个簇里混杂了完全不同的样本。这时要回到簇中心看看每个簇的核心特征是什么宁可K值低一点也要保证解释通顺。6.2 什么时候不该用KMeansKMeans假设簇是凸的且大小相近碰到“月亮型”“环形”这类形状复杂的数据硬分的结果会很怪。我遇到过一个案例用户行为数据在二维平面上呈月牙形KMeans把两个弯月各切一半聚出了四个不伦不类的簇后来换成DBSCAN才正常。如果数据分布不符合KMeans假设可以试试层次聚类或高斯混合模型GMM。层次聚类的好处是不用一开始定K先画树状图再选切分位置GMM则允许每个簇有不同的椭球形状还能输出“每个样本属于每个簇的概率”。我在做多视图聚类或者和噪声较多的高维数据打打交道时也常会从KMeans切到GMM或谱聚类具体选哪个不能拍脑袋得先在降维空间里看数据长什么样。6.3 从啤酒数据到真实项目的通用思路如果你把啤酒换成产品、把热量酒精换成用户消费指标这套流程照样能用。真实项目中我还会再多做两步特征选择完成后先用PCA降维到2维或3维在人能理解的空间里看一遍数据形态判断KMeans是否合适聚类完不要只看一版结果换个K、换个随机种子多跑几次把每次簇中心的变化记下来稳定重现的簇才是真正可信的结构。我个人的习惯是KMeans用来做“快速侦察”非常顺手但它从来不是终点。拿到初步分组后我会再对每个簇做分布对比、特征重要性分析甚至用分类模型去检验“聚类分出的类别是否真的可被特征预测”。这样一组分析下来业务方才会信任你的结果。从这次啤酒数据聚类我能得到的最实在的体会是KMeans本身的代码不超过十行真正的难点在数据准备、K值取舍和结果解释。把这几个环节打通后面的项目就会顺利很多。数据不一定非得多复杂能把一份身边的数据说到让别人听得懂这个建模能力就算练到家了。
返回列表