【机器学习】k均值聚类

目录

一、引言

二、k均值聚类算法的原理

三、k均值聚类算法的步骤

四、k均值聚类算法可视化的Python代码实战

五、总结


一、引言

        在市场营销当中,通过观察,我们可以看到客户的大致年龄、消费行为等信息,但是我们却很难区分它们的类别,因此可以认为它们是没有标签的数据,在观察一幅图片时,我们可以看到图片中的各种颜色,每一种颜色对应着一种数据,但是没有预测的信息,即它们也是没有标签的数据。对于上面的任务,我们只有样本x,却没有其标签y,这样的任务也不再是通过样本x去预测y,而是通过样本的特征找到样本之间的关联。由于没有标签作为监督信号,这一过程也被称为无监督学习。本文将要讲解的k均值聚类(k-Means Clustering)算法就是一个无监督学习算法。它的目标是将数据集中的样本根据其特征分为几个类,使得每一类内部样本的特征都尽可能的相近,这样的任务通常称为聚类任务。作为最简单的聚类算法,k均值聚类算法在现实中有广泛的应用。下面,我们将要详细讲解k均值算法的原理,算法以及可视化的Python代码的实现。

二、k均值聚类算法的原理

(一)数学定义

       给定一组观测数据集 $X = \{x_1, x_2, ..., x_n\}$,其中每个观测 $x_i \in \mathbb{R}^p$ 是一个p维实向量,K均值聚类算法的目标是将数据集划分为k个簇 $C = \{C_1, C_2, ..., C_k\}$,以最小化簇内平方和(Within-Cluster Sum of Squares, WCSS):

                                                 $J = \sum_{i=1}^{k} \sum_{x \in C_i} ||x - \mu_i||^2$

其中,$\mu_i$是簇 $C_i$ 的中心(质心),定义为:

                                                         $\mu_i = \frac{1}{|C_i|} \sum_{x \in C_i} x$

(二) 目标函数

        K均值聚类是一个优化问题,目标是找到簇的划分和质心的位置,使得簇内平方和J最小。这是一个NP难问题,K均值算法采用迭代优化的方法,通常能找到一个局部最优解。

三、k均值聚类算法的步骤

(一)初始化阶段

选择k个初始簇中心(质心)。初始化方法对算法的最终结果有重要影响。常用的初始化方法有:

1. 随机选择法:从数据集中随机选择k个点作为初始质心。

2. K-means++:优化的初始化方法,通过加权概率选择相距较远的点作为初始质心。

(二)迭代优化阶段

重复执行以下两个步骤,直到满足收敛条件:

1. 分配阶段:将每个数据点分配到距离最近的质心所代表的簇。

   对于每个数据点 $x_i$,计算其与各质心的距离,并将其分配给距离最近的质心所在的簇:

                      $C_i^{(t)} = \{x_j : ||x_j - \mu_i^{(t)}||^2 \leq ||x_j - \mu_l^{(t)}||^2 \forall l, 1 \leq l \leq k\}$

   其中,$\mu_i^{(t)}$ 是第 $t$ 轮迭代中第 $i$ 个簇的质心。

2. 更新阶段:重新计算每个簇的质心。

   对于每个簇 $C_i$,计算其新的质心:

                                                         $\mu_i^{(t+1)} = \frac{1}{|C_i^{(t)}|} \sum_{x_j \in C_i^{(t)}} x_j$

(三)收敛条件

算法迭代直到满足以下任一条件:

1. 质心不再显著变化(质心移动距离小于预设阈值)。

2. 数据点的簇分配不再改变。

3. 达到预设的最大迭代次数。

(四) K-means++初始化

K-means++是一种改进的初始化方法,旨在选择更优的初始质心,从而提高聚类结果的质量和算法的收敛速度。算法步骤如下:

1. 随机选择第一个质心 $\mu_1$

2. 计算每个点 $x_i$ 到当前已选质心集合的最短距离 $D(x_i)$

3. 基于概率 $\frac{D(x_i)^2}{\sum_{j=1}^{n} D(x_j)^2}$ 选择下一个质心,使得距离现有质心较远的点更可能被选为下一个质心。

4. 重复步骤2和3,直到选择了 $K$ 个质心。

这种方法倾向于选择彼此间距较远的点作为初始质心,避免了随机初始化可能导致的不良聚类结果。

(五) k均值聚类算法的优缺点

优点:

1. 简洁高效:算法思想简单,实现容易,计算复杂度较低。

2. 扩展性好:可处理大规模数据集。

3. 适用广泛:应用于各种领域的数据分析任务。

缺点:

1. 需要预先确定簇数k:在实际应用中,合适的簇数通常不知道。

2. 对异常值敏感:异常值会显著影响簇质心的计算。

3. 结果依赖初始质心:不同的初始质心可能导致不同的聚类结果。

4. 倾向于发现球状簇:不适合发现复杂形状的簇。

5. 容易陷入局部最优:最终结果可能不是全局最优解。

(六) 算法复杂度

1.时间复杂度:O(t·k·n·d),其中t是迭代次数,k是簇数,n是数据点数,d是维度。

2.空间复杂度:O(k+n),需要存储k个质心和n个数据点的簇标签。

四、k均值聚类算法可视化的Python代码实战

(一)Python代码实战的步骤详解

1. 数据生成与预处理

代码首先实现了多种数据生成方法,以便于测试和演示K均值聚类算法:

(1) 随机数据生成:使用generate_cluster_data函数基于scikit-learn的make_blobs函数生成可控制的聚类数据。

(2) 自定义分布生成:generate_custom_clusters函数生成特定形状的数据(如十字形分布)。

(3) 数据保存与加载:save_to_csv和load_from_csv函数用于数据的持久化存储和读取。

2. k均值聚类核心实现

k均值聚类的核心实现在KMeansClusterer类中,包含以下主要方法:

(1) 距离计算:_compute_distance方法计算数据点到质心的欧几里得距离矩阵:

  def _compute_distance(self, X, centroids):

       return cdist(X, centroids, 'euclidean')

  (2) 簇内平方和(SSE)计算:_compute_sse方法计算当前聚类的误差:

 def _compute_sse(self, X, labels, centroids):

       sse = 0.0

       for i in range(self.n_clusters):

           cluster_points = X[labels == i]

           if len(cluster_points) > 0:

               sse += np.sum(np.square(cdist(cluster_points, centroids[i].reshape(1, -1))))

       return sse

  (3) 质心初始化:

 a.随机初始化:_init_random方法随机选择数据点作为初始质心。

 b.K-means++初始化:_init_kmeans_plus_plus方法实现了K-means++初始化策略。

(4) 聚类过程:fit方法实现了迭代聚类过程,每次迭代都包含:

a.分配数据点到最近的质心。

b.重新计算每个簇的质心。

c. 计算SSE并检查收敛条件。

3.可视化实现

代码实现了多种可视化方法,以帮助理解K均值聚类的工作原理:

(1) 基础聚类可视化:show_cluster函数展示聚类结果,支持2D和3D数据:

 a.  2D数据使用Matplotlib绘制散点图。

 b.3D数据使用Plotly创建交互式3D散点图。

(2) 聚类过程动态可视化:visualize_kmeans_animation函数展示算法迭代过程:

   a.2D数据使用Matplotlib的FuncAnimation创建动画。

   b.3D数据使用Plotly的Frames机制创建交互式动画,支持播放、暂停和帧滑动。

(3) 初始化方法对比可视化:visualize_initialization_comparison函数比较随机初始化和K-means++初始化的效果:

a.质心轨迹对比。

b.聚类结果对比。

c.SSE收敛速度对比。

(4) 簇内误差分布可视化:visualize_cluster_errors函数展示聚类误差分布:

a.使用点大小表示误差大小。

b.提供误差直方图和箱线图。

4.详细算法步骤解析

4.1 K-means++初始化算法在代码中的实现如下:

def _init_kmeans_plus_plus(self, X):

    n_samples, n_features = X.shape

    centroids = np.zeros((self.n_clusters, n_features))

    

    # 随机选择第一个中心点

    first_idx = np.random.choice(n_samples)

    centroids[0] = X[first_idx].copy()

    

    # 选择剩余的中心点

    for c in range(1, self.n_clusters):

        # 计算每个点到已有中心点的最小距离

        dist_sq = np.min(self._compute_distance(X, centroids[:c]), axis=1) ** 2

        # 将距离平方作为概率权重

        probs = dist_sq / dist_sq.sum()

        # 按概率选择下一个中心点

        cumulative_probs = np.cumsum(probs)

        r = np.random.rand()

        for j, p in enumerate(cumulative_probs):

            if r < p:

                centroids[c] = X[j].copy()

                break

    

    return centroids

该算法的数学步骤为:

(1) 随机选择第一个质心$\mu_1$

(2) 对于每个数据点 $x_i$,计算它到最近已选质心的距离:$D(x_i) = \min_{j \in \{1,2,...,c-1\}} ||x_i - \mu_j||$

(3) 计算选择概率:$P(x_i) = \frac{D(x_i)^2}{\sum_{j=1}^{n} D(x_j)^2}$

(4) 按照概率 $P(x_i)$ 选择下一个质心。

(5) 重复步骤2-4直到选择了 $K$ 个质心。

4.2 K均值聚类迭代过程详解

迭代聚类过程在fit方法中实现:

def fit(self, X):

    # 初始化聚类中心

    self.centroids = self._init_centroids(X)

    previous_centroids = None

    self.history = []  # 清空历史记录

    self.sse_history = []  # 清空SSE历史

    

    # 迭代优化

    for iter_num in range(self.max_iters):

        # 分配样本到簇

        distances = self._compute_distance(X, self.centroids)

        self.labels = np.argmin(distances, axis=1)

        

        # 更新聚类中心

        previous_centroids = self.centroids.copy()

        for i in range(self.n_clusters):

            cluster_points = X[self.labels == i]

            if len(cluster_points) > 0:

                self.centroids[i] = np.mean(cluster_points, axis=0)

        

        # 计算当前SSE

        current_sse = self._compute_sse(X, self.labels, self.centroids)

        self.sse_history.append(current_sse)

        

        # 保存当前状态到历史记录

        self.history.append({

            'centroids': self.centroids.copy(),

            'labels': self.labels.copy(),

            'sse': current_sse

        })

        

        # 判断是否收敛

        centroid_shift = np.linalg.norm(self.centroids - previous_centroids)

        if centroid_shift < self.tol:

            break

该过程对应的数学步骤为:

(1) 初始化:选择 $K$ 个初始质心 $\mu_1^{(0)}, \mu_2^{(0)}, ..., \mu_K^{(0)}$

(2) 迭代:对于第 $t$ 次迭代:

  分配:对每个点 $x_i$,计算到各质心的距离 $d(x_i, \mu_k^{(t-1)})$,将 $x_i$ 分配到最近的质心所代表的簇:

                    $C_k^{(t)} = \{x_i : ||x_i - \mu_k^{(t-1)}||^2 \leq ||x_i - \mu_j^{(t-1)}||^2 \; \forall j, 1 \leq j \leq K\}$

   更新:重新计算每个簇的质心:

                                                    $\mu_k^{(t)} = \frac{1}{|C_k^{(t)}|} \sum_{x_i \in C_k^{(t)}} x_i$

   评估:计算簇内平方和误差(SSE):

                                                     $J^{(t)} = \sum_{k=1}^{K} \sum_{x_i \in C_k^{(t)}} ||x_i - \mu_k^{(t)}||^2$

   收敛检查:如果质心变化小于阈值 $\epsilon$,即 $||\mu_k^{(t)} - \mu_k^{(t-1)}|| < \epsilon \; \forall k$,则停止迭代

4.3 可视化技术详解

4.3.1 3D聚类可视化(Plotly实现)

3D聚类结果可视化使用Plotly实现,关键代码如下:

# 创建Plotly图形

fig = go.Figure()



# 添加每个簇的数据点

for i, cls in enumerate(clusters):

    cluster_points = dataset[cluster == cls]

    fig.add_trace(go.Scatter3d(

        x=cluster_points[:, 0],

        y=cluster_points[:, 1],

        z=cluster_points[:, 2],

        mode='markers',

        marker=dict(

            size=5,

            color=colors[i % len(colors)],

            opacity=0.7

        ),

        name=f'簇 {cls+1}'

    ))



# 添加聚类中心

if centroids is not None:

    fig.add_trace(go.Scatter3d(

        x=centroids[:, 0],

        y=centroids[:, 1],

        z=centroids[:, 2],

        mode='markers',

        marker=dict(

            size=10,

            color='black',

            symbol='x'

        ),

        name='聚类中心'

    ))

4.3.2 聚类过程动态可视化(Plotly实现)

3D聚类过程的动态可视化使用Plotly的Frames功能实现,关键代码如下:

# 创建一个包含所有帧的Plotly动画

frames = []



# 准备数据

for i, state in enumerate(kmeans.history):

    frame_data = []

    centroids = state['centroids']

    labels = state['labels']

    

    # 添加各个簇的点和质心

    for cls in range(kmeans.n_clusters):

        cluster_points = X[labels == cls]

        if len(cluster_points) > 0:

            frame_data.append(go.Scatter3d(...))

    

    # 添加质心

    frame_data.append(go.Scatter3d(...))

    

    # 创建帧

    frames.append(go.Frame(

        data=frame_data,

        name=f'frame{i}'

    ))



# 创建初始视图

fig = go.Figure(

    data=frames[0].data,

    frames=frames

)



# 添加播放控件和滑动条

fig.update_layout(

    updatemenus=[{

        'type': 'buttons',

        'buttons': [

            {'label': '播放', 'method': 'animate', ...},

            {'label': '暂停', 'method': 'animate', ...}

        ]

    }],

    sliders=[{

        'steps': [

            {'method': 'animate', 'label': f'{i+1}', ...}

            for i in range(len(frames))

        ]

    }]

)

4.4算法执行流程

完整的算法执行流程包含以下步骤:

(1) 数据准备:生成或加载聚类数据。

(2)基本聚类:使用随机初始化执行K均值聚类。

(3) 改进聚类:使用K-means++初始化执行K均值聚类。

(4) 可视化分析:

   a.对比随机初始化与K-means++初始化的效果。

  b.动态可视化聚类过程。

  c.分析聚类误差分布。

4.5. 3D数据分析:对3D数据执行K均值聚类并进行可视化。

整个流程在main函数中实现:

def main():

    # 1. 数据准备

    X_2d, y_2d = generate_custom_clusters()

    save_to_csv(X_2d, 'kmeans_data.csv')

    dataset = load_from_csv('kmeans_data.csv')

    

    # 2. 基本聚类

    kmeans_random = KMeansClusterer(n_clusters=4, init_method='random', random_state=42)

    kmeans_random.fit(dataset)

    show_cluster(dataset, kmeans_random.labels, kmeans_random.centroids, title="K均值聚类结果 (随机初始化)")

    

    # 3. 改进聚类

    kmeans_pp = KMeansClusterer(n_clusters=4, init_method='k-means++', random_state=42)

    kmeans_pp.fit(dataset)

    show_cluster(dataset, kmeans_pp.labels, kmeans_pp.centroids, title="K均值聚类结果 (K-means++初始化)")

    

    # 4. 可视化分析

    visualize_initialization_comparison(dataset, k=4)

    visualize_kmeans_animation(dataset, kmeans_pp, interval=1000)

    visualize_cluster_errors(dataset, kmeans_pp)

    

    # 5. 3D数据分析

    X_3d, y_3d = generate_cluster_data(n_samples=500, n_features=3, centers=4, cluster_std=0.8)

    kmeans_3d = KMeansClusterer(n_clusters=4, init_method='k-means++', random_state=42)

    kmeans_3d.fit(X_3d)

    show_cluster(X_3d, kmeans_3d.labels, kmeans_3d.centroids, title="3D K均值聚类结果")

    visualize_kmeans_animation(X_3d, kmeans_3d, interval=1000)

(二)Python代码的实现

先运行"随机生成k均值聚类数据集.py"的代码,再运行"k均值聚类.py"的代码,"随机生成k均值聚类数据集.py"文件与"k均值聚类.py"文件应位于同一目录中。

随机生成k均值聚类数据集.py

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs

# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

def generate_cluster_data(n_samples=500, n_features=2, centers=4, cluster_std=1.0, random_state=42):
    """
    生成适合K均值聚类的模拟数据集

    参数:
    n_samples (int): 样本总数
    n_features (int): 特征维度,默认2维便于可视化
    centers (int): 生成的簇数量
    cluster_std (float): 簇内数据的标准差,控制簇的紧密程度
    random_state (int): 随机种子,保证结果可复现

    返回:
    X (ndarray): 生成的特征数据,形状为(n_samples, n_features)
    y (ndarray): 真实的簇标签,形状为(n_samples,)
    """
    print(f"生成数据集: {n_samples}个样本, {n_features}维特征, {centers}个簇...")
    X, y = make_blobs(n_samples=n_samples,
                      n_features=n_features,
                      centers=centers,
                      cluster_std=cluster_std,
                      random_state=random_state)

    return X, y


def generate_custom_clusters():
    """
    生成一个包含4个明显分散的簇的二维数据集

    返回:
    X: 形状为(500, 2)的numpy数组,表示500个二维数据点
    y: 簇标签
    """
    # 设置随机种子以保证结果可复现
    np.random.seed(42)

    # 簇中心点坐标
    centers = [
        [3, 3],  # 第一个簇中心
        [-3, 3],  # 第二个簇中心
        [-3, -3],  # 第三个簇中心
        [3, -3]  # 第四个簇中心
    ]

    # 每个簇的样本数
    n_samples_per_cluster = 125

    # 簇的标准差(控制簇的紧密程度)
    cluster_std = 0.8

    # 创建空数组存储所有数据点和标签
    X = np.zeros((n_samples_per_cluster * len(centers), 2))
    y = np.zeros(n_samples_per_cluster * len(centers), dtype=int)

    # 生成每个簇的数据点
    for i, (cx, cy) in enumerate(centers):
        # 生成正态分布的点,围绕簇中心
        start_idx = i * n_samples_per_cluster
        end_idx = start_idx + n_samples_per_cluster

        # 簇内x坐标的正态分布
        X[start_idx:end_idx, 0] = np.random.normal(cx, cluster_std, n_samples_per_cluster)
        # 簇内y坐标的正态分布
        X[start_idx:end_idx, 1] = np.random.normal(cy, cluster_std, n_samples_per_cluster)
        # 设置簇标签
        y[start_idx:end_idx] = i

    # 随机打乱数据点顺序
    indices = np.arange(len(X))
    np.random.shuffle(indices)
    X = X[indices]
    y = y[indices]

    print(f"生成数据集: 500个样本, 2维特征, 4个簇...")
    return X, y


def save_to_csv(X, filename='kmeans_data.csv'):
    """
    将生成的数据保存为CSV文件

    参数:
    X (ndarray): 特征数据
    filename (str): 保存文件名
    """
    # 保存为CSV
    np.savetxt(filename, X, delimiter=',')
    print(f"数据已保存至: {filename}")
    return True


def load_from_csv(filename='kmeans_data.csv'):
    """
    从CSV文件加载数据

    参数:
    filename (str): 数据文件名

    返回:
    X (ndarray): 加载的数据
    """
    try:
        dataset = np.loadtxt(filename, delimiter=',')
        print(f'成功加载数据集: {filename}, 大小: {len(dataset)}')
        return dataset
    except Exception as e:
        print(f"加载数据失败: {e}")
        return None


def visualize_data(X, y=None, title="生成的聚类数据"):
    """
    可视化生成的数据集

    参数:
    X: 要可视化的数据集
    y: 簇标签(可选)
    title: 图表标题
    """
    plt.figure(figsize=(10, 8))

    if y is not None:
        # 如果提供了标签,使用不同的颜色表示不同的簇
        colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']
        for i in np.unique(y):
            plt.scatter(
                X[y == i, 0], X[y == i, 1],
                s=50, alpha=0.8,
                c=colors[i % len(colors)],
                label=f'簇 {i + 1}'
            )
        plt.legend()
    else:
        # 否则使用单一颜色
        plt.scatter(X[:, 0], X[:, 1], s=50, alpha=0.8, c='skyblue', edgecolors='gray')

    plt.title(title, fontsize=15)
    plt.xlabel('特征1', fontsize=12)
    plt.ylabel('特征2', fontsize=12)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    # 生成方法1:使用sklearn的make_blobs生成数据
    X1, y1 = generate_cluster_data(n_samples=500, n_features=2, centers=4, cluster_std=0.8)
    visualize_data(X1, y1, "使用make_blobs生成的聚类数据")

    # 生成方法2:使用自定义分布生成数据
    X2, y2 = generate_custom_clusters()
    visualize_data(X2, y2, "使用自定义方法生成的聚类数据")

    # 保存数据(这里选择保存自定义生成的数据)
    save_to_csv(X2, 'kmeans_data.csv')

    # 测试加载数据
    loaded_data = load_from_csv('kmeans_data.csv')
    if loaded_data is not None:
        visualize_data(loaded_data, title="从CSV加载的数据")

k均值聚类.py

import os
import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from mpl_toolkits.mplot3d import Axes3D  # 3D绘图必须导入
import pandas as pd
from scipy.spatial.distance import cdist
import time
import warnings

# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

warnings.filterwarnings('ignore')  # 忽略警告信息

# 导入Plotly用于3D可视化
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px

# 尝试导入数据生成模块
try:
    from 随机生成k均值聚类数据集 import generate_cluster_data, generate_custom_clusters
    from 随机生成k均值聚类数据集 import save_to_csv, load_from_csv, visualize_data

    data_module_imported = True
except ImportError:
    print("无法导入数据生成模块,将使用内置的数据生成函数")


    # 如果导入失败,定义简化的数据生成函数
    def generate_cluster_data(n_samples=500, n_features=2, centers=4, cluster_std=1.0, random_state=42):
        """简化版数据生成函数"""
        # 设置随机种子
        np.random.seed(random_state)

        # 生成簇中心
        if isinstance(centers, int):
            centers_coords = np.random.uniform(-5, 5, (centers, n_features))
        else:
            centers_coords = np.array(centers)
            centers = len(centers_coords)

        # 生成数据点
        X = np.zeros((n_samples, n_features))
        y = np.zeros(n_samples, dtype=int)
        samples_per_center = n_samples // centers

        for i, center in enumerate(centers_coords):
            start_idx = i * samples_per_center
            end_idx = start_idx + samples_per_center if i < centers - 1 else n_samples
            X[start_idx:end_idx] = np.random.normal(center, cluster_std, (end_idx - start_idx, n_features))
            y[start_idx:end_idx] = i

        # 随机打乱数据
        idx = np.random.permutation(n_samples)
        X, y = X[idx], y[idx]

        print(f"生成数据集: {n_samples}个样本, {n_features}维特征, {centers}个簇...")
        return X, y


    def generate_custom_clusters():
        """生成十字形分布的聚类数据"""
        # 设置随机种子
        np.random.seed(42)

        # 簇中心点坐标
        centers = [
            [3, 3],  # 第一个簇中心
            [-3, 3],  # 第二个簇中心
            [-3, -3],  # 第三个簇中心
            [3, -3]  # 第四个簇中心
        ]

        # 每个簇的样本数
        n_samples_per_cluster = 125

        # 簇的标准差
        cluster_std = 0.8

        # 生成数据
        X = np.zeros((n_samples_per_cluster * len(centers), 2))
        y = np.zeros(n_samples_per_cluster * len(centers), dtype=int)

        for i, (cx, cy) in enumerate(centers):
            start_idx = i * n_samples_per_cluster
            end_idx = start_idx + n_samples_per_cluster

            X[start_idx:end_idx, 0] = np.random.normal(cx, cluster_std, n_samples_per_cluster)
            X[start_idx:end_idx, 1] = np.random.normal(cy, cluster_std, n_samples_per_cluster)
            y[start_idx:end_idx] = i

        # 打乱顺序
        idx = np.random.permutation(len(X))
        X, y = X[idx], y[idx]

        print(f"生成数据集: 500个样本, 2维特征, 4个簇...")
        return X, y


    def save_to_csv(X, filename='kmeans_data.csv'):
        """保存数据到CSV文件"""
        np.savetxt(filename, X, delimiter=',')
        print(f"数据已保存至: {filename}")
        return True


    def load_from_csv(filename='kmeans_data.csv'):
        """从CSV文件加载数据"""
        try:
            dataset = np.loadtxt(filename, delimiter=',')
            print(f'成功加载数据集: {filename}, 大小: {len(dataset)}')
            return dataset
        except Exception as e:
            print(f"加载数据失败: {e}")
            return None


    def visualize_data(X, y=None, title="生成的聚类数据"):
        """可视化数据"""
        plt.figure(figsize=(10, 8))

        if y is not None:
            colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']
            for i in np.unique(y):
                plt.scatter(
                    X[y == i, 0], X[y == i, 1],
                    s=50, alpha=0.8,
                    c=colors[i % len(colors)],
                    label=f'簇 {i + 1}'
                )
            plt.legend()
        else:
            plt.scatter(X[:, 0], X[:, 1], s=50, alpha=0.8, c='skyblue', edgecolors='gray')

        plt.title(title, fontsize=15)
        plt.xlabel('特征1', fontsize=12)
        plt.ylabel('特征2', fontsize=12)
        plt.grid(True, linestyle='--', alpha=0.7)
        plt.tight_layout()
        plt.show()


    data_module_imported = False


# ==============================
# K均值聚类算法核心类
# ==============================

class KMeansClusterer:
    """
    K均值聚类算法的完整实现,支持可视化

    参数:
    n_clusters: 聚类簇数,默认为4
    max_iters: 最大迭代次数,默认为50
    init_method: 初始化方法,'random'或'k-means++'
    tol: 收敛阈值,当质心移动小于此值时认为收敛
    verbose: 是否打印过程信息
    random_state: 随机数种子
    """

    def __init__(self, n_clusters=4, max_iters=50, init_method='random', tol=1e-4, verbose=True, random_state=None):
        self.n_clusters = n_clusters  # 簇数量
        self.max_iters = max_iters  # 最大迭代次数
        self.init_method = init_method  # 初始化方法
        self.tol = tol  # 收敛阈值
        self.verbose = verbose  # 是否输出详细信息
        self.centroids = None  # 聚类中心点
        self.labels = None  # 样本标签
        self.inertia = None  # 聚类误差(簇内平方和)
        self.n_iter = 0  # 实际迭代次数
        self.history = []  # 迭代过程记录
        self.sse_history = []  # SSE历史记录

        # 设置随机种子
        if random_state is not None:
            np.random.seed(random_state)

    def _compute_distance(self, X, centroids):
        """计算样本点到聚类中心的距离"""
        return cdist(X, centroids, 'euclidean')

    def _compute_sse(self, X, labels, centroids):
        """计算簇内平方和误差(Sum of Squared Errors, SSE)"""
        sse = 0.0
        for i in range(self.n_clusters):
            cluster_points = X[labels == i]
            if len(cluster_points) > 0:
                sse += np.sum(np.square(cdist(cluster_points, centroids[i].reshape(1, -1))))
        return sse

    # ==== 初始化方法 ====

    def _init_random(self, X):
        """随机初始化聚类中心"""
        idx = np.random.choice(X.shape[0], self.n_clusters, replace=False)
        return X[idx].copy()

    def _init_kmeans_plus_plus(self, X):
        """K-means++初始化聚类中心

        原理:
        1. 随机选择第一个聚类中心
        2. 计算每个点到当前所有聚类中心的最小距离
        3. 按照距离的平方作为概率权重选择下一个聚类中心
        4. 重复步骤2-3直到选择了k个聚类中心

        这种方法倾向于选择彼此间距较远的聚类中心,通常能减少迭代次数和提高聚类质量
        """
        n_samples, n_features = X.shape
        centroids = np.zeros((self.n_clusters, n_features))

        # 随机选择第一个中心点
        first_idx = np.random.choice(n_samples)
        centroids[0] = X[first_idx].copy()

        # 选择剩余的中心点
        for c in range(1, self.n_clusters):
            # 计算每个点到已有中心点的最小距离
            dist_sq = np.min(self._compute_distance(X, centroids[:c]), axis=1) ** 2
            # 将距离平方作为概率权重
            probs = dist_sq / dist_sq.sum()
            # 按概率选择下一个中心点
            cumulative_probs = np.cumsum(probs)
            r = np.random.rand()
            for j, p in enumerate(cumulative_probs):
                if r < p:
                    centroids[c] = X[j].copy()
                    break

        return centroids

    def _init_centroids(self, X):
        """根据指定方法初始化聚类中心"""
        if self.init_method.lower() == 'k-means++':
            if self.verbose:
                print("使用K-means++初始化聚类中心...")
            return self._init_kmeans_plus_plus(X)
        else:  # 默认随机初始化
            if self.verbose:
                print("使用随机方法初始化聚类中心...")
            return self._init_random(X)

    # ==== 核心聚类过程 ====

    def fit(self, X):
        """
        执行K均值聚类算法

        参数:
        X: 数据集,形状为(n_samples, n_features)

        返回:
        self: 训练好的聚类器
        """
        # 初始化聚类中心
        self.centroids = self._init_centroids(X)
        previous_centroids = None
        self.history = []  # 清空历史记录
        self.sse_history = []  # 清空SSE历史

        start_time = time.time()

        # 迭代优化
        for iter_num in range(self.max_iters):
            # 分配样本到簇
            distances = self._compute_distance(X, self.centroids)
            self.labels = np.argmin(distances, axis=1)

            # 更新聚类中心
            previous_centroids = self.centroids.copy()
            for i in range(self.n_clusters):
                cluster_points = X[self.labels == i]
                if len(cluster_points) > 0:
                    self.centroids[i] = np.mean(cluster_points, axis=0)

            # 计算当前SSE
            current_sse = self._compute_sse(X, self.labels, self.centroids)
            self.sse_history.append(current_sse)

            # 保存当前状态到历史记录
            self.history.append({
                'centroids': self.centroids.copy(),
                'labels': self.labels.copy(),
                'sse': current_sse
            })

            # 判断是否收敛
            centroid_shift = np.linalg.norm(self.centroids - previous_centroids)
            if self.verbose:
                print(f"迭代 {iter_num + 1}: SSE = {current_sse:.4f}, 质心移动距离 = {centroid_shift:.6f}")

            if centroid_shift < self.tol:
                if self.verbose:
                    print(f"算法在第{iter_num + 1}次迭代后收敛")
                break

        # 记录迭代次数和最终SSE
        self.n_iter = iter_num + 1
        self.inertia = self.sse_history[-1]

        end_time = time.time()
        if self.verbose:
            print(f"K均值聚类完成,用时 {end_time - start_time:.4f} 秒")
            print(f"总迭代次数: {self.n_iter}, 最终SSE: {self.inertia:.4f}")

        return self

    def predict(self, X):
        """
        预测新数据的簇分配

        参数:
        X: 待预测数据,形状为(n_samples, n_features)

        返回:
        labels: 簇标签,形状为(n_samples,)
        """
        if self.centroids is None:
            raise ValueError("模型尚未训练,请先调用fit方法")

        distances = self._compute_distance(X, self.centroids)
        return np.argmin(distances, axis=1)

    def fit_predict(self, X):
        """
        训练模型并预测簇分配

        参数:
        X: 数据集,形状为(n_samples, n_features)

        返回:
        labels: 簇标签,形状为(n_samples,)
        """
        self.fit(X)
        return self.labels


# ==============================
# 基础可视化函数
# ==============================

def show_cluster(dataset, cluster, centroids=None, title=None, save_fig=False, filename=None):
    """
    可视化聚类结果

    参数:
    dataset: 数据集,形状为(n_samples, n_features)
    cluster: 簇标签,形状为(n_samples,)
    centroids: 聚类中心点坐标,如果为None则不显示
    title: 图表标题
    save_fig: 是否保存图像
    filename: 图像保存路径
    """
    # 获取数据维度
    n_dim = dataset.shape[1]

    # 不同种类的颜色和形状,用以区分划分的数据类别
    colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']
    markers = ['o', '^', 's', 'd', 'p', '*']

    if n_dim <= 2:  # 2D可视化 - 使用Matplotlib
        # 创建画布
        plt.figure(figsize=(10, 7))

        # 获取不同的簇
        clusters = np.unique(cluster)
        K = len(clusters)

        # 画出所有样例
        for i, cls in enumerate(clusters):
            plt.scatter(
                dataset[cluster == cls, 0],
                dataset[cluster == cls, 1] if n_dim > 1 else np.zeros(np.sum(cluster == cls)),
                s=50,
                c=colors[i % len(colors)],
                marker=markers[i % len(markers)],
                label=f'簇 {cls + 1}'
            )

        # 画出中心点
        if centroids is not None:
            plt.scatter(
                centroids[:, 0],
                centroids[:, 1] if n_dim > 1 else np.zeros(len(centroids)),
                s=200,
                c='black',
                marker='X',
                label='聚类中心'
            )

        plt.grid(True, linestyle='--', alpha=0.7)
        plt.legend()
        plt.title(title if title else f"K均值聚类结果 (k={K})")
        plt.tight_layout()

        if save_fig and filename:
            plt.savefig(filename, dpi=300, bbox_inches='tight')

        plt.show()

    elif n_dim == 3:  # 3D可视化 - 使用Plotly
        # 获取不同的簇
        clusters = np.unique(cluster)
        K = len(clusters)

        # 创建Plotly图形
        fig = go.Figure()

        # 添加每个簇的数据点
        for i, cls in enumerate(clusters):
            cluster_points = dataset[cluster == cls]
            fig.add_trace(go.Scatter3d(
                x=cluster_points[:, 0],
                y=cluster_points[:, 1],
                z=cluster_points[:, 2],
                mode='markers',
                marker=dict(
                    size=5,
                    color=colors[i % len(colors)],
                    symbol='circle',
                    opacity=0.7,
                    line=dict(width=0.5, color='white')
                ),
                name=f'簇 {cls + 1}'
            ))

        # 添加聚类中心
        if centroids is not None:
            fig.add_trace(go.Scatter3d(
                x=centroids[:, 0],
                y=centroids[:, 1],
                z=centroids[:, 2],
                mode='markers',
                marker=dict(
                    size=10,
                    color='black',
                    symbol='x',
                    line=dict(width=2, color='black')
                ),
                name='聚类中心'
            ))

        # 设置图形布局
        fig.update_layout(
            title=title if title else f"K均值聚类结果 (k={K})",
            scene=dict(
                xaxis_title='特征1',
                yaxis_title='特征2',
                zaxis_title='特征3',
                aspectmode='cube'
            ),
            legend=dict(
                yanchor="top",
                y=0.99,
                xanchor="left",
                x=0.01
            ),
            margin=dict(l=0, r=0, b=0, t=30)
        )

        if save_fig and filename:
            fig.write_image(filename, width=1000, height=800)

        fig.show()

    else:
        print("Error: 仅支持2D和3D数据可视化")
        return


# ==============================
# 高级可视化功能
# ==============================

def visualize_kmeans_animation(X, kmeans, interval=500, save_animation=False, filename=None):
    """
    动态可视化K均值聚类过程 - 支持2D和3D数据可视化

    参数:
    X: 数据集,形状为(n_samples, n_features)
    kmeans: 已训练的KMeansClusterer对象
    interval: 帧间隔时间(毫秒)- 仅用于Matplotlib
    save_animation: 是否保存动画
    filename: 动画保存路径
    """
    # 确保模型已训练
    if not kmeans.history:
        print("错误: 模型未训练或训练过程未记录历史数据")
        return

    # 获取数据维度
    n_dim = X.shape[1]
    if n_dim > 3:
        print("错误: 仅支持2D和3D数据可视化")
        return

    # 颜色和标记
    colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']
    markers = ['o', '^', 's', 'd', 'p', '*']

    if n_dim <= 2:  # 2D可视化 - 使用Matplotlib动画
        # 创建画布
        fig = plt.figure(figsize=(12, 8))

        # 创建布局: 上面是聚类过程,下面是SSE曲线
        ax_cluster = fig.add_subplot(211)  # 上半部分: 聚类过程
        ax_sse = fig.add_subplot(212)  # 下半部分: SSE曲线

        # 为SSE曲线设置轴
        max_sse = max(kmeans.sse_history) * 1.1
        min_sse = min(kmeans.sse_history) * 0.9
        ax_sse.set_xlim(0, len(kmeans.history))
        ax_sse.set_ylim(min_sse, max_sse)
        ax_sse.set_xlabel('迭代次数')
        ax_sse.set_ylabel('簇内平方和 (SSE)')
        ax_sse.set_title('收敛过程')
        ax_sse.grid(True, linestyle='--', alpha=0.7)

        # SSE线初始化
        line, = ax_sse.plot([], [], 'r-o', linewidth=2, alpha=0.8)
        iter_text = ax_sse.text(0.02, 0.95, '', transform=ax_sse.transAxes)

        # 聚类可视化区设置
        ax_cluster.set_xlim(np.min(X[:, 0]) - 1, np.max(X[:, 0]) + 1)
        if n_dim == 2:
            ax_cluster.set_ylim(np.min(X[:, 1]) - 1, np.max(X[:, 1]) + 1)
        ax_cluster.set_xlabel('特征1')
        ax_cluster.set_ylabel('特征2' if n_dim == 2 else '')
        ax_cluster.grid(True, linestyle='--', alpha=0.7)

        # 初始化散点和中心点
        if n_dim == 1:
            scatter = ax_cluster.scatter(X[:, 0], np.zeros(len(X)), s=50, c='gray', alpha=0.5)
            centroids_scatter = ax_cluster.scatter([], [], s=200, c='black', marker='X')
        else:  # n_dim == 2
            scatter = ax_cluster.scatter(X[:, 0], X[:, 1], s=50, c='gray', alpha=0.5)
            centroids_scatter = ax_cluster.scatter([], [], s=200, c='black', marker='X')

        # 设置标题
        ax_cluster.set_title(f"K均值聚类过程 ({kmeans.init_method}初始化, k={kmeans.n_clusters})")

        # 准备初始文本
        status_text = ax_cluster.text(0.02, 0.95, '', transform=ax_cluster.transAxes)

        # 先绘制一次基本框架,确保图像显示正确
        plt.tight_layout()
        plt.draw()

        # 更新函数
        def update(frame):
            artists = []

            if frame < len(kmeans.history):
                # 获取当前状态
                current = kmeans.history[frame]
                centroids = current['centroids']
                labels = current['labels']
                sse = current['sse']

                # 更新散点标签颜色
                scatter.set_color([colors[l % len(colors)] for l in labels])
                centroids_scatter.set_offsets(
                    centroids if n_dim == 2 else np.column_stack([centroids[:, 0], np.zeros(len(centroids))]))
                status_text.set_text(f'迭代: {frame + 1}, SSE: {sse:.4f}')
                artists.extend([scatter, centroids_scatter, status_text])

                # 更新SSE曲线
                x_data = range(frame + 1)
                y_data = kmeans.sse_history[:frame + 1]
                line.set_data(x_data, y_data)
                iter_text.set_text(f'总迭代: {frame + 1}/{len(kmeans.history)}')

                artists.extend([line, iter_text])

            return artists

        # 创建动画,确保动画不被垃圾回收
        anim = FuncAnimation(fig, update, frames=len(kmeans.history), interval=interval, blit=True)

        # 保存动画
        if save_animation and filename:
            anim.save(filename, writer='pillow', fps=30)

        plt.tight_layout()
        plt.show()

        return anim

    elif n_dim == 3:  # 3D可视化 - 使用Plotly动态图表
        # 创建一个包含所有帧的Plotly动画
        frames = []

        # 准备数据
        for i, state in enumerate(kmeans.history):
            frame_data = []
            centroids = state['centroids']
            labels = state['labels']
            sse = state['sse']

            # 添加各个簇的点
            for cls in range(kmeans.n_clusters):
                cluster_points = X[labels == cls]
                if len(cluster_points) > 0:
                    frame_data.append(
                        go.Scatter3d(
                            x=cluster_points[:, 0],
                            y=cluster_points[:, 1],
                            z=cluster_points[:, 2],
                            mode='markers',
                            marker=dict(
                                size=5,
                                color=colors[cls % len(colors)],
                                opacity=0.7
                            ),
                            name=f'簇 {cls + 1}',
                            showlegend=(i == 0)  # 只在第一帧显示图例
                        )
                    )

            # 添加质心
            frame_data.append(
                go.Scatter3d(
                    x=centroids[:, 0],
                    y=centroids[:, 1],
                    z=centroids[:, 2],
                    mode='markers',
                    marker=dict(
                        size=10,
                        color='black',
                        symbol='x',
                        line=dict(width=2, color='black')
                    ),
                    name='聚类中心',
                    showlegend=(i == 0)  # 只在第一帧显示图例
                )
            )

            # 创建帧
            frames.append(go.Frame(
                data=frame_data,
                name=f'frame{i}',
                traces=list(range(kmeans.n_clusters + 1))  # 指定要更新的轨迹
            ))

        # 创建初始视图(使用第一帧的数据)
        fig = go.Figure(
            data=frames[0].data,
            frames=frames
        )

        # 添加标题和布局
        fig.update_layout(
            title=f"K均值聚类过程 ({kmeans.init_method}初始化, k={kmeans.n_clusters})",
            scene=dict(
                xaxis_title='特征1',
                yaxis_title='特征2',
                zaxis_title='特征3'
            ),
            updatemenus=[{
                'type': 'buttons',
                'buttons': [
                    {
                        'label': '播放',
                        'method': 'animate',
                        'args': [None, {'frame': {'duration': 500, 'redraw': True}, 'fromcurrent': True}]
                    },
                    {
                        'label': '暂停',
                        'method': 'animate',
                        'args': [[None], {'frame': {'duration': 0, 'redraw': True}, 'mode': 'immediate',
                                          'transition': {'duration': 0}}]
                    }
                ],
                'direction': 'left',
                'pad': {'r': 10, 't': 10},
                'x': 0.1,
                'y': 0,
                'xanchor': 'right',
                'yanchor': 'top'
            }]
        )

        # 添加滑动条以手动浏览帧
        fig.update_layout(
            sliders=[{
                'active': 0,
                'yanchor': 'top',
                'xanchor': 'left',
                'currentvalue': {
                    'prefix': '迭代: ',
                    'xanchor': 'right'
                },
                'pad': {'b': 10, 't': 50},
                'len': 0.9,
                'x': 0.1,
                'y': 0,
                'steps': [
                    {
                        'method': 'animate',
                        'label': f'{i + 1}',
                        'args': [
                            [f'frame{i}'],
                            {'frame': {'duration': 300, 'easing': 'cubic-in-out', 'redraw': True},
                             'transition': {'duration': 300}}
                        ]
                    }
                    for i in range(len(frames))
                ]
            }]
        )

        # 添加SSE曲线子图
        sse_values = np.array([h['sse'] for h in kmeans.history])
        iterations = np.arange(1, len(sse_values) + 1)

        # 创建带有SSE曲线的新图形
        fig2 = make_subplots(
            rows=2, cols=1,
            subplot_titles=(f"K均值聚类过程 ({kmeans.init_method}初始化, k={kmeans.n_clusters})", "收敛过程 (SSE)"),
            specs=[[{"type": "scene"}], [{"type": "xy"}]],
            row_heights=[0.7, 0.3]
        )

        # 将3D聚类数据复制到新图形
        for trace in fig.data:
            fig2.add_trace(trace, row=1, col=1)

        # 添加SSE曲线
        fig2.add_trace(
            go.Scatter(
                x=iterations,
                y=sse_values,
                mode='lines+markers',
                name='SSE',
                marker=dict(color='red'),
                line=dict(color='red', width=2)
            ),
            row=2, col=1
        )

        # 移植帧数据到新图形
        fig2.frames = fig.frames

        # 更新布局
        fig2.update_layout(
            scene=dict(
                xaxis_title='特征1',
                yaxis_title='特征2',
                zaxis_title='特征3'
            ),
            xaxis=dict(title='迭代次数'),
            yaxis=dict(title='簇内平方和 (SSE)'),
            updatemenus=[{
                'type': 'buttons',
                'buttons': [
                    {
                        'label': '播放',
                        'method': 'animate',
                        'args': [None, {'frame': {'duration': 500, 'redraw': True}, 'fromcurrent': True}]
                    },
                    {
                        'label': '暂停',
                        'method': 'animate',
                        'args': [[None], {'frame': {'duration': 0, 'redraw': True}, 'mode': 'immediate',
                                          'transition': {'duration': 0}}]
                    }
                ],
                'direction': 'left',
                'pad': {'r': 10, 't': 10},
                'x': 0.1,
                'y': 0,
                'xanchor': 'right',
                'yanchor': 'top'
            }],
            sliders=[{
                'active': 0,
                'yanchor': 'top',
                'xanchor': 'left',
                'currentvalue': {
                    'prefix': '迭代: ',
                    'xanchor': 'right'
                },
                'pad': {'b': 10, 't': 50},
                'len': 0.9,
                'x': 0.1,
                'y': 0,
                'steps': [
                    {
                        'method': 'animate',
                        'label': f'{i + 1}',
                        'args': [
                            [f'frame{i}'],
                            {'frame': {'duration': 300, 'easing': 'cubic-in-out', 'redraw': True},
                             'transition': {'duration': 300}}
                        ]
                    }
                    for i in range(len(frames))
                ]
            }],
            height=900,  # 增加图表高度以适应两个子图
            margin=dict(l=50, r=50, t=50, b=50)
        )

        # 保存为HTML文件
        if save_animation and filename:
            fig2.write_html(filename)

        fig2.show()

        return fig2
    else:
        print("错误: 仅支持2D和3D数据可视化")
        return None


def visualize_initialization_comparison(X, k=4, random_state=42):
    """
    可视化比较随机初始化和K-means++初始化的效果

    参数:
    X: 数据集,形状为(n_samples, n_features)
    k: 聚类簇数
    random_state: 随机种子
    """
    # 获取数据维度
    n_dim = X.shape[1]
    if n_dim > 3:
        print("错误: 仅支持2D和3D数据可视化")
        return

    # 设置随机种子
    np.random.seed(random_state)

    # 创建两个KMeans模型
    km_random = KMeansClusterer(n_clusters=k, init_method='random', random_state=random_state, verbose=False)
    km_pp = KMeansClusterer(n_clusters=k, init_method='k-means++', random_state=random_state, verbose=False)

    # 训练模型
    km_random.fit(X)
    km_pp.fit(X)

    # 计算最终结果
    print(f"{'初始化方法':<12} {'迭代次数':<12} {'最终SSE':<15} {'时间(秒)':<12}")
    print("-" * 55)
    print(
        f"{'随机初始化':<12} {km_random.n_iter:<12d} {km_random.inertia:<15.4f} {len(km_random.history) * 0.1:<12.4f}")
    print(f"{'K-means++':<12} {km_pp.n_iter:<12d} {km_pp.inertia:<15.4f} {len(km_pp.history) * 0.1:<12.4f}")

    if n_dim <= 2:  # 2D可视化 - 使用Matplotlib
        # 创建画布
        fig = plt.figure(figsize=(15, 10))

        # 创建子图
        # 上排: 迭代过程中的质心轨迹
        ax_random_path = fig.add_subplot(231)
        ax_pp_path = fig.add_subplot(232)
        ax_path_compare = fig.add_subplot(233)

        # 下排: 最终聚类结果和SSE对比
        ax_random = fig.add_subplot(234)
        ax_pp = fig.add_subplot(235)
        ax_sse = fig.add_subplot(236)

        # 颜色和标记
        colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']
        markers = ['o', '^', 's', 'd', 'p', '*']

        # 绘制SSE对比曲线
        ax_sse.plot(km_random.sse_history, 'r-o', linewidth=2, alpha=0.7, label='随机初始化')
        ax_sse.plot(km_pp.sse_history, 'b-^', linewidth=2, alpha=0.7, label='K-means++初始化')
        ax_sse.set_xlabel('迭代次数')
        ax_sse.set_ylabel('SSE')
        ax_sse.set_title('收敛速度对比')
        ax_sse.legend()
        ax_sse.grid(True, linestyle='--', alpha=0.7)

        # 绘制质心轨迹
        for i in range(k):
            # 随机初始化的轨迹
            points = np.array([h['centroids'][i] for h in km_random.history])
            ax_random_path.plot(points[:, 0], points[:, 1], 'o-', c=colors[i], alpha=0.7, linewidth=1.5)
            ax_random_path.scatter(points[-1, 0], points[-1, 1], s=100, c=colors[i], marker='X')

            # K-means++初始化的轨迹
            points_pp = np.array([h['centroids'][i] for h in km_pp.history])
            ax_pp_path.plot(points_pp[:, 0], points_pp[:, 1], 'o-', c=colors[i], alpha=0.7, linewidth=1.5)
            ax_pp_path.scatter(points_pp[-1, 0], points_pp[-1, 1], s=100, c=colors[i], marker='X')

            # 轨迹对比
            ax_path_compare.plot(points[:, 0], points[:, 1], 'r--', alpha=0.5, linewidth=1)
            ax_path_compare.plot(points_pp[:, 0], points_pp[:, 1], 'b-', alpha=0.5, linewidth=1)
            ax_path_compare.scatter(points[0, 0], points[0, 1], s=80, c='r', marker='o',
                                    label='随机初始' if i == 0 else "")
            ax_path_compare.scatter(points_pp[0, 0], points_pp[0, 1], s=80, c='b', marker='^',
                                    label='K-means++初始' if i == 0 else "")

        # 设置轨迹图的标题
        ax_random_path.set_title('随机初始化质心轨迹')
        ax_pp_path.set_title('K-means++初始化质心轨迹')
        ax_path_compare.set_title('质心轨迹对比')
        ax_path_compare.legend()

        # 绘制数据点
        for i in range(k):
            # 随机初始化的结果
            ax_random.scatter(
                X[km_random.labels == i, 0],
                X[km_random.labels == i, 1] if n_dim == 2 else np.zeros(np.sum(km_random.labels == i)),
                s=30, c=colors[i], marker=markers[i], alpha=0.7,
                label=f'簇{i + 1}' if i < k else None
            )

            # K-means++初始化的结果
            ax_pp.scatter(
                X[km_pp.labels == i, 0],
                X[km_pp.labels == i, 1] if n_dim == 2 else np.zeros(np.sum(km_pp.labels == i)),
                s=30, c=colors[i], marker=markers[i], alpha=0.7,
                label=f'簇{i + 1}' if i < k else None
            )

        # 绘制最终质心
        ax_random.scatter(km_random.centroids[:, 0], km_random.centroids[:, 1], s=150, c='black', marker='X',
                          label='质心')
        ax_pp.scatter(km_pp.centroids[:, 0], km_pp.centroids[:, 1], s=150, c='black', marker='X', label='质心')

        # 设置最终结果图的标题
        ax_random.set_title(f'随机初始化结果 (迭代{km_random.n_iter}次, SSE={km_random.inertia:.4f})')
        ax_pp.set_title(f'K-means++初始化结果 (迭代{km_pp.n_iter}次, SSE={km_pp.inertia:.4f})')

        # 添加图例
        ax_random.legend()
        ax_pp.legend()

        # 添加网格线
        ax_random.grid(True, linestyle='--', alpha=0.7)
        ax_pp.grid(True, linestyle='--', alpha=0.7)
        ax_random_path.grid(True, linestyle='--', alpha=0.7)
        ax_pp_path.grid(True, linestyle='--', alpha=0.7)
        ax_path_compare.grid(True, linestyle='--', alpha=0.7)

        plt.tight_layout()
        plt.draw()  # 确保绘图正常
        plt.show()

    elif n_dim == 3:  # 3D可视化 - 使用Plotly
        # 使用Plotly创建两个并排比较的3D图和一个SSE对比图
        # 创建子图布局
        fig = make_subplots(
            rows=2, cols=2,
            specs=[[{'type': 'scene'}, {'type': 'scene'}], [{'colspan': 2, 'type': 'xy'}]],
            subplot_titles=(
                f'随机初始化结果 (迭代{km_random.n_iter}次, SSE={km_random.inertia:.4f})',
                f'K-means++初始化结果 (迭代{km_pp.n_iter}次, SSE={km_pp.inertia:.4f})',
                '收敛速度对比'
            ),
            column_widths=[0.5, 0.5],
            row_heights=[0.7, 0.3],
            vertical_spacing=0.05
        )

        # 颜色列表
        colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']

        # 随机初始化结果
        for i in range(k):
            mask = km_random.labels == i
            if np.any(mask):
                fig.add_trace(
                    go.Scatter3d(
                        x=X[mask, 0],
                        y=X[mask, 1],
                        z=X[mask, 2],
                        mode='markers',
                        marker=dict(
                            size=5,
                            color=colors[i % len(colors)],
                            opacity=0.7
                        ),
                        name=f'簇{i + 1} (随机)'
                    ),
                    row=1, col=1
                )

        # 随机初始化的质心
        fig.add_trace(
            go.Scatter3d(
                x=km_random.centroids[:, 0],
                y=km_random.centroids[:, 1],
                z=km_random.centroids[:, 2],
                mode='markers',
                marker=dict(
                    size=10,
                    color='black',
                    symbol='x',
                    line=dict(width=2, color='black')
                ),
                name='质心 (随机)'
            ),
            row=1, col=1
        )

        # K-means++初始化结果
        for i in range(k):
            mask = km_pp.labels == i
            if np.any(mask):
                fig.add_trace(
                    go.Scatter3d(
                        x=X[mask, 0],
                        y=X[mask, 1],
                        z=X[mask, 2],
                        mode='markers',
                        marker=dict(
                            size=5,
                            color=colors[i % len(colors)],
                            opacity=0.7
                        ),
                        name=f'簇{i + 1} (K-means++)'
                    ),
                    row=1, col=2
                )

        # K-means++初始化的质心
        fig.add_trace(
            go.Scatter3d(
                x=km_pp.centroids[:, 0],
                y=km_pp.centroids[:, 1],
                z=km_pp.centroids[:, 2],
                mode='markers',
                marker=dict(
                    size=10,
                    color='black',
                    symbol='x',
                    line=dict(width=2, color='black')
                ),
                name='质心 (K-means++)'
            ),
            row=1, col=2
        )

        # SSE曲线对比
        fig.add_trace(
            go.Scatter(
                x=list(range(1, len(km_random.sse_history) + 1)),
                y=km_random.sse_history,
                mode='lines+markers',
                name='随机初始化 SSE',
                line=dict(color='red', width=2),
                marker=dict(color='red', size=8)
            ),
            row=2, col=1
        )

        fig.add_trace(
            go.Scatter(
                x=list(range(1, len(km_pp.sse_history) + 1)),
                y=km_pp.sse_history,
                mode='lines+markers',
                name='K-means++ SSE',
                line=dict(color='blue', width=2),
                marker=dict(color='blue', size=8)
            ),
            row=2, col=1
        )

        # 更新布局
        fig.update_layout(
            height=900,
            width=1200,
            scene=dict(
                xaxis_title='特征1',
                yaxis_title='特征2',
                zaxis_title='特征3'
            ),
            scene2=dict(
                xaxis_title='特征1',
                yaxis_title='特征2',
                zaxis_title='特征3'
            ),
            xaxis=dict(title='迭代次数'),
            yaxis=dict(title='簇内平方和 (SSE)'),
            title_text="随机初始化 vs. K-means++初始化对比"
        )

        # 显示图表
        fig.show()

    else:
        print("错误: 仅支持2D和3D数据可视化")
        return


def visualize_cluster_errors(X, kmeans):
    """
    可视化聚类结果中每个簇的误差分布

    参数:
    X: 数据集,形状为(n_samples, n_features)
    kmeans: 已训练的KMeansClusterer对象
    """
    # 确保模型已训练
    if kmeans.centroids is None:
        print("错误: 模型未训练")
        return

    # 获取数据维度
    n_dim = X.shape[1]

    # 计算每个点到其质心的距离
    labels = kmeans.labels
    centroids = kmeans.centroids
    distances = np.zeros(len(X))

    for i in range(kmeans.n_clusters):
        cluster_points = X[labels == i]
        if len(cluster_points) > 0:
            dist = cdist(cluster_points, centroids[i].reshape(1, -1))
            distances[labels == i] = dist.flatten()

    if n_dim <= 2:  # 2D可视化 - 使用Matplotlib
        # 创建图形
        fig = plt.figure(figsize=(15, 8))

        # 配置布局: 左侧数据可视化,右侧误差分布
        ax_data = fig.add_subplot(121)
        ax_hist = fig.add_subplot(222)  # 右上: 直方图
        ax_box = fig.add_subplot(224)  # 右下: 簇内误差箱线图

        # 颜色和标记
        colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']
        markers = ['o', '^', 's', 'd', 'p', '*']

        # 左侧: 数据可视化,点的大小反映误差
        max_distance = np.max(distances)
        size_scale = 50  # 基础大小

        for i in range(kmeans.n_clusters):
            cluster_points = X[labels == i]
            cluster_distances = distances[labels == i]

            ax_data.scatter(
                cluster_points[:, 0],
                cluster_points[:, 1] if n_dim == 2 else np.zeros(len(cluster_points)),
                s=size_scale + 100 * cluster_distances / max_distance,  # 误差越大,点越大
                c=colors[i % len(colors)],
                marker=markers[i % len(markers)],
                alpha=0.6,
                edgecolors='w',
                linewidths=0.5,
                label=f'簇 {i + 1}'
            )

        ax_data.scatter(
            centroids[:, 0],
            centroids[:, 1] if n_dim > 1 else np.zeros(len(centroids)),
            s=200,
            c='black',
            marker='X',
            label='质心'
        )

        # 右上: 误差直方图
        ax_hist.hist(distances, bins=30, alpha=0.7, color='skyblue', edgecolor='black')
        ax_hist.set_xlabel('到质心的距离')
        ax_hist.set_ylabel('样本数')
        ax_hist.set_title('误差分布直方图')
        ax_hist.grid(True, linestyle='--', alpha=0.7)

        # 右下: 簇内误差箱线图
        cluster_errors = []
        cluster_labels = []

        for i in range(kmeans.n_clusters):
            if np.any(labels == i):
                cluster_errors.append(distances[labels == i])
                cluster_labels.append(f'簇 {i + 1}')

        ax_box.boxplot(cluster_errors, labels=cluster_labels)
        ax_box.set_ylabel('到质心的距离')
        ax_box.set_title('各簇内误差分布箱线图')
        ax_box.grid(True, linestyle='--', alpha=0.7)

        # 数据图标题和图例
        ax_data.set_title('聚类结果(点大小表示误差)')
        ax_data.legend()
        ax_data.grid(True, linestyle='--', alpha=0.7)

        # 调整布局
        plt.tight_layout()
        plt.draw()  # 确保绘图正常
        plt.show()

    elif n_dim == 3:  # 3D可视化 - 使用Plotly
        # 创建Plotly子图布局
        fig = make_subplots(
            rows=2, cols=2,
            specs=[[{'type': 'scene', 'rowspan': 2}, {'type': 'xy'}],
                   [None, {'type': 'xy'}]],
            subplot_titles=(
                '聚类结果(点大小表示误差)',
                '误差分布直方图',
                '各簇内误差分布箱线图'
            ),
            column_widths=[0.6, 0.4],
            row_heights=[0.5, 0.5]
        )

        # 颜色列表
        colors = ['#4EACC5', '#FF9C34', '#4E9A06', '#9A4EAE', '#C53434', '#34C5FF']

        # 左侧: 3D数据可视化,点的大小反映误差
        max_distance = np.max(distances)

        # 添加簇中的点
        for i in range(kmeans.n_clusters):
            mask = labels == i
            cluster_points = X[mask]
            cluster_distances = distances[mask]

            if len(cluster_points) > 0:
                fig.add_trace(
                    go.Scatter3d(
                        x=cluster_points[:, 0],
                        y=cluster_points[:, 1],
                        z=cluster_points[:, 2],
                        mode='markers',
                        marker=dict(
                            size=5 + 15 * cluster_distances / max_distance,  # 误差越大,点越大
                            color=colors[i % len(colors)],
                            opacity=0.7,
                            symbol='circle',
                            line=dict(width=0.5, color='white')
                        ),
                        name=f'簇 {i + 1}'
                    ),
                    row=1, col=1
                )

        # 添加质心
        fig.add_trace(
            go.Scatter3d(
                x=centroids[:, 0],
                y=centroids[:, 1],
                z=centroids[:, 2],
                mode='markers',
                marker=dict(
                    size=10,
                    color='black',
                    symbol='x',
                    line=dict(width=2, color='black')
                ),
                name='质心'
            ),
            row=1, col=1
        )

        # 右上: 误差直方图
        fig.add_trace(
            go.Histogram(
                x=distances,
                nbinsx=30,
                marker_color='skyblue',
                marker_line=dict(color='black', width=1),
                opacity=0.7,
                name='误差分布'
            ),
            row=1, col=2
        )

        # 右下: 簇内误差箱线图
        cluster_errors = []
        cluster_labels = []
        for i in range(kmeans.n_clusters):
            if np.any(labels == i):
                cluster_errors.append(distances[labels == i])
                cluster_labels.append(f'簇 {i + 1}')

        fig.add_trace(
            go.Box(
                y=cluster_errors[0] if cluster_errors else [],
                name=cluster_labels[0] if cluster_labels else '',
                marker_color=colors[0 % len(colors)]
            ),
            row=2, col=2
        )

        for i in range(1, len(cluster_errors)):
            fig.add_trace(
                go.Box(
                    y=cluster_errors[i],
                    name=cluster_labels[i],
                    marker_color=colors[i % len(colors)]
                ),
                row=2, col=2
            )

        # 更新布局
        fig.update_layout(
            height=800,
            scene=dict(
                xaxis_title='特征1',
                yaxis_title='特征2',
                zaxis_title='特征3'
            ),
            xaxis=dict(title='到质心的距离'),
            yaxis=dict(title='样本数'),
            xaxis2=dict(title='簇'),
            yaxis2=dict(title='到质心的距离'),
            title_text='聚类误差分析',
            showlegend=True,
            legend=dict(
                orientation="h",
                yanchor="bottom",
                y=1.02,
                xanchor="right",
                x=1
            )
        )

        # 显示图表
        fig.show()

    else:
        print("错误: 仅支持2D和3D数据可视化")
        return


# ==============================
# 主程序
# ==============================

def main():
    """
    K均值聚类演示主程序
    """
    print("=" * 50)
    print("K均值聚类算法可视化演示")
    print("=" * 50)

    # --- 1. 数据准备 ---
    print("\n1. 数据准备")
    print("-" * 30)

    # 生成2D数据并保存
    X_2d, y_2d = generate_custom_clusters()
    save_to_csv(X_2d, 'kmeans_data.csv')

    # 从文件加载数据
    dataset = load_from_csv('kmeans_data.csv')

    # --- 2. 基本聚类和可视化 ---
    print("\n2. 基本K均值聚类")
    print("-" * 30)

    # 使用随机初始化方法
    print("\n使用随机初始化:")
    kmeans_random = KMeansClusterer(n_clusters=4, init_method='random', random_state=42)
    kmeans_random.fit(dataset)

    # 可视化聚类结果
    print("\n基本聚类结果可视化:")
    show_cluster(dataset, kmeans_random.labels, kmeans_random.centroids,
                 title="K均值聚类结果 (随机初始化)")

    # --- 3. 使用K-means++初始化 ---
    print("\n3. 使用K-means++初始化")
    print("-" * 30)

    kmeans_pp = KMeansClusterer(n_clusters=4, init_method='k-means++', random_state=42)
    kmeans_pp.fit(dataset)

    # 可视化聚类结果
    show_cluster(dataset, kmeans_pp.labels, kmeans_pp.centroids,
                 title="K均值聚类结果 (K-means++初始化)")

    # --- 4. 高级可视化 ---
    print("\n4. 高级可视化功能演示")
    print("-" * 30)

    # 4.1 随机初始化与K-means++初始化对比
    print("\n4.1 初始化方法对比可视化:")
    visualize_initialization_comparison(dataset, k=4)

    # 4.2 聚类过程动态可视化
    print("\n4.2 聚类过程动态可视化:")
    visualize_kmeans_animation(dataset, kmeans_pp, interval=1000)  # 增加了间隔时间,便于观察

    # 4.3 簇内误差分布可视化
    print("\n4.3 簇内误差分布可视化:")
    visualize_cluster_errors(dataset, kmeans_pp)

    # --- 5. 3D数据示例 ---
    print("\n5. 3D数据聚类演示")
    print("-" * 30)

    # 生成3D数据
    X_3d, y_3d = generate_cluster_data(n_samples=500, n_features=3, centers=4, cluster_std=0.8)

    # 3D聚类
    print("\n3D数据聚类:")
    kmeans_3d = KMeansClusterer(n_clusters=4, init_method='k-means++', random_state=42)
    kmeans_3d.fit(X_3d)

    # 可视化3D聚类结果
    print("\n3D聚类结果可视化:")
    show_cluster(X_3d, kmeans_3d.labels, kmeans_3d.centroids, title="3D K均值聚类结果")

    # 3D数据聚类过程动态可视化
    print("\n3D聚类过程动态可视化:")
    visualize_kmeans_animation(X_3d, kmeans_3d, interval=1000)


    print("\n演示完成!")


if __name__ == "__main__":
    # 运行主程序
    main()

"随机生成k均值聚类数据集.py"的程序运行结果如下:

kmeans_data.csv

"k均值聚类.py"的程序运行结果如下:

==================================================
K均值聚类算法可视化演示
==================================================

1. 数据准备
------------------------------
生成数据集: 500个样本, 2维特征, 4个簇...
数据已保存至: kmeans_data.csv
成功加载数据集: kmeans_data.csv, 大小: 500

2. 基本K均值聚类
------------------------------

使用随机初始化:
使用随机方法初始化聚类中心...
迭代 1: SSE = 4245.1649, 质心移动距离 = 8.867450
迭代 2: SSE = 635.2410, 质心移动距离 = 3.043416
迭代 3: SSE = 607.9868, 质心移动距离 = 0.052485
迭代 4: SSE = 607.9868, 质心移动距离 = 0.000000
算法在第4次迭代后收敛
K均值聚类完成,用时 0.0011 秒
总迭代次数: 4, 最终SSE: 607.9868

基本聚类结果可视化:

3. 使用K-means++初始化
------------------------------
使用K-means++初始化聚类中心...
迭代 1: SSE = 647.2863, 质心移动距离 = 2.977551
迭代 2: SSE = 607.9868, 质心移动距离 = 0.085365
迭代 3: SSE = 607.9868, 质心移动距离 = 0.000000
算法在第3次迭代后收敛
K均值聚类完成,用时 0.0025 秒
总迭代次数: 3, 最终SSE: 607.9868

4. 高级可视化功能演示
------------------------------

4.1 初始化方法对比可视化:
初始化方法        迭代次数         最终SSE           时间(秒)       
-------------------------------------------------------
随机初始化        4            607.9868        0.4000      
K-means++    2            607.9868        0.2000      

4.2 聚类过程动态可视化:

4.3 簇内误差分布可视化:

5. 3D数据聚类演示
------------------------------
生成数据集: 500个样本, 3维特征, 4个簇...

3D数据聚类:
使用K-means++初始化聚类中心...
迭代 1: SSE = 924.0294, 质心移动距离 = 2.636694
迭代 2: SSE = 924.0294, 质心移动距离 = 0.000000
算法在第2次迭代后收敛
K均值聚类完成,用时 0.0010 秒
总迭代次数: 2, 最终SSE: 924.0294

3D聚类结果可视化:

3D聚类过程动态可视化:

演示完成!
 

五、总结

        k均值聚类(k-Means Clustering)是一种无监督学习的分类算法,旨在将数据点划分为k个不同的簇,使得每个数据点属于离它最近的质心(簇中心)所代表的簇。该算法迭代进行,直到满足收敛条件。

        k均值聚类作为一种经典算法,凭借其简单高效的特性,在各领域中展现出强大的应用价值。尽管存在一些局限性,但通过适当的预处理、参数选择和算法改进,k均值聚类仍能有效地应对各种实际问题。随着计算技术的发展和算法的不断完善,k均值聚类及其变体将在更多场景中发挥重要作用,为数据分析和决策提供有力支持。

        在实际应用中,k均值聚类通常不是独立使用的,而是作为数据分析流程中的一个环节,与其他技术相结合,共同构建完整的解决方案。因此,深入理解k均值聚类的原理、特性和应用场景,对于充分发挥其价值至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值