跳到主要内容Python 实现 K-Means 最优 K 值选择:肘部法则与轮廓系数 | 极客日志PythonAI写作Node.js算法
Python 实现 K-Means 最优 K 值选择:肘部法则与轮廓系数
K-Means 的关键在于先选好聚类数 K。这里用 Python 分别实现了肘部法则和轮廓系数的可视化:前者通过 SSE 曲线找下降放缓的拐点,后者通过平均轮廓系数和样本轮廓图判断聚类质量。示例数据中两种方法都指向 K=4,并进一步给出聚类效果图、真实数据集的迁移方式,以及 K-Means 常见问题的处理办法。
奇形怪状1 浏览 在聚类分析里,K-Means 最麻烦的一步不是训练模型,而是先把 K 选对。K 设小了,几个本来不该挤在一起的样本会被硬塞进同一类;K 设大了,模型又会把数据切得过碎。肘部法则和轮廓系数是最常见的两种判断方式,前者看 SSE 的拐点,后者看聚类的紧密度和分离度。下面这套代码直接用 Python 跑起来就行,适合先用模拟数据把流程走通,再换成自己的数据。
一、先把思路捋顺
K 值选择说到底是在找一个平衡点:既不能太粗,也不能太碎。
几个概念先放在一起看会更清楚:
- SSE(误差平方和):样本到所属聚类中心的距离平方和。它越小,簇内越紧;肘部法则关注的是 SSE 下降开始变慢的那个点。
- 轮廓系数:取值范围是 [-1, 1]。越接近 1,说明样本放在当前簇里越合理;接近 0,通常意味着边界模糊;为负时,多半是分错了。
- 工具:scikit-learn 负责 K-Means、SSE 和轮廓系数;pandas 负责数据整理;matplotlib 和 seaborn 用来画图。
环境上没什么特别要求,能跑 Python 3.7 以上基本就够了。常用依赖如下:
| 工具 / 依赖 | 版本要求 | 作用 |
|---|
| Python | 3.7+ | 运行环境 |
| scikit-learn | 0.23+ | K-Means、SSE、轮廓系数 |
| pandas | 1.0+ | 数据处理 |
| matplotlib / seaborn | 3.0+ / 0.10+ | 绘图 |
| numpy | 1.18+ | 数值计算 |
| pip | 20.0+ | 安装依赖 |
安装命令很直接:
pip install scikit-learn pandas matplotlib seaborn numpy
二、先造一份数据,把流程跑通
这里用 make_blobs 造一个 4 类的二维数据集。这样做的好处是结果容易看,也方便验证肘部点和轮廓系数是不是一致。
from sklearn.datasets import make_blobs
X, y_true = make_blobs(n_samples=1000, n_features=2, centers=4, cluster_std=0.6, random_state=42)
import pandas as pd
data = pd.DataFrame(X, columns=["特征 1", "特征 2"])
print("数据集预览:")
print(data.head())
print()
f"\n数据集形状:{data.shape}"
三、肘部法则:先看 SSE 曲线
肘部法则的实现不复杂,就是把一组候选 K 从头跑到尾,记录每个 K 的 SSE,再看曲线什么时候开始'弯'。真正有用的往往不是拐得最漂亮,而是下降幅度明显放缓的那个点。
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.cluster import KMeans
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def elbow_method_visualization(X, k_range):
"""
肘部法则可视化
:param X: 聚类数据集(特征矩阵)
:param k_range: K 值候选集(如 range(2, 11))
:return: 各 K 值对应的 SSE 列表
"""
sse_list = []
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X)
sse = kmeans.inertia_
sse_list.append(sse)
print(f"K={k} 时,SSE={sse:.2f}")
plt.figure(figsize=(10, 6))
sns.lineplot(x=k_range, y=sse_list, marker='o', linewidth=2, markersize=8, color='#2E86AB')
elbow_k = 4
elbow_sse = sse_list[elbow_k - k_range.start]
plt.scatter(elbow_k, elbow_sse, color='#E74C3C', s=200, zorder=5)
plt.annotate(f'肘部点 (K={elbow_k}, SSE={elbow_sse:.2f})',
xy=(elbow_k, elbow_sse),
xytext=(elbow_k+0.5, elbow_sse+50),
arrowprops=dict(arrowstyle='->', color='#E74C3C', linewidth=2),
fontsize=12, color='#E74C3C', fontweight='bold')
plt.title('K-Means 聚类肘部法则曲线(最优 K 值选择)', fontsize=14, fontweight='bold')
plt.xlabel('聚类数 K', fontsize=12)
plt.ylabel('SSE(簇内误差平方和)', fontsize=12)
plt.grid(True, alpha=0.3)
plt.xticks(k_range)
plt.tight_layout()
plt.savefig('肘部法则曲线.png', dpi=300)
plt.show()
return sse_list
k_candidates = range(2, 11)
sse_results = elbow_method_visualization(X, k_candidates)
跑完后控制台会打印每个 K 对应的 SSE。这个例子里,SSE 会在 K 从 2 增加到 4 时下降得很快,之后就没那么明显了。图上如果 K=4 附近出现明显拐点,通常就可以先把它记下来。
四、轮廓系数:再看聚类质量
单看 SSE 还不够,因为 SSE 天生会随着 K 变大而变小,光靠这个很容易偏向更大的 K。轮廓系数更稳一点,它同时考虑了类内紧密度和类间分离度。实际调参时,我更愿意把它和肘部法则一起看,单独用任何一个都不够放心。
from sklearn.metrics import silhouette_score, silhouette_samples
def silhouette_visualization(X, k_range):
"""
轮廓系数可视化(含平均轮廓系数曲线 + 样本轮廓图)
:param X: 聚类数据集(特征矩阵)
:param k_range: K 值候选集
:return: 各 K 值对应的平均轮廓系数列表
"""
silhouette_avg_list = []
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
cluster_labels = kmeans.fit_predict(X)
silhouette_avg = silhouette_score(X, cluster_labels)
silhouette_avg_list.append(silhouette_avg)
print(f"K={k} 时,平均轮廓系数={silhouette_avg:.4f}")
plt.figure(figsize=(10, 6))
sns.lineplot(x=k_range, y=silhouette_avg_list, marker='s', linewidth=2, markersize=8, color='#8E44AD')
best_k = k_range[np.argmax(silhouette_avg_list)]
best_score = max(silhouette_avg_list)
plt.scatter(best_k, best_score, color='#F39C12', s=200, zorder=5)
plt.annotate(f'最优 K 值 (K={best_k}, 系数={best_score:.4f})',
xy=(best_k, best_score),
xytext=(best_k+0.5, best_score-0.02),
arrowprops=dict(arrowstyle='->', color='#F39C12', linewidth=2),
fontsize=12, color='#F39C12', fontweight='bold')
plt.title('K-Means 聚类平均轮廓系数曲线(最优 K 值选择)', fontsize=14, fontweight='bold')
plt.xlabel('聚类数 K', fontsize=12)
plt.ylabel('平均轮廓系数', fontsize=12)
plt.grid(True, alpha=0.3)
plt.xticks(k_range)
plt.ylim(0, 1)
plt.tight_layout()
plt.savefig('平均轮廓系数曲线.png', dpi=300)
plt.show()
best_kmeans = KMeans(n_clusters=best_k, random_state=42, n_init=10)
best_cluster_labels = best_kmeans.fit_predict(X)
sample_silhouette_values = silhouette_samples(X, best_cluster_labels)
plt.figure(figsize=(12, 7))
y_lower = 10
for i in range(best_k):
ith_cluster_silhouette_values = sample_silhouette_values[best_cluster_labels == i]
ith_cluster_silhouette_values.sort()
size_cluster_i = ith_cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
color = plt.cm.Spectral(i / float(best_k))
plt.fill_betweenx(np.arange(y_lower, y_upper), 0, ith_cluster_silhouette_values,
facecolor=color, edgecolor=color, alpha=0.7)
plt.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i),
ha='center', va='center', fontweight='bold')
y_lower = y_upper + 10
plt.axvline(x=best_score, color='#E74C3C', linestyle='--', linewidth=2, label=f'平均轮廓系数:{best_score:.4f}')
plt.title(f'K={best_k}时的样本轮廓图', fontsize=14, fontweight='bold')
plt.xlabel('轮廓系数', fontsize=12)
plt.ylabel('样本聚类编号', fontsize=12)
plt.xlim([-0.1, 1.0])
plt.ylim([0, len(X) + (best_k + 1) * 10])
plt.legend(loc='upper right')
plt.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.savefig(f'K={best_k}_样本轮廓图.png', dpi=300)
plt.show()
return silhouette_avg_list
silhouette_results = silhouette_visualization(X, k_candidates)
这个例子里,K=4 的平均轮廓系数通常会最高。样本轮廓图也会更直观:每个簇的轮廓带比较完整,负值样本不多,说明分得还算干净。如果轮廓系数整体偏低,那就别急着相信 K-Means,先看看数据本身是不是适合做这种硬划分。
五、把两种结果放在一起看
真正落地时,我一般不会只盯着一张图。肘部法则给的是一个'下降开始放缓'的位置,轮廓系数给的是一个'分得是否合理'的信号。两者一致,心里就踏实很多;不一致时,通常要回头检查数据尺度、噪声或者 K 的候选范围。
print("\n=== 最优 K 值选择结果对比 ===")
result_df = pd.DataFrame({
'K 值': k_candidates,
'SSE': sse_results,
'平均轮廓系数': silhouette_results
})
print(result_df.to_string(index=False))
optimal_k = 4
print(f"\n综合两种方法,最优聚类数 K={optimal_k}")
有了 optimal_k 之后,再跑一次 K-Means,把聚类结果画出来看看:
optimal_kmeans = KMeans(n_clusters=optimal_k, random_state=42, n_init=10)
optimal_labels = optimal_kmeans.fit_predict(X)
centers = optimal_kmeans.cluster_centers_
plt.figure(figsize=(10, 8))
sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=optimal_labels, palette='viridis',
s=60, alpha=0.8, legend='full')
sns.scatterplot(x=centers[:, 0], y=centers[:, 1], color='red', s=200, marker='X',
label='聚类中心', edgecolor='black', linewidth=2)
plt.title(f'K={optimal_k}时 K-Means 聚类效果可视化', fontsize=14, fontweight='bold')
plt.xlabel('特征 1', fontsize=12)
plt.ylabel('特征 2', fontsize=12)
plt.grid(True, alpha=0.3)
plt.legend(title='聚类编号')
plt.tight_layout()
plt.savefig(f'K={optimal_k}_聚类效果可视化.png', dpi=300)
plt.show()
六、跑起来时容易踩的坑
K-Means 本身不算复杂,麻烦往往出在数据和参数上。
- 结果波动大:同一个 K 每次跑出的 SSE 或轮廓系数不一样,通常把
n_init 调大一点就能稳住,n_init=10 是个比较省事的起点。
- 肘部不明显:SSE 曲线很平,没什么拐弯。这时候别硬找肘部,先扩大 K 的候选范围,再看看数据有没有做标准化。很多时候问题不在图,而在特征尺度。
- 轮廓系数偏低:如果所有 K 的轮廓系数都不高,先怀疑数据结构,而不是怀疑自己代码写错了。数据本身分布松散、噪声多,K-Means 本来就不太擅长。
- 中文乱码:图里中文显示成方块,多半是字体没配好。Windows 用
SimHei,Mac 可以试 Arial Unicode MS。
七、换成真实数据时怎么做
如果要拿鸢尾花数据集试一遍,先做标准化,再挑前两个特征看看效果。K-Means 对尺度很敏感,这一步别省。
from sklearn.datasets import load_iris
from sklearn.preprocessing import StandardScaler
iris = load_iris()
X_iris = iris.data[:, :2]
y_iris = iris.target
scaler = StandardScaler()
X_iris_scaled = scaler.fit_transform(X_iris)
k_iris = range(2, 8)
sse_iris = elbow_method_visualization(X_iris_scaled, k_iris)
silhouette_iris = silhouette_visualization(X_iris_scaled, k_iris)
如果想让 K 的选择更自动一点,也可以用 SSE 的差分去粗略找拐点,不过这个方法更像辅助判断,不是严格答案:
def auto_select_optimal_k(sse_list, k_range):
"""自动化寻找肘部点(基于 SSE 一阶差分)"""
sse_diff = np.diff(sse_list)
sse_diff_rate = np.diff(sse_diff)
elbow_idx = np.argmax(np.abs(sse_diff_rate)) + 2
return elbow_idx
auto_optimal_k = auto_select_optimal_k(sse_results, k_candidates)
print(f"\n自动化选择的最优 K 值:{auto_optimal_k}")
如果数据维度不止 2 个,先做 PCA 再看聚类图会更顺手。高维数据直接画散点图没意义,降到 2 维至少能把结构看出来:
from sklearn.decomposition import PCA
X_iris_full = scaler.fit_transform(iris.data)
kmeans_iris = KMeans(n_clusters=3, random_state=42, n_init=10)
labels_iris = kmeans_iris.fit_predict(X_iris_full)
pca = PCA(n_components=2)
X_iris_pca = pca.fit_transform(X_iris_full)
plt.figure(figsize=(10, 8))
sns.scatterplot(x=X_iris_pca[:, 0], y=X_iris_pca[:, 1], hue=labels_iris, palette='Set2', s=60)
plt.title('PCA 降维后 K-Means 聚类效果(鸢尾花数据集)', fontsize=14, fontweight='bold')
plt.xlabel('PCA 特征 1', fontsize=12)
plt.ylabel('PCA 特征 2', fontsize=12)
plt.grid(True, alpha=0.3)
plt.show()
八、最后怎么选
这套流程跑下来,结论其实很简单:先用肘部法则把候选范围收窄,再用轮廓系数确认聚类是否站得住。这个例子里,两者都指向 K=4,所以直接用 4 就行。要是两种方法不一致,我会优先看轮廓系数,同时检查数据预处理是否到位。
对 K-Means 来说,K 选对了,后面的结果才有讨论价值。否则聚类图画得再漂亮,也只是把错误分得更整齐而已。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online