k-平均演算法實驗 ( K-Means Clustering )

摘要
K-Means 分群 (K-Means Clustering) 是一種常用的無監督學習算法,用來將資料集分成 K 個群集,目的是將資料點分成 K 個群集,使同一群集內的資料點在特徵空間中彼此接近,而不同群集之間的相對分離,通透過最小化群內資料點與其群心之平方距離總和來達成分群目的
在理解 K-Means 的演算法運作之前,可以參考
向量空間中的歐幾里得距離與 L2 範數 (2025-05-06). 重構節點. https://renode.site/articles/artificial-intelligence/AI20250016
K-Means 的最佳化目標
成本函數(Cost Function)
K-Means 的成本函數(Cost Function,也常稱為 Objective Function)定義為所有資料點到其所屬群心距離的平方總和
其中 表示第 個群集, 為其群心(centroid),此函數表示的是整體分群結構的緊密程度,值越小代表群內越集中。
在演算法實作上,K-Means 並不需要顯式成本函數,而是透過兩個步驟的交替執行,使 單調下降,最終收斂至一個局部最小值。
分群品質指標
SSE 用於衡量分群後「群內緊密程度」,數值越小代表資料點越集中於各自的群心。然而,SSE 會隨著 增加而單調下降,因此其本身並不適合直接用來比較不同 的模型好壞,而是作為相對變化的觀測量。
起初函數優先加單一個群心的總向量距離,然後再加總每個群心的總向量距離
- :群數(number of clusters)
- :平方歐幾里得距離(Squared Euclidean Distance)
群數選擇策略:手肘法(Elbow Method)
手肘法是群數選擇策略,利用 SSE 隨 變化的特性來選擇合理的群數。透過計算不同 值下的 SSE,並繪製 對 SSE 的關係曲線。
當 增加時,SSE 初期會快速下降,代表增加群數能顯著改善群內緊密度,然而在某一點之後,下降幅度會明顯趨緩。此轉折位置即稱為「手肘」,對應的 被視為在模型複雜度與分群效果之間較為平衡的選擇。

演算法原理
基本流程
-
初始化 K 個群心(centroids):隨機選擇 K 個點作為初始群心 可以使用肘部法則(Elbow Method)來選擇 K-means 中最佳的群集數
-
分配資料點:將每個資料點分配到最近的群心,形成 K 個群 每一個向量都會各自計算到每一個群心的距離,並加入最近的群心
-
更新群心:根據每個群集內的資料點,計算新的群心,即群集內所有點的均值
-
重複步驟:直到群心不再改變(收斂),或者達到預設的最大迭代次數
實驗
使用套件: sklearn
import pandas as pd from sklearn.preprocessing import StandardScaler import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.decomposition import PCA
使用資料來源 M. Cardoso. "Wholesale customers," UCI Machine Learning Repository, 2013. [Online]. Available: https://doi.org/10.24432/C5030X.
讀取資料
# 讀取資料集
df = pd.read_csv('Wholesale customers data.csv')
# 顯示資料概況
print("資料欄位資訊:")
print(df.info())
print("敘述統計摘要:")
print(df.describe())
print(df.head())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440 entries, 0 to 439 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Channel 440 non-null int64 1 Region 440 non-null int64 2 Fresh 440 non-null int64 3 Milk 440 non-null int64 4 Grocery 440 non-null int64 5 Frozen 440 non-null int64 6 Detergents_Paper 440 non-null int64 7 Delicassen 440 non-null int64 dtypes: int64(8) memory usage: 27.6 KB
資料處理
資料處理學術稱為特徵工程,這裡使用的是 z-score 標準化,如果原始資料接近常態分佈,大部分 z-score 值會落在 區間內(99.73%)
# 移除不重要欄位,從第2欄位開始
numeric_features = df.iloc[:, 2:]
# 特徵標準化(z-score 標準化)
scaler = StandardScaler()
scaled_data = scaler.fit_transform(numeric_features)
print("第一筆資料")
print(df.head(1))
print("標準化後的資料:")
print(scaled_data[:1]) # 顯示前5筆標準化後的資料
結果
第一筆資料 Channel Region Fresh Milk Grocery Frozen Detergents_Paper Delicassen 0 2 3 12669 9656 7561 214 2674 1338 標準化後的資料: [[ 0.05293319 0.52356777 -0.04111489 -0.58936716 -0.04356873 -0.06633906]]
肘部法則(Elbow Method)
前面的內容中提到
當 增加時,SSE 初期會快速下降,代表增加群數能顯著改善群內緊密度,然而在某一點之後,下降幅度會明顯趨緩。此轉折位置即稱為「手肘」
此部分可以觀察出k群為3的位子是明顯的下降處,因此此實驗將選擇3群作為分群數

# %% 肘部法則(Elbow Method)找最佳 k 值
inertia_list = []
k_range = range(1, 11)
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42)
kmeans.fit(scaled_data)
inertia_list.append(kmeans.inertia_)
print("Inertia List:", inertia_list)
# 繪製
plt.figure(figsize=(8, 4))
plt.plot(k_range, inertia_list, 'bo-')
plt.xlabel('群數 k')
plt.ylabel('Inertia (總內部平方誤差)')
plt.title('Elbow Method — 判斷最佳群數')
plt.grid(True)
plt.show()
預測結果
# %% 預測結果
k = 3
kmeans = KMeans(n_clusters=k, random_state=42)
cluster_labels = kmeans.fit_predict(scaled_data)
# 將分群結果加入原始資料框
df['Cluster'] = cluster_labels
print("分群結果:")
print(df['Cluster'].value_counts())
n_clusters: K群集random_state: 固定隨機值,避免每次測試結果不一樣
KMeans 自動執行以下流程:
-
隨機初始化 個群心(centroids)
-
根據距離分配每筆資料到最近的群心
-
更新每個群的群心(計算平均值)
-
反覆進行直到收斂(群心不再移動,或到達預設的最大迭代次數)
PCA(主成分分析)
PCA (Principal Component Analysis) 是資料降維與特徵萃取的技術,原因是資料集多以多維度的特徵在向量空間中計算,在視覺化圖表中需要使用資料降為的方式查得已在平面進行觀察
# %%
# PCA 將資料降為 2 維以便視覺化
pca = PCA(n_components=2)
pca_result = pca.fit_transform(scaled_data)
df['PCA1'] = pca_result[:, 0]
df['PCA2'] = pca_result[:, 1]
視覺化分群結果

plt.figure(figsize=(8, 6))
colors = ['#4F4F4F', '#003060', '#C4C4C4']
for cluster_id in range(k):
subset = df[df['Cluster'] == cluster_id]
plt.scatter(subset['PCA1'], subset['PCA2'],
label=f'Cluster {cluster_id}', color=colors[cluster_id])
plt.xlabel('PCA1')
plt.ylabel('PCA2')
plt.title('K-Means')
plt.legend()
plt.grid(True, alpha=0.5)
plt.show()
結果分析
個集群人數
分群結果: Cluster 0 350 1 53 2 37 Name: count, dtype: int64
每個群的消費總額平均

Cluster 0
整體消費總額偏低,而且群集數高達350人,且消費集中於生鮮食品,因此可能是一般消費者(家庭)
Cluster 1
整體消費總額至於中間,消費項目集中在生鮮食品、冷凍食品、熟食,可能是大型團體、零售商、小型餐飲業
Cluster 2
整體消費總額最高,消費項目集中在乳製品、日常雜貨以及清潔用品,因此可能是中、大型餐飲業者,或是商業客戶


