DAY 17 常见聚类算法

@浙大疏锦行https://blog.csdn.net/weixin_45655710

day 17笔记全流程(可点开下载)

# 导入必要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings

# 忽略警告信息,使输出更整洁
warnings.filterwarnings("ignore")

# 设置中文字体,确保图表中的中文能够正常显示
plt.rcParams['font.sans-serif'] = ['SimHei']  # SimHei是Windows下常用的黑体
plt.rcParams['axes.unicode_minus'] = False  # 正常显示图表中的负号

# --- 步骤 1: 加载心脏病数据集 ---
print("--- 步骤 1: 加载心脏病数据集 ---")
try:
    # 读取名为 'heart.csv' 的数据文件
    data = pd.read_csv('heart.csv')
    print("✅ 心脏病数据加载成功!")
    print(f"数据形状 (行数, 列数): {data.shape}")
    print("\n数据前5行预览:")
    print(data.head())
    print("\n数据基本信息:")
    data.info()
    print("\n数据描述性统计:")
    print(data.describe())
except FileNotFoundError:
    print("❌ 错误: 'heart.csv' 文件未找到。请确保文件在此脚本的同一目录下。")
    exit() # 如果文件未找到,则退出脚本
except Exception as e:
    print(f"❌ 加载文件时发生错误: {e}")
    exit() # 如果发生其他错误,则退出脚本

# --- 步骤 2: 数据探索与预处理 ---
# 心脏病数据集的特征通常是数值型的,或者已经是编码好的类别型数值。
# 我们将检查是否有明显的字符串类型需要编码,并处理缺失值。

print("\n--- 步骤 2: 数据探索与预处理 ---")

# 2.1 检查是否有字符串类型的特征需要处理
# (根据常见的心脏病数据集,大部分特征应该是数值型)
object_columns = data.select_dtypes(include=['object']).columns
if not object_columns.empty:
    print(f"发现字符串类型的列: {object_columns.tolist()}")
    print("请根据具体列的含义决定如何编码(例如标签编码或独热编码)。")
    # 示例:如果需要,可以在此处添加对特定对象列的编码逻辑
    # 例如: data['某个对象列'] = data['某个对象列'].astype('category').cat.codes
else:
    print("✅ 数据集中没有发现需要特殊编码的字符串类型列。")

# 2.2 检查并处理缺失值
# 对于心脏病数据集,通常用中位数或均值填充数值型特征的缺失值
print("\n检查各列的缺失值数量:")
print(data.isnull().sum())

# 筛选出数值类型的列(包括整数和浮点数)
numerical_features = data.select_dtypes(include=[np.number]).columns.tolist()

if data.isnull().sum().sum() > 0: # 如果存在任何缺失值
    print("\n正在使用中位数填充数值型特征的缺失值...")
    for feature in numerical_features:
        if data[feature].isnull().any(): # 如果该列存在缺失值
            median_value = data[feature].median() # 计算中位数
            data[feature].fillna(median_value, inplace=True) # 用中位数填充
            print(f"   列 '{feature}' 的缺失值已用中位数 {median_value} 填充。")
    print("✅ 缺失值处理完成。")
else:
    print("✅ 数据集中没有缺失值。")



print("\n--- 步骤 3: 划分特征与标签 ---")
# 3.1 分离特征 (X) 和标签 (y)
# 假设目标变量(是否患有心脏病)的列名为 'target'
X = data.drop(['thalach'], axis=1)  # 特征集,移除目标变量列
y = data['thalach']                 # 标签集,只包含目标变量列

print("\n--- 步骤 4: 划分训练集和测试集 ---")
# 3.2 按照8:2的比例划分训练集和测试集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,     # 20% 的数据作为测试集
    random_state=42,   # 设置随机种子以确保结果可复现
)
print("✅ 训练集和测试集已成功划分。")
print(f"训练集特征 X_train 的形状: {X_train.shape}")
print(f"测试集特征 X_test 的形状: {X_test.shape}")
print(f"训练集标签 y_train 的形状: {y_train.shape}")
print(f"测试集标签 y_test 的形状: {y_test.shape}")

print("\n--- 数据预处理流程结束 ---")

import numpy as np
import pandas as pd
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import seaborn as sns

# 标准化数据(聚类前通常需要标准化)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# X_scaled

知识点

聚类的指标

## 聚类评估指标介绍

以下是三种常用的聚类效果评估指标,分别用于衡量聚类的质量和簇的分离与紧凑程度:

### 1. 轮廓系数 (Silhouette Score)
- **定义**:轮廓系数衡量每个样本与其所属簇的紧密程度以及与最近其他簇的分离程度。
- **取值范围**:[-1, 1]
  - 轮廓系数越接近 **1**,表示样本与其所属簇内其他样本很近,与其他簇很远,聚类效果越好。
  - 轮廓系数越接近 **-1**,表示样本与其所属簇内样本较远,与其他簇较近,聚类效果越差(可能被错误分类)。
  - 轮廓系数接近 **0**,表示样本在簇边界附近,聚类效果无明显好坏。
- **使用建议**:选择轮廓系数最高的 `k` 值作为最佳簇数量。

### 2. CH 指数 (Calinski-Harabasz Index)
- **定义**:CH 指数是簇间分散度与簇内分散度之比,用于评估簇的分离度和紧凑度。
- **取值范围**:[0, +∞)
  - CH 指数越大,表示簇间分离度越高,簇内紧凑度越高,聚类效果越好。
  - 没有固定的上限,值越大越好。
- **使用建议**:选择 CH 指数最高的 `k` 值作为最佳簇数量。

### 3. DB 指数 (Davies-Bouldin Index)
- **定义**:DB 指数衡量簇间距离与簇内分散度的比值,用于评估簇的分离度和紧凑度。
- **取值范围**:[0, +∞)
  - DB 指数越小,表示簇间分离度越高,簇内紧凑度越高,聚类效果越好。
  - 没有固定的上限,值越小越好。
- **使用建议**:选择 DB 指数最低的 `k` 值作为最佳簇数量。

聚类常见算法:kmeans聚类、dbscan聚类、层次聚类

## KMeans 聚类

### 算法原理
KMeans 是一种基于距离的聚类算法,需要预先指定聚类个数,即 `k`。其核心步骤如下:
1. 随机选择 `k` 个样本点作为初始质心(簇中心)。
2. 计算每个样本点到各个质心的距离,将样本点分配到距离最近的质心所在的簇。
3. 更新每个簇的质心为该簇内所有样本点的均值。
4. 重复步骤 2 和 3,直到质心不再变化或达到最大迭代次数为止。

### 确定簇数的方法:肘部法
- **肘部法(Elbow Method)** 是一种常用的确定 `k` 值的方法。
- 原理:通过计算不同 `k` 值下的簇内平方和(Within-Cluster Sum of Squares, WCSS),绘制 `k` 与 WCSS 的关系图。
- 选择标准:在图中找到“肘部”点,即 WCSS 下降速率明显减缓的 `k` 值,通常认为是最佳簇数。这是因为增加 `k` 值带来的收益(WCSS 减少)在该点后变得不显著。

### KMeans 算法的优缺点
#### 优点
- **简单高效**:算法实现简单,计算速度快,适合处理大规模数据集。
- **适用性强**:对球形或紧凑的簇效果较好,适用于特征空间中簇分布较为均匀的数据。
- **易于解释**:聚类结果直观,簇中心具有明确的物理意义。

#### 缺点
- **需预先指定 `k` 值**:对簇数量 `k` 的选择敏感,不合适的 `k` 会导致聚类效果较差。
- **对初始质心敏感**:初始质心的随机选择可能导致结果不稳定或陷入局部最优(可通过 KMeans++ 初始化方法缓解)。
- **对噪声和异常值敏感**:异常值可能会显著影响质心的位置,导致聚类结果失真。
- **不适合非球形簇**:对非线性可分或形状复杂的簇效果较差,无法处理簇密度不均的情况。

三种算法对应的流程

kmeans聚类

import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
import matplotlib.pyplot as plt
import seaborn as sns

# 评估不同 k 值下的指标
k_range = range(2, 11)  # 测试 k 从 2 到 10
inertia_values = []
silhouette_scores = []
ch_scores = []
db_scores = []

for k in k_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    kmeans_labels = kmeans.fit_predict(X_scaled)
    inertia_values.append(kmeans.inertia_)  # 惯性(肘部法则)
    silhouette = silhouette_score(X_scaled, kmeans_labels)  # 轮廓系数
    silhouette_scores.append(silhouette)
    ch = calinski_harabasz_score(X_scaled, kmeans_labels)  # CH 指数
    ch_scores.append(ch)
    db = davies_bouldin_score(X_scaled, kmeans_labels)  # DB 指数
    db_scores.append(db)
    print(f"k={k}, 惯性: {kmeans.inertia_:.2f}, 轮廓系数: {silhouette:.3f}, CH 指数: {ch:.2f}, DB 指数: {db:.3f}")

# 绘制评估指标图
plt.figure(figsize=(15, 10))

# 肘部法则图(Inertia)
plt.subplot(2, 2, 1)
plt.plot(k_range, inertia_values, marker='o')
plt.title('肘部法则确定最优聚类数 k(惯性,越小越好)')
plt.xlabel('聚类数 (k)')
plt.ylabel('惯性')
plt.grid(True)

# 轮廓系数图
plt.subplot(2, 2, 2)
plt.plot(k_range, silhouette_scores, marker='o', color='orange')
plt.title('轮廓系数确定最优聚类数 k(越大越好)')
plt.xlabel('聚类数 (k)')
plt.ylabel('轮廓系数')
plt.grid(True)

# CH 指数图
plt.subplot(2, 2, 3)
plt.plot(k_range, ch_scores, marker='o', color='green')
plt.title('Calinski-Harabasz 指数确定最优聚类数 k(越大越好)')
plt.xlabel('聚类数 (k)')
plt.ylabel('CH 指数')
plt.grid(True)

# DB 指数图
plt.subplot(2, 2, 4)
plt.plot(k_range, db_scores, marker='o', color='red')
plt.title('Davies-Bouldin 指数确定最优聚类数 k(越小越好)')
plt.xlabel('聚类数 (k)')
plt.ylabel('DB 指数')
plt.grid(True)

plt.tight_layout()
plt.show()

DAY 17 常见聚类算法_第1张图片

### 选择k值确实需要在“分得更细”(通常k越大,簇内差异越小)和“避免过度划分导致失去意义”(k太大,每个簇样本太少,解释性差)之间做权衡。 

# 提示用户选择 k 值
selected_k = 4

# 使用选择的 k 值进行 KMeans 聚类
kmeans = KMeans(n_clusters=selected_k, random_state=42)
kmeans_labels = kmeans.fit_predict(X_scaled)
X['KMeans_Cluster'] = kmeans_labels

# 使用 PCA 降维到 2D 进行可视化
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

# KMeans 聚类结果可视化
plt.figure(figsize=(6, 5))
sns.scatterplot(x=X_pca[:, 0], y=X_pca[:, 1], hue=kmeans_labels, palette='viridis')
plt.title(f'KMeans Clustering with k={selected_k} (PCA Visualization)')
plt.xlabel('PCA Component 1')
plt.ylabel('PCA Component 2')
plt.show()

# 打印 KMeans 聚类标签的前几行
print(f"KMeans Cluster labels (k={selected_k}) added to X:")
print(X[['KMeans_Cluster']].value_counts())

DAY 17 常见聚类算法_第2张图片 

当前的 KMeans 聚类结果,在通过PCA降维进行二维可视化时,未能展现出清晰的簇结构,表明数据在所选主成分方向上的可分性较差

聚类是否“成功”或有意义,不能仅凭这张PCA图判断,更关键的在于分析该图中的特征均值表。 如果能从特征均值中发现不同簇之间存在稳定且可解释的模式(例如,某些簇代表了特定的病人画像),那么即使PCA可视化不佳,聚类也可能提供了一些有价值的洞察。反之,如果特征均值差异不明显或难以解释,则说明当前特征集和KMeans算法的组合可能不适合在此数据集上发现有意义的、分离良好的簇。

dbscan聚类

import numpy as np
import pandas as pd
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
import matplotlib.pyplot as plt
import seaborn as sns



# 评估不同 eps 和 min_samples 下的指标
# eps这个参数表示邻域的半径,min_samples表示一个点被认为是核心点所需的最小样本数。
# min_samples这个参数表示一个核心点所需的最小样本数。

eps_range = np.arange(0.1, 2.1, 0.2)  # 测试 eps 从 0.1 到 2.0
min_samples_range = range(3, 16, 2)  # 测试 min_samples 从 3 到 15
results = []

for eps in eps_range:
    for min_samples in min_samples_range:
        dbscan = DBSCAN(eps=eps, min_samples=min_samples)
        dbscan_labels = dbscan.fit_predict(X_scaled)
        # 计算簇的数量(排除噪声点 -1)
        n_clusters = len(np.unique(dbscan_labels)) - (1 if -1 in dbscan_labels else 0)
        # 计算噪声点数量
        n_noise = list(dbscan_labels).count(-1)
        # 只有当簇数量大于 1 且有有效簇时才计算评估指标
        if n_clusters > 1:
            # 排除噪声点后计算评估指标
            mask = dbscan_labels != -1
            if mask.sum() > 0:  # 确保有非噪声点
                silhouette = silhouette_score(X_scaled[mask], dbscan_labels[mask])
                ch = calinski_harabasz_score(X_scaled[mask], dbscan_labels[mask])
                db = davies_bouldin_score(X_scaled[mask], dbscan_labels[mask])
                results.append({
                    'eps': eps,
                    'min_samples': min_samples,
                    'n_clusters': n_clusters,
                    'n_noise': n_noise,
                    'silhouette': silhouette,
                    'ch_score': ch,
                    'db_score': db
                })
                print(f"eps={eps:.1f}, min_samples={min_samples}, 簇数: {n_clusters}, 噪声点: {n_noise}, "
                      f"轮廓系数: {silhouette:.3f}, CH 指数: {ch:.2f}, DB 指数: {db:.3f}")
        else:
            print(f"eps={eps:.1f}, min_samples={min_samples}, 簇数: {n_clusters}, 噪声点: {n_noise}, 无法计算评估指标")

# 将结果转为 DataFrame 以便可视化和选择参数
results_df = pd.DataFrame(results)

DAY 17 常见聚类算法_第3张图片

# 绘制评估指标图,增加点论文中的工作量
plt.figure(figsize=(15, 10))
# 轮廓系数图
plt.subplot(2, 2, 1)
for min_samples in min_samples_range:
    subset = results_df[results_df['min_samples'] == min_samples] # 
    plt.plot(subset['eps'], subset['silhouette'], marker='o', label=f'min_samples={min_samples}')
plt.title('轮廓系数确定最优参数(越大越好)')
plt.xlabel('eps')
plt.ylabel('轮廓系数')
plt.legend()
plt.grid(True)

# CH 指数图
plt.subplot(2, 2, 2)
for min_samples in min_samples_range:
    subset = results_df[results_df['min_samples'] == min_samples]
    plt.plot(subset['eps'], subset['ch_score'], marker='o', label=f'min_samples={min_samples}')
plt.title('Calinski-Harabasz 指数确定最优参数(越大越好)')
plt.xlabel('eps')
plt.ylabel('CH 指数')
plt.legend()
plt.grid(True)

# DB 指数图
plt.subplot(2, 2, 3)
for min_samples in min_samples_range:
    subset = results_df[results_df['min_samples'] == min_samples]
    plt.plot(subset['eps'], subset['db_score'], marker='o', label=f'min_samples={min_samples}')
plt.title('Davies-Bouldin 指数确定最优参数(越小越好)')
plt.xlabel('eps')
plt.ylabel('DB 指数')
plt.legend()
plt.grid(True)

# 簇数量图
plt.subplot(2, 2, 4)
for min_samples in min_samples_range:
    subset = results_df[results_df['min_samples'] == min_samples]
    plt.plot(subset['eps'], subset['n_clusters'], marker='o', label=f'min_samples={min_samples}')
plt.title('簇数量变化')
plt.xlabel('eps')
plt.ylabel('簇数量')
plt.legend()
plt.grid(True)

plt.tight_layout()
plt.show()

DAY 17 常见聚类算法_第4张图片

# 选择 eps 和 min_samples 值(根据图表选择最佳参数)
selected_eps = 1.5  # 根据图表调整
selected_min_samples = 7  # 根据图表调整

# 使用选择的参数进行 DBSCAN 聚类
dbscan = DBSCAN(eps=selected_eps, min_samples=selected_min_samples)
dbscan_labels = dbscan.fit_predict(X_scaled)
X['DBSCAN_Cluster'] = dbscan_labels

# 使用 PCA 降维到 2D 进行可视化
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

# DBSCAN 聚类结果可视化
plt.figure(figsize=(6, 5))
sns.scatterplot(x=X_pca[:, 0], y=X_pca[:, 1], hue=dbscan_labels, palette='viridis')
plt.title(f'DBSCAN Clustering with eps={selected_eps}, min_samples={selected_min_samples} (PCA Visualization)')
plt.xlabel('PCA Component 1')
plt.ylabel('PCA Component 2')
plt.show()

# 打印 DBSCAN 聚类标签的分布
print(f"DBSCAN Cluster labels (eps={selected_eps}, min_samples={selected_min_samples}) added to X:")
print(X[['DBSCAN_Cluster']].value_counts())

DAY 17 常见聚类算法_第5张图片

DBSCAN的核心思想是基于密度来发现簇。当结果显示绝大多数点都是噪声,或者只形成极少数非常小的簇时,通常意味着:

参数不合适:eps(邻域半径)和 min_samples(核心点最小样本数)的组合没有捕捉到数据的真实密度分布。当前的eps可能太小,或者min_samples太大。

数据本身不适合DBSCAN:数据的密度分布可能非常均匀,或者不具备DBSCAN所能识别的那种基于密度的簇结构。

层次聚类

import numpy as np
import pandas as pd
from sklearn.cluster import AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
import matplotlib.pyplot as plt
import seaborn as sns

# 标准化数据
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 评估不同 n_clusters 下的指标
n_clusters_range = range(2, 11)  # 测试簇数量从 2 到 10
silhouette_scores = []
ch_scores = []
db_scores = []

for n_clusters in n_clusters_range:
    agglo = AgglomerativeClustering(n_clusters=n_clusters, linkage='ward')  # 使用 Ward 准则合并簇
    agglo_labels = agglo.fit_predict(X_scaled)
    
    # 计算评估指标
    silhouette = silhouette_score(X_scaled, agglo_labels)
    ch = calinski_harabasz_score(X_scaled, agglo_labels)
    db = davies_bouldin_score(X_scaled, agglo_labels)
    
    silhouette_scores.append(silhouette)
    ch_scores.append(ch)
    db_scores.append(db)
    
    print(f"n_clusters={n_clusters}, 轮廓系数: {silhouette:.3f}, CH 指数: {ch:.2f}, DB 指数: {db:.3f}")

# 绘制评估指标图
plt.figure(figsize=(15, 5))

# 轮廓系数图
plt.subplot(1, 3, 1)
plt.plot(n_clusters_range, silhouette_scores, marker='o')
plt.title('轮廓系数确定最优簇数(越大越好)')
plt.xlabel('簇数量 (n_clusters)')
plt.ylabel('轮廓系数')
plt.grid(True)

# CH 指数图
plt.subplot(1, 3, 2)
plt.plot(n_clusters_range, ch_scores, marker='o')
plt.title('Calinski-Harabasz 指数确定最优簇数(越大越好)')
plt.xlabel('簇数量 (n_clusters)')
plt.ylabel('CH 指数')
plt.grid(True)

# DB 指数图
plt.subplot(1, 3, 3)
plt.plot(n_clusters_range, db_scores, marker='o')
plt.title('Davies-Bouldin 指数确定最优簇数(越小越好)')
plt.xlabel('簇数量 (n_clusters)')
plt.ylabel('DB 指数')
plt.grid(True)

plt.tight_layout()
plt.show()

DAY 17 常见聚类算法_第6张图片

# 提示用户选择 n_clusters 值(这里可以根据图表选择最佳簇数)
selected_n_clusters = 5  # 示例值,根据图表调整

# 使用选择的簇数进行 Agglomerative Clustering 聚类
agglo = AgglomerativeClustering(n_clusters=selected_n_clusters, linkage='ward')
agglo_labels = agglo.fit_predict(X_scaled)
X['Agglo_Cluster'] = agglo_labels

# 使用 PCA 降维到 2D 进行可视化
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

# Agglomerative Clustering 聚类结果可视化
plt.figure(figsize=(6, 5))
sns.scatterplot(x=X_pca[:, 0], y=X_pca[:, 1], hue=agglo_labels, palette='viridis')
plt.title(f'Agglomerative Clustering with n_clusters={selected_n_clusters} (PCA Visualization)')
plt.xlabel('PCA Component 1')
plt.ylabel('PCA Component 2')
plt.show()

# 打印 Agglomerative Clustering 聚类标签的分布
print(f"Agglomerative Cluster labels (n_clusters={selected_n_clusters}) added to X:")
print(X[['Agglo_Cluster']].value_counts())

 DAY 17 常见聚类算法_第7张图片

# 层次聚类的树状图可视化
from scipy.cluster import hierarchy
import matplotlib.pyplot as plt

# 假设 X_scaled 是标准化后的数据
# 计算层次聚类的链接矩阵
Z = hierarchy.linkage(X_scaled, method='ward')  # 'ward' 是常用的合并准则

# 绘制树状图
plt.figure(figsize=(10, 6))
hierarchy.dendrogram(Z, truncate_mode='level', p=3)  # p 控制显示的层次深度
# hierarchy.dendrogram(Z, truncate_mode='level')  # 不用p这个参数,可以显示全部的深度
plt.title('Dendrogram for Agglomerative Clustering')
plt.xlabel('Cluster Size')
plt.ylabel('Distance')
plt.show()

DAY 17 常见聚类算法_第8张图片

底部 (X轴):代表数据集中的每一个独立样本(或非常小的初始簇)。

垂直线 (Y轴):代表样本或簇之间的距离或不相似度。线越长,表示合并这两个簇(或样本)时的距离越大,即它们之间的相似度越低。

横线:表示两个或多个下方的簇(或样本)在某个距离阈值下被合并成了一个更大的簇。横线所在的高度就是合并时的距离。

从下往上看:
最开始,每个样本自成一簇。

随着我们向上移动(即放宽合并的距离阈值),最相似的样本/簇会首先合并。

这个过程持续进行,簇越来越大,数量越来越少。

直到最顶部,所有样本都合并成了一个大簇。


决定簇的数量 (k):您可以想象用一把水平的剪刀从上往下“剪”这个树状图。

剪刀切断了多少条垂直线,就意味着您将数据分成了多少个簇。

通常选择一个“剪切点”,使得剪切处上方的合并距离(即横线的高度)有一个显著的跳跃。这意味着再往上合并,就需要合并差异很大的簇了,所以在这个跳跃点下方形成的簇是相对合理的。

例如,观察该图,如果在Y轴大约70-80的高度横向切一刀,可能会得到2或3个大的簇。如果在更低的位置(比如Y轴20-30),可能会得到更多更小的簇。

观察簇的结构和层次:树状图直观地展示了数据点是如何逐级聚合的,能帮您理解数据点之间的远近关系和数据的层次结构。

你可能感兴趣的:(python训练营打卡笔记,机器学习)