TL;DR

场景:用 NumPy/Pandas 手写 K-Means,对 Iris.txt 做 3 类聚类并输出质心与分簇结果

结论:实现可跑通,但需补齐”空簇处理 / 最大迭代 / 版本与数据类型约束 / 特征尺度”才能工程化稳定

产出:distEclud + randCent + kMeans 完整链路、结果表 result_set、常见错误定位与修复速查

Python实现

导入依赖

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# 解决坐标轴刻度负号乱码
plt.rcParams['axes.unicode_minus'] = False
# 解决中文乱码问题
plt.rcParams['font.sans-serif'] = ['Simhei']

导入数据集

此处使用鸢尾花数据集为例:

import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline
#导入数据集
iris = pd.read_csv("iris.txt",header = None)
iris.head()
iris.shape

编写距离计算函数

我们需要定义一个两个长度相等的数组之间欧式距离计算函数,在不直接应用计算结果,只比较距离远近的情况下,我们可以用距离平方和代替距离进行比较,化简开平方运算,从而减少函数计算量。此外需要说明的是,涉及到距离计算的,一定要注意量纲的统一。如果量纲不统一的话,模型极易偏向量纲大的那一方。

函数功能:计算两个数据集之间的欧式距离 输入:两个 array 数据集 返回:两个数据集之间的欧式距离(此处用距离平方和代替距离)

def distEclud(arrA, arrB):
    d = arrA - arrB
    dist = np.sum(np.power(d, 2), axis=1)
    return dist

编写随机函数生成质心函数

在定义随机质心生成函数时,需要按照以下步骤进行操作:

  1. 数据范围计算:首先遍历数据集中的每一列(特征),计算该列的最小值(min)和最大值(max)
  2. 随机质心生成:根据用户指定的簇个数k,生成k个质心
  3. 参数说明:
    • 输入参数:dataSet(包含标签的完整数据集),k(需要生成的质心数量)
    • 输出参数:data_cent(生成的k个质心)
def randCent(dataSet, k):
    # n为列数,假设dataSet是一个DataFrame
    n = dataSet.shape[1]

    # 获取每一列的最小值和最大值(仅使用前 n-1 列,最后一列是标签或类别)
    data_min = dataSet.iloc[:, :n-1].min()
    data_max = dataSet.iloc[:, :n-1].max()

    # 在最小值和最大值之间生成 k 个随机中心点
    data_cent = np.random.uniform(data_min, data_max, (k, n-1))

    return data_cent

编写 K-Means 聚类函数

def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):
    # 获取数据集的维度,m 是行数,n 是列数
    m, n = dataSet.shape

    # 初始化质心 centroids,生成 k 个随机质心
    centroids = createCent(dataSet, k)

    # 初始化 clusterAssment 矩阵
    clusterAssment = np.zeros((m, 3))
    clusterAssment[:, 0] = np.inf
    clusterAssment[:, 1:3] = -1

    # 将数据集和 clusterAssment 合并,形成 result_set
    result_set = pd.concat([dataSet, pd.DataFrame(clusterAssment)], axis=1, ignore_index=True)

    # 标记簇是否发生变化
    clusterChanged = True

    while clusterChanged:
        clusterChanged = False
        # 遍历每个样本点
        for i in range(m):
            # 计算当前数据点到所有质心的距离
            dist = distMeas(dataSet.iloc[i, :n-1].values, centroids)
            # 记录最小距离和对应质心的索引
            result_set.iloc[i, n] = dist.min()
            result_set.iloc[i, n+1] = np.where(dist == dist.min())[0][0]

        # 检查当前簇分配与上次是否完全一致
        clusterChanged = not (result_set.iloc[:, -1] == result_set.iloc[:, -2]).all()

        # 如果簇分配发生变化,则更新质心
        if clusterChanged:
            cent_df = result_set.groupby(n+1).mean()
            centroids = cent_df.iloc[:, :n-1].values
            result_set.iloc[:, -1] = result_set.iloc[:, -2]

    return centroids, result_set

错误速查

症状根因修复
读入 iris.txt 报错或为空路径不对/文件不在工作目录使用 os.getcwd()pd.read_csv 报错栈;使用绝对路径或把数据放到工作目录
iris.shape 显示列数不符合预期分隔符/编码问题导致整行进一列iris.head() 检查列是否被挤到一列;指定 sep=','
K-Means 迭代报 TypeError/could not convert string to float标签列是字符串,但参与了数值运算检查 dataSet.dtypesresult_set.groupby(...).mean() 只对数值特征列计算
质心数量变少(k=3 变成 2)或出现 NaN空簇:某簇没有样本被分配检查 result_set.iloc[:, n+1].value_counts();检测缺失簇并重置该簇质心
程序长时间不结束或”看似卡住”缺少 max_iterwhile 循环增加 max_iter 与容差阈值
每次运行结果差异大,难复现未设置随机种子randCent 前设置 np.random.seed(...)
聚类结果偏向某一维度,分簇不合理特征量纲不统一对比各列范围 min/max;先做标准化/归一化