自己动手写聚类(一)——初步搭建 k-means 聚类框架

又到了数挖实验课了,这次老师让我们自己动手写聚类,k-means 聚类和层次聚类选一个,时间有限就只能写一下 k-means 聚类了,层次聚类后面有时间再搞吧(期末真的让人捉急,事情好多啊)。

实验题目是这样的的,要求使用 k-means 算法在一个无标签的开源数据集上进行聚类,并对聚类结果进行分析,数据集可以自己找,我就找了一个统计世界上所有国家的信息的数据集进行聚类,接下来讲一下我的思路。

1 数据集的下载与处理

首先,在网上找一个无标签的数据集,然后我就找了一个统计世界上所有国家的信息的数据集,这个数据集在哪找的就不放出来了,因为原本的数据集中涉及了一些敏感问题(有些人真的恶心,你说你做数据集就好好做数据集呗,你搞这些东西,真的服了),我把涉及这些的数据删掉了,现在把基本上没问题的数据集上传放在这里:countries of the world old.csv,不用积分,大家直接下载即可(有时候我上传时设置的是 0 积分,但是过一段时间它又突然需要花积分下载了,不知道为什么,请大家遇到这种情况就在评论区告知一声,我改回来,或者留下 QQ,我私发给你),但是这个数据集里面的数据格式还有一些问题,所以我把它记为 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)来计算我们的样本之间的距离:
    d i s t e d ( x i , x j ) = ∣ ∣ x i − x j ∣ ∣ 2 = ∑ u = 1 n ∣ x i u − x j u ∣ 2 \bf{dist_{ed} (x_i, x_j) = || x_i - x_j ||_2 = \sqrt{ \sum_{ u =1 }^n | x_{iu} - x_{ju} |^2 }} disted(xi,xj)=xixj2=u=1nxiuxju2

    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 ] = ∑ j ∈ C p , i ≠ j d i s t ( i , j ) ∣ C p − 1 ∣ a[i] = \frac {\sum_{j \in C_p, i \ne j} dist(i, j)} {|C_p - 1|} \\ a[i]=Cp1jCp,i=jdist(i,j)

  • i 到 不属于 i 的所属簇 Cp 的最小平均距离 b[i],b[i] 的值反映样本 i 与其他簇的分离程度,b[i] 的值越大,则说明样本 i 与其他簇越分离:
    b [ i ] = min ⁡ C q : 1 ⩽ q ⩽ t , q ≠ p { ∑ j ∈ C q d i s t ( i , j ) ∣ C q ∣ } 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 \} b[i]=Cq:1qt,q=pmin{CqjCqdist(i,j)}

  • 样本 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] \}} s[i]=max{a[i],b[i]}b[i]a[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")
        dataProcess(old_dataset)
    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

  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值