自己動手寫聚類(一)——初步搭建 k-means 聚類框架

又到了數挖實驗課了,這次老師讓我們自己動手寫聚類,k-means 聚類和層次聚類選一個,時間有限就只能寫一下 k-means 聚類了,層次聚類後面有時間再搞吧(期末真的讓人捉急,事情好多啊)。

實驗題目是這樣的的,要求使用 k-means 算法在一個無標籤的開源數據集上進行聚類,並對聚類結果進行分析,數據集可以自己找,我就找了一個統計世界上所有國家的信息的數據集進行聚類,接下來講一下我的思路。

1 數據集的下載與處理

首先,在網上找一個無標籤的數據集,然後我就找了一個統計世界上所有國家的信息的數據集,這個數據集在哪找的就不放出來了,因爲原本的數據集中涉及了一些敏感問題(有些人真的噁心,你說你做數據集就好好做數據集唄,你搞這些東西,真的服了),我把涉及這些的數據刪掉了,現在把基本上沒問題的數據集上傳放在這裏:countries of the world old.csv,不用積分,大家直接下載即可,但是這個數據集裏面的數據格式還有一些問題,所以我把它記爲 old 版本,下面我們對其進行一定的處理。

我們使用 pandas 從文件 countries of the world old.csv 中讀取數據然後打印:

import pandas as pd

path = "countries of the world old.csv"
dataset = pd.read_csv(path)
print(dataset)

我們會發現數據集主要有三個問題,第一個是 Region 地區那一列的對齊問題,第二個是很多數據的小數點用逗號表示,第三個是數據集中存在一些缺失值,然後我們要對它們進行處理:
在這裏插入圖片描述
我們通過以下的代碼對上述三個問題進行處理,對應的也使用註釋進行了標明:

def DataProcess(dataset):
    '''
    msg: 數據預處理
    param {
        dataset:pandas.DataFrame 數據集
    } 
    return: None
    '''
    dataset_len = len(dataset)
    for i in range(dataset_len):
        dataset.loc[i, "Region"] = dataset.loc[i, "Region"].strip() # 處理 Region 這一列的對齊問題
        for j in range(2, 20):
            value = dataset.iloc[i, j]
            if type(value) == str:
                dataset.iloc[i, j] = float(value.replace(",", ".")) # 處理小數點用逗號表示的問題
    dataset = dataset.fillna(dataset.mean()) # 使用均值插補的方法處理缺失值問題
    dataset.to_csv("countries of the world.csv", index=0) # 保存爲 CSV 文件,index=0 表示不保留行索引

經過上面的數據預處理後,我們可以生成一個清洗過的數據集文件,我把我處理過的也上傳放在這裏:countries of the world.csv,同樣不要積分,大家可以直接下載。

我們從清洗過的數據集文件中讀取數據再打印看看,我們可以看到現在數據基本沒有什麼問題了:
在這裏插入圖片描述

2 處理離散的無序屬性

觀察數據集我們發現,存在一個 Region 屬性,它是一個離散的無序屬性(non-ordinal attribute),相對的另一個概念就是有序屬性(ordinal attribute),有序屬性就是擁有像 “初級,中級,高級” 這樣的屬性值的屬性,雖然他們也是離散屬性,但它們可以映射爲 “1, 2, 3” 這樣有序的數值,因此有序屬性能夠根據屬性值直接計算距離。

然而像我們這裏的 Region 屬性映射成有序的數值是不合理的,因爲這樣相當於給他們強行加上了順序關係,但是如果不是映射成數值的話,不僅距離無法根據屬性值直接計算(這裏是無法直接計算,但是也有適合於無序屬性的距離,比如說 VDM 距離),而且也沒辦法在後面參與到生成均值向量的行爲當中,所以我們這裏使用 One-Hot 獨熱編碼的方法。

什麼是 One-Hot 獨熱編碼呢?就是比如說,現在有一個名爲交通工具的屬性,像 “飛機、汽車、火車” 這樣的屬性值可以直接映射爲 [1, 0, 0]、[0, 1, 0] 和 [0, 0, 1] 這樣的向量。類似地,我們的 Region 屬性擁有 ['ASIA (EX. NEAR EAST)', 'BALTICS', 'C.W. OF IND. STATES', 'EASTERN EUROPE', 'LATIN AMER. & CARIB', 'NEAR EAST', 'NORTHERN AFRICA', 'NORTHERN AMERICA', 'OCEANIA', 'SUB-SAHARAN AFRICA', 'WESTERN EUROPE'] 這樣 11 個屬性值(對應 11 個地區),那我們就可以映射成 11 個 11 維的向量。

因此,爲了在後面的聚類當中可以方便地處理數據集,我們可以捨棄 Region 屬性,然後增加 11 個屬性,它們分別對應上述的 11 個地區,舉個例子,如果某個國家位於某個地區,那麼在該地區對應的屬性的那一列就填 1,否則就填 0,我們可以通過下面的代碼進行處理:

def oneHot(raw_dataset):
    '''
    msg: 使用 One-Hot 處理離散的無序屬性
    param {
        raw_dataset:pandas.DataFrame 數據集
    } 
    return: None
    '''
    dataset_len = len(raw_dataset)
    regions = sorted(set(raw_dataset["Region"])) # 使用集合得到 Region 屬性的 11 個屬性值即對應 11 個地區
    dataset = raw_dataset.drop(columns="Region") # 捨棄 Region 屬性列
    for region in regions:
        # 如果國家位於某個地區,那麼在該地區對應的屬性列就填 1,否則就填 0
        region_column = [float(raw_dataset.loc[i, "Region"] == region) for i in range(dataset_len)]
        dataset[region] = region_column # 增加 11 個地區對應的屬性列
    return dataset

以下是經過上面的代碼所處理過的數據集,我們可以看到 Region 屬性列已經沒有了,而是增加了對應的 11 個地區的屬性列:
在這裏插入圖片描述

3 初始化聚類簇

k-means 聚類算法在開始時需要隨機選擇 k 個樣本作爲對應的 k 個初始均值向量,因此我們在這一步選擇對數據集進行簇的初始化,同時生成對應的初始均值向量。

我們可以給數據集再添加一個屬性 “Class”,用這個屬性來記錄每個樣本的簇類別,但是該屬性不參與 k-means 聚類,僅僅相當於一個標籤。

然後我們從數據集當中隨機選擇 k 個樣本(代碼當中 clusters_num 即對應我們的 k),接着把這 k 個樣本作爲初始均值向量,在數據集中將對應的 “Class” 屬性那一列填上自己的簇類別號,簇類別號分別爲 0, 1, 2, ……, k - 1

對上面這些想法進行實現的代碼如下:

def initClusters(dataset, clusters_num, seed):
    '''
    msg: 初始化聚類簇
    param {
        dataset:pandas.DataFrame 數據集
        clusters_num:int 簇的數量
        seed:int 隨機數種子
    } 
    return: {
        mean_vector_dict:dict 簇均值向量字典
    }
    '''
    dataset_len = len(dataset)
    dataset["Class"] = [-1 for i in range(dataset_len)] # 給數據集再添加一個屬性 “Class”
    random.seed(seed) # 設置隨機數種子
    init_clusters_index = sorted([random.randint(0, dataset_len - 1) for i in range(clusters_num)]) # 從數據集當中隨機選擇 clusters_num 個樣本
    mean_vector_dict = {} # 創建均值向量字典
    for i in range(clusters_num):
        mean_vector_dict[i] = dataset.iloc[init_clusters_index[i], 1:-1] # 把這 clusters_num 個樣本作爲初始均值向量
        dataset.loc[init_clusters_index[i], "Class"] = i # 在數據集中對應的 “Class” 屬性列上填入自己的簇類別號
    return mean_vector_dict

4 實現 k-means 聚類

這裏給出西瓜書上一張非常清晰的 k-means 聚類步驟圖,我們可以根據這張圖一步一步地進行代碼的編寫:
在這裏插入圖片描述
以下是對 k-means 聚類算法的具體實現,同時也是整個程序的核心代碼:

def kMeansClusters(dataset, mean_vector_dict):
    '''
    msg: 實現 k-means 聚類
    param {
        dataset:pandas.DataFrame 數據集
        mean_vector_dict:dict 簇均值向量字典
    } 
    return: None
    '''
    dataset_len = len(dataset)
    bar = trange(100) # 使用 tqdm 第三方庫,調用 tqdm.std.trange 方法給循環加個進度條
    for _ in bar: # 使用 _ 表示進行佔位,因爲在這裏我們只是循環而沒有用到循環變量
        bar.set_description("The clustering is runing") # 給進度條加個描述
        for i in range(dataset_len):
            dist_dict = {}
            for cluster_id in mean_vector_dict:
                dist_dict[cluster_id] = vectorDist(dataset.iloc[i, 1:-1], mean_vector_dict[cluster_id], p=2) # 計算樣本 xi 與各均值向量的距離
            dist_sorted = sorted(dist_dict.items(), key=lambda item: item[1]) # 對樣本 xi 與各均值向量的距離進行排序
            dataset.loc[i, "Class"] = dist_sorted[0][0] # 根據距離最近的均值向量確定 xi 的簇類別並在 “Class” 屬性列上填入對應簇類別號,即將 xi 劃入相應的簇
        flag = 0
        for cluster_id in mean_vector_dict:
            cluster = dataset[dataset["Class"] == cluster_id] # 得到簇內的所有樣本
            cluster_mean_vector = vectorAverage(cluster) # 根據簇內的所有樣本計算新的均值向量
            if not ifEqual(mean_vector_dict[cluster_id], cluster_mean_vector): # 判斷新的均值向量是否和當前均值向量相同
                mean_vector_dict[cluster_id] = cluster_mean_vector # 不相同,將新的均值向量替換當前均值向量
            else:
                flag += 1 # 保持當前均值向量不變,並進行計數
        if flag == len(mean_vector_dict): # 判斷是否所有簇的均值向量均未更新
            bar.close() # 所有簇的均值向量均未更新,關閉進度條,退出循環
            print("The mean vectors are no longer changing, the clustering is over.")
            return # 直接退出循環
    print("Reach the maximum number of iterations, the clustering is over.")

觀察上面的代碼,我們會發現其實我們還有三處語句沒有具體實現,我們來一一進行實現:

  1. 計算距離用到的 vectorDist() 方法:

    我們使用歐式距離(Euclidean Distance)來計算我們的樣本之間的距離:
    disted(xi,xj)=xixj2=u=1nxiuxju2 \bf{dist_{ed} (x_i, x_j) = || x_i - x_j ||_2 = \sqrt{ \sum_{ u =1 }^n | x_{iu} - x_{ju} |^2 }}

    def vectorDist(vector_X, vector_Y, p):
        vector_X = np.array(vector_X)
        vector_Y = np.array(vector_Y)
        return sum((vector_X - vector_Y) ** p) ** (1 / p)
    
  2. 計算新的簇內均值向量用到的 vectorAverage() 方法:

    def vectorAverage(cluster):
        return cluster.iloc[:, 1:-1].mean()
    
  3. 判斷新的均值向量是否和當前均值向量相同用到的 ifEqual() 方法:

    def ifEqual(pandas_X, pandas_Y):
        return pandas_X.equals(pandas_Y)
    

在聚類之後,我們可以通過以下代碼提取聚類結果,然後打印結果並進行觀察:

def getClusters(dataset, clusters_num):
    '''
    msg: 提取聚類結果
    param {
        dataset:pandas.DataFrame 數據集
        clusters_num:int 簇的數量
    } 
    return {
        clusters_dict:dict 鍵值對的值爲 pandas.DataFrame 類型
        cluster_indexs_dict:dict 鍵值對的值爲 list 類型
        cluster_countries_dict:dict 鍵值對的值爲 list 類型
    }
    '''
    clusters_dict = {}
    cluster_indexs_dict = {}
    cluster_countries_dict = {}
    for cluster_id in range(clusters_num):
        clusters_dict[cluster_id] = dataset[dataset["Class"] == cluster_id]
        cluster_indexs_dict[cluster_id] = list(clusters_dict[cluster_id].index)
        cluster_countries_dict[cluster_id] = list(dataset.loc[cluster_indexs_dict[cluster_id], "Country"])
    return clusters_dict, cluster_indexs_dict, cluster_countries_dict

這裏設置 clusters_num=11 時,因爲選取簇數量的一種簡單的經驗方法就是,對於 n 個樣本的數據集,可以設置簇數大致爲 n / 2 的平方根,我們這裏就是 11,打印聚類結果如下:
在這裏插入圖片描述

5 構建數據集的距離矩陣

在上一步其實聚類就已經完成了,但是我們還需要評估我們聚類的質量並進行分析,而因爲我們用到的數據集沒有基準(專家構建的理想聚類)可用,所以通過考慮簇的分離情況評估聚類的好壞,這裏我們選用輪廓係數作爲我們的評估標準,我們將在下一步詳細講述。

因爲在計算輪廓係數時需要用到大量樣本之間的距離,同時也爲了層次聚類做準備(現在沒時間做,時間充裕了補回來),所以本着一勞永逸的想法,這裏就針對數據集構建一個距離矩陣,裏面存放所有樣本之間的距離,不過我們在代碼當中並不以矩陣的形式存儲,而是以字典形式存儲,這樣既能夠快速查詢,又方便保存矩陣(使用字典可能並不節約空間,真正節約空間的形式是存儲爲上三角矩陣,但這裏考慮到方便之後的查詢,所以構建爲字典形式),實現代碼如下:

def distanceMatrix(dataset, path):
    '''
    msg: 以字典形式構建數據集的距離矩陣
    param {
        dataset:pandas.DataFrame 數據集
        path:str 存放距離矩陣的文件,建議格式爲 .json
    } 
    return{
        matrix_dict:dict 字典形式的距離矩陣
    }
    '''
    if not os.path.exists(path):
        dataset_len = len(dataset)
        matrix_dict = {}
        for i in range(dataset_len):
            for j in range(i + 1, dataset_len):
                matrix_dict[str((i, j))] = vectorDist(dataset.iloc[i, 1:-1], dataset.iloc[j, 1:-1], p=2)
        with open(path, 'w+') as f:
            json.dump(matrix_dict, f)
    else:
        with open(path, 'r+') as f:
            matrix_dict = json.load(f)
    return matrix_dict

6 評估聚類質量

在上一步我們也提到過了,我們可以通過輪廓係數(silhouette coefficient)這個評估標準來評估與分析聚類質量,那什麼是輪廓係數呢?怎麼計算呢?接下來我們來了解一下。

對於 n 個樣本的數據集 D,假設 D 被劃分成 k 個簇 C1,C2,……,Ck,對於 D 中的每個樣本 i,我們可以計算如下的值:

  • i 與 i 所屬簇 Cp(1 <= p <= t)的其他對象之間的平均距離 a[i],a[i] 的值反映樣本 i 所屬簇的緊湊性,該值越小,則說明簇越緊湊:
    a[i]=jCp,ijdist(i,j)Cp1 a[i] = \frac {\sum_{j \in C_p, i \ne j} dist(i, j)} {|C_p - 1|} \\

  • i 到 不屬於 i 的所屬簇 Cp 的最小平均距離 b[i],b[i] 的值反映樣本 i 與其他簇的分離程度,b[i] 的值越大,則說明樣本 i 與其他簇越分離:
    b[i]=minCq:1qt,qp{jCqdist(i,j)Cq} b[i] = \min_{C_q:1 \leqslant q \leqslant t, q \ne p} \left \{ \frac {\sum_{j \in C_q} {dist(i, j)}} {|C_q|} \right \}

  • 樣本 i 的輪廓係數 s[i],輪廓係數的值在 -1 和 1 之間,當樣本 i 的輪廓係數的值接近 1 時,則說明包含樣本 i 的簇是緊湊的,並且樣本 i 遠離其他簇,而當輪廓係數爲負數時(即 b[i] < a[i]),則說明此時聚類情況比較糟糕,樣本 i 距離其他簇的樣本比距離與自己同屬簇的樣本更近:
    s[i]=b[i]a[i]max{a[i],b[i]} s[i] = \frac {b[i] - a[i]} {\max \{ a[i], b[i] \}}

一般地,爲了度量聚類的質量,我們可以使用數據集中所有樣本的輪廓係數的平均值來評估聚類的好壞。

以下是達到上述目的的具體實現代碼,不過這段代碼我寫的有點 low,好多 for 循環,但一時間不知道怎麼更改,希望大家有什麼好辦法的話可以告知我一下,大家互相交流嘛:

def silhouetteCoefficient(dataset, clusters_num, clusters_dict, cluster_indexs_dict, dist_matrix):
    '''
    msg: 計算數據集中所有樣本的輪廓係數的平均值
    param {
        dataset:pandas.DataFrame 數據集
        clusters_num:int 簇的數量
        clusters_dict:dict 鍵值對的值爲 pandas.DataFrame 類型
        cluster_indexs_dict:dict 鍵值對的值爲 list 類型
        dist_matrix:dict 字典形式的距離矩陣
    } 
    return {
        silhouette_coefficient:float 數據集中所有樣本的輪廓係數的平均值
    }
    '''
    dataset_len = len(dataset)
    a = np.array([0 for i in range(dataset_len)], dtype=np.float64)
    b = np.array([0 for i in range(dataset_len)], dtype=np.float64)
    s = np.array([0 for i in range(dataset_len)], dtype=np.float64)

    for cluster_id in range(clusters_num):

        cluster_len = len(cluster_indexs_dict[cluster_id])
        clusters_copy_remove = cluster_indexs_dict.copy()
        clusters_copy_remove.pop(cluster_id)
        
        for i in cluster_indexs_dict[cluster_id]:
            cluster_copy_remove = cluster_indexs_dict[cluster_id].copy()
            cluster_copy_remove.remove(i)
            for j in cluster_copy_remove:
                a[i] += dist_matrix[str((min(i, j), max(i, j)))]
            a[i] = a[i] / cluster_len - 1

            bi = []
            for key in clusters_copy_remove:
                xb = 0
                for k in clusters_copy_remove[key]:
                    xb += dist_matrix[str((min(i, k), max(i, k)))]
                xb = xb / len(clusters_copy_remove[key])
                bi.append(xb)
            if len(bi) != 0:
                b[i] = min(bi)

            s[i] = ((b[i] - a[i]) / max(a[i], b[i]))

    silhouette_coefficient = np.average(s)
    return silhouette_coefficient

寫好上述代碼後,我們來測試一下我們剛纔的聚類的好壞,我們發現當我們把簇的數目設置爲 11 以及隨機數種子設置爲 1 時,數據集中所有樣本的輪廓係數的平均值爲 0.6239293293560384,這個效果還算過得去,但是並沒有想象中的好,可能是因爲簇的數目設置的不好,或者是初始均值向量產生的不合適即隨機數種子設置的不太妥當,至於具體是什麼原因,在後面的內容中我們來詳細地對其進行研究:
在這裏插入圖片描述

7 組合模塊形成完整代碼

最後,將前面的所有模塊進行組合,並添加 main 函數,得到完整代碼:

'''
Description: 初步搭建 k-means 聚類框架
Author: stepondust
Date: 2020-05-21
'''
import random, json, os
import pandas as pd
import numpy as np
from tqdm.std import trange


def DataProcess(dataset):
    '''
    msg: 數據預處理
    param {
        dataset:pandas.DataFrame 數據集
    } 
    return: None
    '''
    dataset_len = len(dataset)
    for i in range(dataset_len):
        dataset.loc[i, "Region"] = dataset.loc[i, "Region"].strip() # 處理 Region 這一列的對齊問題
        for j in range(2, 20):
            value = dataset.iloc[i, j]
            if type(value) == str:
                dataset.iloc[i, j] = float(value.replace(",", ".")) # 處理小數點用逗號表示的問題
    dataset = dataset.fillna(dataset.mean()) # 使用均值插補的方法處理缺失值問題
    dataset.to_csv("countries of the world.csv", index=0) # 保存爲 CSV 文件,index=0 表示不保留行索引


def oneHot(raw_dataset):
    '''
    msg: 使用 One-Hot 處理離散的無序屬性
    param {
        raw_dataset:pandas.DataFrame 數據集
    } 
    return: None
    '''
    dataset_len = len(raw_dataset)
    regions = sorted(set(raw_dataset["Region"])) # 使用集合得到 Region 屬性的 11 個屬性值即對應 11 個地區
    dataset = raw_dataset.drop(columns="Region") # 捨棄 Region 屬性列
    for region in regions:
        # 如果國家位於某個地區,那麼在該地區對應的屬性列就填 1,否則就填 0
        region_column = [float(raw_dataset.loc[i, "Region"] == region) for i in range(dataset_len)]
        dataset[region] = region_column # 增加 11 個地區對應的屬性列
    return dataset


def initClusters(dataset, clusters_num, seed):
    '''
    msg: 初始化聚類簇
    param {
        dataset:pandas.DataFrame 數據集
        clusters_num:int 簇的數量
        seed:int 隨機數種子
    } 
    return {
        mean_vector_dict:dict 簇均值向量字典
    }
    '''
    dataset_len = len(dataset)
    dataset["Class"] = [-1 for i in range(dataset_len)] # 給數據集再添加一個屬性 “Class”
    random.seed(seed) # 設置隨機數種子
    init_clusters_index = sorted([random.randint(0, dataset_len - 1) for i in range(clusters_num)]) # 從數據集當中隨機選擇 clusters_num 個樣本
    mean_vector_dict = {} # 創建均值向量字典
    for i in range(clusters_num):
        mean_vector_dict[i] = dataset.iloc[init_clusters_index[i], 1:-1] # 把這 clusters_num 個樣本作爲初始均值向量
        dataset.loc[init_clusters_index[i], "Class"] = i # 在數據集中對應的 “Class” 屬性列上填入自己的簇類別號
    return mean_vector_dict


def vectorDist(vector_X, vector_Y, p):
    vector_X = np.array(vector_X)
    vector_Y = np.array(vector_Y)
    return sum((vector_X - vector_Y) ** p) ** (1 / p)


def vectorAverage(cluster):
    return cluster.iloc[:, 1:-1].mean()


def ifEqual(pandas_X, pandas_Y):
    return pandas_X.equals(pandas_Y)


def kMeansClusters(dataset, mean_vector_dict):
    '''
    msg: 實現 k-means 聚類
    param {
        dataset:pandas.DataFrame 數據集
        mean_vector_dict:dict 簇均值向量字典
    } 
    return: None
    '''
    dataset_len = len(dataset)
    bar = trange(100) # 使用 tqdm 第三方庫,調用 tqdm.std.trange 方法給循環加個進度條
    for _ in bar: # 使用 _ 表示進行佔位,因爲在這裏我們只是循環而沒有用到循環變量
        bar.set_description("The clustering is runing") # 給進度條加個描述
        for i in range(dataset_len):
            dist_dict = {}
            for cluster_id in mean_vector_dict:
                dist_dict[cluster_id] = vectorDist(dataset.iloc[i, 1:-1], mean_vector_dict[cluster_id], p=2) # 計算樣本 xi 與各均值向量的距離
            dist_sorted = sorted(dist_dict.items(), key=lambda item: item[1]) # 對樣本 xi 與各均值向量的距離進行排序
            dataset.loc[i, "Class"] = dist_sorted[0][0] # 根據距離最近的均值向量確定 xi 的簇類別並在 “Class” 屬性列上填入對應簇類別號,即將 xi 劃入相應的簇
        flag = 0
        for cluster_id in mean_vector_dict:
            cluster = dataset[dataset["Class"] == cluster_id] # 得到簇內的所有樣本
            cluster_mean_vector = vectorAverage(cluster) # 根據簇內的所有樣本計算新的均值向量
            if not ifEqual(mean_vector_dict[cluster_id], cluster_mean_vector): # 判斷新的均值向量是否和當前均值向量相同
                mean_vector_dict[cluster_id] = cluster_mean_vector # 不相同,將新的均值向量替換當前均值向量
            else:
                flag += 1 # 保持當前均值向量不變,並進行計數
        if flag == len(mean_vector_dict): # 判斷是否所有簇的均值向量均未更新
            bar.close() # 所有簇的均值向量均未更新,關閉進度條
            print("The mean vectors are no longer changing, the clustering is over.")
            return # 直接退出循環
    print("Reach the maximum number of iterations, the clustering is over.")


def getClusters(dataset, clusters_num):
    '''
    msg: 提取聚類結果
    param {
        dataset:pandas.DataFrame 數據集
        clusters_num:int 簇的數量
    } 
    return {
        clusters_dict:dict 鍵值對的值爲 pandas.DataFrame 類型
        cluster_indexs_dict:dict 鍵值對的值爲 list 類型
        cluster_countries_dict:dict 鍵值對的值爲 list 類型
    }
    '''
    clusters_dict = {}
    cluster_indexs_dict = {}
    cluster_countries_dict = {}
    for cluster_id in range(clusters_num):
        clusters_dict[cluster_id] = dataset[dataset["Class"] == cluster_id]
        cluster_indexs_dict[cluster_id] = list(clusters_dict[cluster_id].index)
        cluster_countries_dict[cluster_id] = list(dataset.loc[cluster_indexs_dict[cluster_id], "Country"])
    return clusters_dict, cluster_indexs_dict, cluster_countries_dict


def distanceMatrix(dataset, path):
    '''
    msg: 以字典形式構建數據集的距離矩陣
    param {
        dataset:pandas.DataFrame 數據集
        path:str 存放距離矩陣的文件,建議格式爲 .json
    } 
    return{
        matrix_dict:dict 字典形式的距離矩陣
    }
    '''
    if not os.path.exists(path):
        dataset_len = len(dataset)
        matrix_dict = {}
        for i in range(dataset_len):
            for j in range(i + 1, dataset_len):
                matrix_dict[str((i, j))] = vectorDist(dataset.iloc[i, 1:-1], dataset.iloc[j, 1:-1], p=2)
        with open(path, 'w+') as f:
            json.dump(matrix_dict, f)
    else:
        with open(path, 'r+') as f:
            matrix_dict = json.load(f)
    return matrix_dict


def silhouetteCoefficient(dataset, clusters_num, clusters_dict, cluster_indexs_dict, dist_matrix):
    '''
    msg: 計算數據集中所有樣本的輪廓係數的平均值
    param {
        dataset:pandas.DataFrame 數據集
        clusters_num:int 簇的數量
        clusters_dict:dict 鍵值對的值爲 pandas.DataFrame 類型
        cluster_indexs_dict:dict 鍵值對的值爲 list 類型
        dist_matrix:dict 字典形式的距離矩陣
    } 
    return {
        silhouette_coefficient:float 數據集中所有樣本的輪廓係數的平均值
    }
    '''
    dataset_len = len(dataset)
    a = np.array([0 for i in range(dataset_len)], dtype=np.float64)
    b = np.array([0 for i in range(dataset_len)], dtype=np.float64)
    s = np.array([0 for i in range(dataset_len)], dtype=np.float64)

    for cluster_id in range(clusters_num):

        cluster_len = len(cluster_indexs_dict[cluster_id])
        clusters_copy_remove = cluster_indexs_dict.copy()
        clusters_copy_remove.pop(cluster_id)

        for i in cluster_indexs_dict[cluster_id]:
            cluster_copy_remove = cluster_indexs_dict[cluster_id].copy()
            cluster_copy_remove.remove(i)
            for j in cluster_copy_remove:
                a[i] += dist_matrix[str((min(i, j), max(i, j)))]
            a[i] = a[i] / cluster_len - 1

            bi = []
            for key in clusters_copy_remove:
                xb = 0
                for k in clusters_copy_remove[key]:
                    xb += dist_matrix[str((min(i, k), max(i, k)))]
                xb = xb / len(clusters_copy_remove[key])
                bi.append(xb)
            if len(bi) != 0:
                b[i] = min(bi)

            s[i] = ((b[i] - a[i]) / max(a[i], b[i]))

    silhouette_coefficient = np.average(s)
    return silhouette_coefficient


if __name__ == "__main__":
    clusters_num = 11
    seed = 1

    if not os.path.exists("countries of the world.csv"):
        old_dataset = pd.read_csv("countries of the world old.csv")
    raw_dataset = pd.read_csv("countries of the world.csv")
    
    dataset = oneHot(raw_dataset)

    mean_vector_dict = initClusters(dataset, clusters_num, seed)

    print(f"Set the number of clusters to {clusters_num} and the random seed to {seed}, start clustering.")
    kMeansClusters(dataset, mean_vector_dict)
    clusters_dict, cluster_indexs_dict, cluster_countries_dict = getClusters(dataset, clusters_num)
    print("The result of clusters:\n", cluster_countries_dict)

    dist_matrix = distanceMatrix(dataset, "matrix.json")

    silhouette_coefficient = silhouetteCoefficient(dataset, clusters_num, clusters_dict, cluster_indexs_dict, dist_matrix)
    print(f"The average of the silhouette coefficients of all samples in the dataset is {silhouette_coefficient}")

8 數據與結果分析

剛纔說過,簇的數目設置的不好,或者是初始均值向量產生的不合適即隨機數種子設置的不太妥當,都可能導致聚類的結果不理想,接下來我們分別來對兩種情況進行分析。

我們先設置 seed=1,然後將 clusters_num 從 1 變化到 15,觀察輪廓係數 silhouette_coefficient 的變化:
在這裏插入圖片描述
上述這張圖是 clusters_num 從 1 變化到 15 程序運行的結果,可能上面的這張圖看的還不明顯,我們來畫一下趨勢圖看看,我們可以清晰地看到,當簇的數目 clusters_num 設置爲 2 時得到了最好的聚類效果:
在這裏插入圖片描述
然後,我們設置 clusters_num=2 ,並設置不同的隨機數種子從 1 到 15,也就是獲得不同的初始均值向量,我們來觀察輪廓係數 silhouette_coefficient 的變化:
在這裏插入圖片描述
這次我們連趨勢圖都不用畫,就可以清楚地看到,設置不同的隨機數種子對聚類效果並沒有影響,因此,綜上所述,對我們的聚類質量有影響的是簇的數目的設置,與隨機數種子的設置關係不大

好了本文到此就快結束了,以上就是我自己動手寫 k-means 聚類的想法和思路,大家參照一下即可,重要的還是經過自己的思考來編寫代碼,文章中還有很多不足和不正確的地方,歡迎大家指正(也請大家體諒,寫一篇博客真的挺累的,花的時間比我寫出代碼的時間還要長),我會盡快修改,之後我也會盡量完善本文,儘量寫得通俗易懂。

博文創作不易,轉載請註明本文地址:https://blog.csdn.net/qq_44009891/article/details/106214080

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章