第一次听说K-means这个词时,我还以为是什么高深莫测的黑科技。后来才发现,它其实就是个特别实用的数据分组工具,就像老师给小朋友分小组一样自然。想象你面前有一堆混在一起的彩色积木,红的、蓝的、黄的都有,现在要你把它们按颜色分开——这就是K-means最擅长的事。
在实际工作中,我经常遇到这样的情况:手头有一大堆客户数据,既不知道谁是谁,也不知道该怎么分类。这时候K-means就像个尽职的助手,能自动帮我把相似的用户归到一组。比如去年做的一个电商项目,我们就是用这个算法,把10万用户分成了5个消费群体,结果比人工分类准确多了。
K-means属于无监督学习,这意味着它不需要事先知道答案就能工作。就像给你一筐水果,不用告诉你有苹果、香蕉,它自己就能发现这些自然存在的类别。这种特性让它特别适合探索性数据分析,我经常在项目初期用它来"摸清底细"。
说到距离计算,很多人第一反应就是地图上的直线距离。没错,K-means最常用的欧氏距离就是这个概念的多维版本。我刚开始学的时候,喜欢用奶茶店的例子来理解:假设我们要分析城市里的奶茶店,每家店有两个特征 - 日均客流量和平均单价,这样就可以在二维平面上表示每家店的位置。
计算两家店的距离公式很简单:
python复制distance = √[(客流量1-客流量2)² + (单价1-单价2)²]
这个距离越小,说明两家店的经营状况越相似。在Python里用numpy实现特别方便:
python复制import numpy as np
def euclid_distance(x1, x2):
return np.sqrt(np.sum((x1 - x2)**2))
在实际项目中,我发现有几点特别需要注意。首先是数据标准化,因为不同特征的量纲可能天差地别。比如一个特征是年薪(几万到百万),另一个特征是年龄(20-60),如果不做标准化,年薪会完全主导距离计算。
我常用的标准化方法是Z-score:
python复制from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
另一个常见问题是高维数据的距离失效。当特征维度很高时,所有样本间的距离会变得异常接近。这时可以考虑先用PCA降维,或者改用余弦相似度等其他度量方式。
确定了距离计算方法后,下一步就是把每个样本分配到最近的簇中心。这个过程就像是在城市里找最近的便利店 - 你会比较去全家、7-11和罗森的距离,然后选择最近的那家。
代码实现时,我习惯先用列表推导式计算所有距离:
python复制def nearest_cluster_center(x, centers):
distances = [euclid_distance(x, center) for center in centers]
return np.argmin(distances)
这里有个性能优化的小技巧:如果数据量很大,可以用向量化操作代替循环:
python复制distances = np.sqrt(((x - centers)**2).sum(axis=1))
分配完所有样本后,需要重新计算每个簇的中心点。这个过程就像调整便利店的位置 - 先看看顾客都住在哪里,然后把店开到顾客分布的中心位置。
用numpy计算均值非常方便:
python复制def estimate_centers(X, labels, n_clusters):
centers = np.zeros((n_clusters, X.shape[1]))
for i in range(n_clusters):
centers[i] = X[labels == i].mean(axis=0)
return centers
在实际项目中,我发现空簇是个常见问题。当某个簇没有分配到任何样本时,通常的处理方法是随机重新初始化这个中心,或者把它移到离其他中心最远的位置。
在有真实标签的情况下,我们可以用准确率评估聚类效果:
python复制def acc(y_true, y_pred):
return np.sum(y_true == y_pred) / len(y_true)
但要注意,聚类结果的标签编号可能与真实标签不一致。比如算法可能把类别A标记为1,而真实数据标记为2。这时需要先进行标签对齐:
python复制from sklearn.metrics import confusion_matrix
conf_mat = confusion_matrix(y_true, y_pred)
更多时候我们没有真实标签,这时可以用轮廓系数(Silhouette Score)来评估:
python复制from sklearn.metrics import silhouette_score
score = silhouette_score(X, cluster_labels)
轮廓系数在-1到1之间,值越大表示聚类效果越好。我通常会把结果可视化,更直观地发现问题:
python复制from sklearn.metrics import silhouette_samples
import matplotlib.pyplot as plt
sample_silhouette_values = silhouette_samples(X, cluster_labels)
y_lower = 10
for i in range(n_clusters):
ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i]
ith_cluster_silhouette_values.sort()
size_cluster_i = ith_cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
plt.fill_betweenx(np.arange(y_lower, y_upper),
0, ith_cluster_silhouette_values)
y_lower = y_upper + 10
把前面的模块组合起来,就得到了完整的K-means算法:
python复制def k_means(X, n_clusters, max_iters=100):
# 随机初始化中心点
centers = X[np.random.choice(len(X), n_clusters, replace=False)]
for _ in range(max_iters):
# 分配样本到最近的中心
labels = np.array([nearest_cluster_center(x, centers) for x in X])
# 更新中心点位置
new_centers = estimate_centers(X, labels, n_clusters)
# 检查是否收敛
if np.all(centers == new_centers):
break
centers = new_centers
return labels, centers
在实际使用中,我发现有几个关键点需要注意:
python复制inertias = []
for k in range(1, 10):
kmeans = KMeans(n_clusters=k)
kmeans.fit(X)
inertias.append(kmeans.inertia_)
plt.plot(range(1,10), inertias, 'bx-')
plt.xlabel('k')
plt.ylabel('Inertia')
python复制best_score = -1
for _ in range(10):
labels, centers = k_means(X, n_clusters)
current_score = silhouette_score(X, labels)
if current_score > best_score:
best_score = current_score
best_labels = labels
python复制from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder()
X_encoded = encoder.fit_transform(X_categorical)
让我们用经典的鸢尾花数据集做个完整案例。首先加载数据:
python复制from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data
y = iris.target
先看看数据分布:
python复制import pandas as pd
df = pd.DataFrame(X, columns=iris.feature_names)
df['target'] = y
pd.plotting.scatter_matrix(df, c=y, figsize=(10,10))
标准化数据后运行K-means:
python复制from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3)
kmeans.fit(X_scaled)
y_pred = kmeans.labels_
评估结果:
python复制from sklearn.metrics import adjusted_rand_score
ari = adjusted_rand_score(y, y_pred)
print(f"Adjusted Rand Index: {ari:.3f}")
可视化聚类结果:
python复制import matplotlib.pyplot as plt
plt.scatter(X[:,0], X[:,1], c=y_pred)
plt.scatter(kmeans.cluster_centers_[:,0],
kmeans.cluster_centers_[:,1],
s=200, marker='X', c='red')
当数据量很大时,可以考虑使用Mini-Batch K-means:
python复制from sklearn.cluster import MiniBatchKMeans
mbk = MiniBatchKMeans(n_clusters=3, batch_size=100)
mbk.fit(X_large)
除了肘部法则,Gap统计量也是个好方法:
python复制from gap_statistic import OptimalK
optimalK = OptimalK()
n_clusters = optimalK(X, cluster_array=range(1, 10))
在电商用户分群项目中,我发现这些特征处理技巧很有效:
在实际项目中踩过不少坑,这里分享几个典型问题的解决方法:
问题1:聚类结果不稳定,每次运行都不一样
np.random.seed(42),或者使用K-means++初始化问题2:某些簇特别大,其他簇特别小
问题3:算法收敛太慢
max_iter和tol参数,或者先采样部分数据确定中心点问题4:高维数据聚类效果差
记得第一次用K-means处理用户数据时,因为没做标准化,结果完全被高消费用户主导。后来加入对数变换和Z-score标准化,才得到了有业务意义的分类结果。这也让我深刻理解到数据预处理的重要性——再好的算法,没有干净合适的数据也是白搭。