作家:Jacob Inglesanji
图片由 DALL-E 生成
前言: 本文使用的数据基于学问分享许可条约提供。天然我们依然尽最大勤劳确保信息的准确性和圆善性,但我们并不保证其圆善性或可靠性。数据的使用苦守各自许可条约的条件和条件,任何第三方使用也应该苦守原许可要求。提议读者详备稽查与数据集连接的具体学问分享许可条约,以了解允许的使用口头、修改功令以及签字要求。
本文的技俩是一个随着作念的技俩,演示如安在不依赖机器学习库的情况下,用 Python 构建你我方的 K-Means 对象
迎接来到我 DIY AI & ML 系列的第三篇著作!此次,我们将用 Python 搭建一个 K-Means 对象。K-Means 是我最可爱的机器学习算法之一,因为它能帮我们发现数据中那些看不见的模式。履行得好的话,它能通过严实的数学逻辑,展示出数据中很特酷好酷好的分组或聚类。这在履行天下中能带来好多很蛮横的应用。比如说,假定你被要求分析一个电商网站的点击流数据。你就可以哄骗 K-Means 把客户左证他们点击的东西、加到购物车的东西、或者购买的东西,辞别红特定的群体。这可以匡助制定个性化政策,让你左证客户所属的群体,量身定制网站体验。好啦,我们目下证实插足算法的内容。
K-Means 算法起首我得提一下,K-Means 算法是个无监督的机器学习算法。无监督机器学习模子的症结特色是:数据中莫得指标值或者标签。换句话说,我们不是要斟酌什么,而是念念给数据打上标签。在 K-Means 算法里,我们的指标是把数据分红不同的组或者簇。那这是怎么作念到的呢?起首,用户有“解放”去指定要把数据分红若干个簇。我这里加上引号,是因为信托簇的数目时,有些最好实践要洽商。太少可能会遗漏好多对你的用例有价值的信息;太多的话,可能就变得冗余了。至于怎么分派簇,每个数据点会被分到离它最近的阿谁簇。底下还有更多细节,我们接着讲。
症结想法:欧几里得距离
图片来自 Pythagorean Theorem
念念念念你早期数学课的内容吧。你可能还牢记毕达哥拉斯定理:a² + b² = c²我们用这个公式来算直角三角形斜边(也即是我那时叫的“最长边”)的长度。要获取 c 的准确数值,得对 a² + b² 开方。也许你没坚韧到,其实贯通了这个公式,你也就懂了怎么在坐标平面上研究两点之间的距离!这亦然线性代数中的一个想法,叫 L2 范数。比如说,假定你有一个二维坐标平面,x 是横轴,y 是纵轴。给定两点 (x1,y1) 和 (x2,y2),我们可以用这个公式研究它们之间的欧几里得距离:
图片由作家提供sanji
我们也可以在多于二维的空间里研究欧几里得距离。比如说,在三维空间中找两点之间距离的公式是:
图片由作家提供
那这和 K-Means 有什么相干?我们立地就讲。
质心:运行化、迭代、敛迹这部分,我们先拿个毛糙例子来显露 K-Means 怎么运作。底下这张图,是我纰谬念念的一个二维数据集。如若把这些数据点画出来,八成能一眼看出分组,不太需要用 K-Means 来分。不外为了演示,我照旧来走一遍经由。
图片由作家提供
像我前边说的,我们可以指定分红若干个簇。这个例子里,我们分红 3 个簇。起首,偶而生成三个点,手脚率先的质心。质心即是代表一个簇中心的坐标。我们用星形标记这些偶而质心:
图片由作家提供
接着,我们要给每个数据点分派质心,也即是看它离哪个质心最近,用欧几里得距离来研究。换句话说,每个数据点被分派到离它最近的质心。看到欧几里得距离在 K-Means 里起到的症结作用了吗?
图片由作家提供
运行化之后,我们要研究每个簇里统统点的均值,获取新的质心坐标。比如说,在一个簇里,把统统点的 x 和 y 坐标分别取平均数,这个新的坐标就成了新的质心或者簇中心。遵守八成是这样(看重,红色簇的质心恰好落在一个数据点上,因为阿谁簇唯独这一个点):
图片由作家提供
这些簇看着是不是不太对?不首要,我们还没齐全。要一直相通这个过程,直到算法舒服特定条件敛迹。待会儿详备说。再来一次迭代吧。起首,偶而运行化三个质心:
图片由作家提供
然后,按照欧几里得距离,把数据点分派给最近的质心。
终末,再行研究每个簇的新质心:
图片由作家提供
肉眼看,此次 K-Means 算法依然差未几把数据分好了。不外它我方可不知谈我方依然敛迹了。是以我们还得继续作念雷同的法子,何况查抄新旧质心的变化。如若新旧质心之间的欧几里得距离互异低于设定的容差(比如 0.0001 这样小的值),我们就可以住手算法,说它敛迹了。是不是嗅觉为什么我一脱手要先讲欧几里得距离了?来看敛迹的过程吧。照老功令,先偶而生成质心:
图片由作家提供
给数据点分派最近的质心:
终末,再行研究每个簇的新质心,同期查抄新旧质心的距离是否低于容差。此次,距离是 0,显露确乎达到了我们的法式,算法敛迹了:
图片由作家提供
找到最好簇数目目下,给定 k 个簇,我们依然知谈怎么把数据正确分红 k 个簇了。不外,还有个问题:最好的 k 是若干呢?有几种步调可以用,在我们要构建的 KMeans 对象里,我们会找出能获取最高综合统统(silhouette score)的 k。
图片由作家提供
a 是每个数据点到它处所簇质心的平均距离,b 是每个簇质心到最隔邻簇质心的平均距离。更庸碌点说,综合统统越高,显露一个簇里面的数据点互相之间越围聚、越长入,同期不同簇之间又分得越开。
KMeans 类目下我们依然讲收场贯通 KMeans 算法所需要的统统想法!是时刻来构建我们的对象了。先从运行化函数脱手。可以看到,我们只会用到 numpy、pandas、tqdm(用来暴露进程条),以及 scikit-learn 里的 silhouette_score(省心,就只用 sklearn 里的这一个功能,我们确实会从零构建算法)。底下说说各个属性:
import numpy as np
import pandas as pd
from sklearn.metrics import silhouette_score
from tqdm import tqdm
class DIYKMeans:
def __init__(self, max_k=10, max_iters=100, tol=1e-4, random_state=None):
self.max_k = max_k
self.max_iters = max_iters
self.tol = tol
self.random_state = random_state
self.k = None
self.centroids = None
self.labels_ = None
self.X_scaled = None
self.feature_means = None
self.feature_stds = None
self.stable_iterations = 0
• max_k:模子要尝试拟合的最大簇数目。• max_iters:拟合模子时,偶而运行化质心的最大次数。• tol:研究新旧质心距离时的容差阈值。• random_state:偶而数种子,保证可复现。• k、centroids、labels_:找到最好 k 后,会赋值这些属性。k 是最好簇数目,centroids 是最好 k 的质心坐标,labels_ 是每个数据点的簇标签。• X_scaled、feature_means、feature_stds:数据的法式化值,以及各特征的均值和法式差。• stable_iterations:纪录在容差畛域内平稳迭代次数的占位符。默许要相接 3 次平稳后,才认为算法敛迹。
质心操作 & 法式化接下来这一组步调,即是 K-Means 算法的基本操作啦。看起来应该很眼熟吧?毕竟我们之前依然讲过了。看重,我们还加了一个数据法式化的步调。为什么迫切呢?因为,数据得在合并表率上,否则表率大的特征会让算法偏向它们。比如说,我们念念按卧室数、浴室数和房价来聚类房屋。卧室和浴室可能就在 1-5 个房间之间,但价钱可能是几十万。如若不法式化,距离研究时价钱特征就会皆备主导聚类。为了幸免这种问题,我们要用法式化,这里用的是均值归一化。另外,看重我的敛迹步调也作念了改造。平时算法只消在一次迭代中舒服容差就算敛迹了,我以为不太稳,是以开采成相接 3 次舒服容差才算确实敛迹。
• standardize_data:把每个特征法式化到合并表率。• initialize_centroids:给定数据集特征数和 k,偶而生成 k 个质心。• compute_distances:用 numpy 的线性代数库研究欧几里得距离,也即是两个坐标之间酿成向量的模。• assign_clusters:给定一个数据点和质心,复返最近的质心索引。• compute_new_centroids:左证每个簇的点,分别取各特征的均值,复返新的质心坐标。• has_converged:给定容差值,查抄新旧质心是否在容差畛域内。相接 3 次舒服,才判定敛迹。
def standardize_data(self, X):
self.feature_means = np.mean(X, axis=0)
self.feature_stds = np.std(X, axis=0)
self.feature_stds[self.feature_stds == 0] = 1
return (X - self.feature_means) / self.feature_stds
def initialize_centroids(self, X, k):
np.random.seed(self.random_state)
return X[np.random.choice(X.shape[0], k, replace=False)]
def compute_distances(self, X, centroids):
return np.linalg.norm(X[:, np.newaxis] - centroids, axis=2)
def assign_clusters(self, distances):
return np.argmin(distances, axis=1)
def compute_new_centroids(self, X, labels, k):
return np.array([X[labels == i].mean(axis=0) for i in range(k)])
def has_converged(self, old_centroids, new_centroids, stable_threshold=3):
if np.linalg.norm(new_centroids - old_centroids) < self.tol:
self.stable_iterations += 1
else:
self.stable_iterations = 0
return self.stable_iterations >= stable_threshold
Fit 步调这是统统法子确切和会到沿路的步调。给定我们开采的 max_k 属性,我们会尝试统统在这个畛域内的 k 值。关于每一个 k,我们都会跑一遍迭代经由:偶而运行化质心、分派数据点、再行研究质心,一直到敛迹为止。同期,我们也司帐算每一组聚类的综合统统(silhouette score)。哪个 k 的综合统统最高,就选哪个手脚最终的簇数目。之后,我们就把 k、centroids 和 labels_ 属性赋值成最好的遵守啦。
def fit(self, X):
X = np.array(X)
X = self.standardize_data(X)
self.X_scaled = X
best_k = None
best_score = -1
best_labels = None
best_centroids = None
print("Fitting KMeans across different k values:")
for k in tqdm(range(2, self.max_k + 1), desc="Searching for optimal k"):
centroids = self.initialize_centroids(X, k)
self.stable_iterations = 0
for _ in range(self.max_iters):
distances = self.compute_distances(X, centroids)
labels = self.assign_clusters(distances)
new_centroids = self.compute_new_centroids(X, labels, k)
if self.has_converged(centroids, new_centroids):
break
centroids = new_centroids
score = silhouette_score(X, labels)
if score > best_score:
best_k = k
best_score = score
best_labels = labels
best_centroids = centroids
self.k = best_k
self.centroids = best_centroids
self.labels_ = best_labels
用信用卡数据举个例子
像片来自 Avery Evans,发布在 Unsplash
让我们用 Kaggle 上的一个信用卡数据集来测试我们的 DIY KMeans 对象。假定你在一乡信用卡公司当数据科学家,被分派到一个任务:要识别客户群体。之前,业务团队平时是左证客户的蹂躏类别和平均月余额来辞别客户群的。目下相易念念要膨大这套逻辑,但又不知谈怎么作念到范畴化。这时刻,我们我方造的 KMeans 对象就派上用场啦。先导入数据:
data = pd.read_csv('CC GENERAL.csv')
品色堂免费论坛我们有好多特征可以用。不外,为了让业务部门能闲散妥贴新的步调,我们只保留以下这些列。看重,这些界说都是凯旋从 Kaggle 页面拿下来的:
BALANCE_FREQUENCY:账户余额更新的频率,分数在 0 到 1 之间(1 = 平时更新,0 = 很少更新)
PURCHASES_FREQUENCY:购物的频率,分数在 0 到 1 之间(1 = 平时购物,0 = 很少购物)
CREDIT_LIMIT:用户的信用卡额度
PAYMENTS:用户的支付金额
PRCFULLPAYMENT:用户支付全额的百分比
TENURE:用户握卡时刻
cols_to_keep = ['BALANCE_FREQUENCY','PURCHASES_FREQUENCY','CREDIT_LIMIT','PAYMENTS','PRC_FULL_PAYMENT','TENURE']
data_for_kmeans = data[cols_to_keep]
data_for_kmeans = data_for_kmeans.dropna()
快速作念个探索性分析,望望这些特征的基本统计信息。这里有几个症结事实:
我们有八成 8600 个圆善数据的客户样本
购物频繁与不频繁的客户八成是 50/50 各占一半
大大批客户的信用额度低于 6500 好意思元
大大批客户支付金额在 2000 好意思元以内,但最高支付到了 50000 好意思元
大大批客户只支付了最多 15% 的账单
大大批客户的握卡时刻是 12 年,险些没什么波动
data_for_kmeans.describe()
图片由作家提供
拟合我们好处的 KMeans 模子目下对数据有了个大要了解,我们来拟合我们的模子吧!
km = DIYKMeans(max_k=20)
km.fit(X=data_for_kmeans)
图片由作家提供
print('optimal number of clusters: ', km.k)
图片由作家提供
贯通聚类的含义
像片来自 Shlomo Shalev,发布在 Unsplash
太好了!我们好处的 KMeans 对象把数据分红了 7 个簇。但职责远没齐全。既然我们依然通过数学步调把客户群分开了,接下来就要探索每个群体里荫藏的症结模式了。为了作念这件事,我我方又写了一个小用具函数,它能输出每个簇的东谈主数、每个特征的均值,还配上热力争,肤浅对比分析。我在数据长入加了一个叫 'Cluster' 的列,来源是我们 KMeans 对象的 labels_ 属性。以下是我不雅察到的一些症结模式:
除了第 3 组,其他每一组的客户握卡时刻基本都是 12 年。
第 2 组是独逐一个高购物频率、高支付比例的群体,信用额度略高于平均水平。
第 0、4、5 组的支付比例都很低;但唯独第 4 组的购物频率也很低。
第 6 组是高蹂躏群体,信用额度最高,支付金额最大。
第 5 组在蹂躏水平上和第 6 组接近,但支付金额彰着低好多。
左证这些模式,有哪些内容的政策或者决策可以推出来呢?记着,信用卡公司收获,靠的是收手续费和利息。是以,从公司的角度,最好是客户不要一次性全额付清账单,这样能赚点利息,但又不可付太少,否则风险就大了。这里有几个我的小念念法:
念念目标让第 5 组的客户支付比例向第 6 组围聚;
洽商给第 2 组客户齐全普及信用额度,因为他们支付才略可以;
对第 0、4、5 组客户,洽商合适裁汰信用额度,直到他们的支付比例普及。
data_for_kmeans['Cluster'] = km.labels_
def summarize_by_cluster_with_heatmap(df, cluster_col='Cluster'):
if cluster_col not in df.columns:
raise ValueError(f"Column '{cluster_col}' not found in DataFrame.")
value_cols = [col for col in df.select_dtypes(include='number').columns if col != cluster_col]
if not value_cols:
raise ValueError("No numeric columns to summarize (other than the cluster column).")
grouped = df.groupby(cluster_col)
means = grouped[value_cols].mean().round(2).add_suffix('_mean')
counts = grouped.size().to_frame('count')
summary = pd.concat([counts, means], axis=1).reset_index()
return summary.style.background_gradient(cmap='YlGnBu', subset=summary.columns[1:]).format("{:.2f}")
summarize_by_cluster_with_heatmap(df=data_for_kmeans)
图片由作家提供