聚类分析实践详解
聚类分析实践详解
聚类是无监督学习中最常见的任务,通过发现数据中的隐藏结构来进行分组。
客户细分案例
数据准备
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, adjusted_rand_score
from sklearn.datasets import make_blobs
import seaborn as sns
# 创建模拟客户数据
np.random.seed(42)
n_customers = 500
# 客户特征
age = np.random.randint(18, 70, n_customers)
income = np.random.normal(50000, 15000, n_customers)
spending_score = np.random.randint(1, 100, n_customers)
membership_years = np.random.randint(0, 10, n_customers)
purchase_frequency = np.random.poisson(5, n_customers) # 购买频率
# 创建DataFrame
df = pd.DataFrame({
'年龄': age,
'收入': income,
'消费评分': spending_score,
'会员年限': membership_years,
'购买频率': purchase_frequency
})
print("客户数据集信息:")
print(df.info())
print("\n数据统计描述:")
print(df.describe())
数据探索
# 数据可视化
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
# 年龄分布
axes[0, 0].hist(df['年龄'], bins=20, edgecolor='black', alpha=0.7, color='skyblue')
axes[0, 0].set_xlabel('年龄')
axes[0, 0].set_ylabel('客户数量')
axes[0, 0].set_title('年龄分布')
axes[0, 0].grid(True, alpha=0.3)
# 收入分布
axes[0, 1].hist(df['收入'], bins=20, edgecolor='black', alpha=0.7, color='lightcoral')
axes[0, 1].set_xlabel('收入')
axes[0, 1].set_ylabel('客户数量')
axes[0, 1].set_title('收入分布')
axes[0, 1].grid(True, alpha=0.3)
# 消费评分分布
axes[0, 2].hist(df['消费评分'], bins=20, edgecolor='black', alpha=0.7, color='lightgreen')
axes[0, 2].set_xlabel('消费评分')
axes[0, 2].set_ylabel('客户数量')
axes[0, 2].set_title('消费评分分布')
axes[0, 2].grid(True, alpha=0.3)
# 年龄 vs 收入
axes[1, 0].scatter(df['年龄'], df['收入'], alpha=0.5)
axes[1, 0].set_xlabel('年龄')
axes[1, 0].set_ylabel('收入')
axes[1, 0].set_title('年龄 vs 收入')
axes[1, 0].grid(True, alpha=0.3)
# 年龄 vs 消费评分
axes[1, 1].scatter(df['年龄'], df['消费评分'], alpha=0.5)
axes[1, 1].set_xlabel('年龄')
axes[1, 1].set_ylabel('消费评分')
axes[1, 1].set_title('年龄 vs 消费评分')
axes[1, 1].grid(True, alpha=0.3)
# 收入 vs 消费评分
axes[1, 2].scatter(df['收入'], df['消费评分'], alpha=0.5)
axes[1, 2].set_xlabel('收入')
axes[1, 2].set_ylabel('消费评分')
axes[1, 2].set_title('收入 vs 消费评分')
axes[1, 2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 相关性分析
print("\n特征相关性:")
print(df.corr().round(2))
聚类分析
数据预处理
# 特征选择
features = ['年龄', '收入', '消费评分', '会员年限', '购买频率']
X = df[features]
# 标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
print(f"标准化后数据形状: {X_scaled.shape}")
print(f"标准化后数据均值: {X_scaled.mean(axis=0).round(2)}")
print(f"标准化后数据标准差: {X_scaled.std(axis=0).round(2)}")
确定最佳聚类数
# 肘部法则
inertias = []
silhouette_scores = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X_scaled)
inertias.append(kmeans.inertia_)
silhouette_scores.append(silhouette_score(X_scaled, kmeans.labels_))
# 可视化
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# 肘部法则
ax1.plot(K_range, inertias, 'bo-', linewidth=2, markersize=8)
ax1.set_xlabel('聚类数量 K')
ax1.set_ylabel('惯性 (WCSS)')
ax1.set_title('肘部法则')
ax1.grid(True, alpha=0.3)
# 轮廓系数
ax2.plot(K_range, silhouette_scores, 'ro-', linewidth=2, markersize=8)
ax2.set_xlabel('聚类数量 K')
ax2.set_ylabel('轮廓系数')
ax2.set_title('轮廓系数法')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 找到最佳K值
best_k = K_range[np.argmax(silhouette_scores)]
print(f"最佳聚类数量: {best_k}")
print(f"最佳轮廓系数: {max(silhouette_scores):.4f}")
应用K-means聚类
# 应用K-means
kmeans = KMeans(n_clusters=best_k, random_state=42, n_init=10)
clusters = kmeans.fit_predict(X_scaled)
# 添加聚类标签到原始数据
df['聚类'] = clusters
print("聚类结果统计:")
print(df.groupby('聚类')[features].mean().round(2))
# 可视化聚类结果
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 年龄 vs 收入
scatter1 = axes[0, 0].scatter(df['年龄'], df['收入'], c=df['聚类'],
cmap='viridis', alpha=0.6)
axes[0, 0].set_xlabel('年龄')
axes[0, 0].set_ylabel('收入')
axes[0, 0].set_title('聚类结果: 年龄 vs 收入')
plt.colorbar(scatter1, ax=axes[0, 0])
# 年龄 vs 消费评分
scatter2 = axes[0, 1].scatter(df['年龄'], df['消费评分'], c=df['聚类'],
cmap='viridis', alpha=0.6)
axes[0, 1].set_xlabel('年龄')
axes[0, 1].set_ylabel('消费评分')
axes[0, 1].set_title('聚类结果: 年龄 vs 消费评分')
plt.colorbar(scatter2, ax=axes[0, 1])
# 收入 vs 消费评分
scatter3 = axes[1, 0].scatter(df['收入'], df['消费评分'], c=df['聚类'],
cmap='viridis', alpha=0.6)
axes[1, 0].set_xlabel('收入')
axes[1, 0].set_ylabel('消费评分')
axes[1, 0].set_title('聚类结果: 收入 vs 消费评分')
plt.colorbar(scatter3, ax=axes[1, 0])
# 聚类分布
cluster_counts = df['聚类'].value_counts().sort_index()
axes[1, 1].bar(cluster_counts.index, cluster_counts.values,
color=plt.cm.viridis(np.linspace(0, 1, len(cluster_counts))))
axes[1, 1].set_xlabel('聚类')
axes[1, 1].set_ylabel('客户数量')
axes[1, 1].set_title('各聚类客户数量')
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
其他聚类算法
DBSCAN聚类
# DBSCAN聚类
dbscan = DBSCAN(eps=0.5, min_samples=5)
dbscan_clusters = dbscan.fit_predict(X_scaled)
# 评估
n_clusters_dbscan = len(set(dbscan_clusters)) - (1 if -1 in dbscan_clusters else 0)
n_noise = list(dbscan_clusters).count(-1)
print(f"DBSCAN发现的聚类数量: {n_clusters_dbscan}")
print(f"噪声点数量: {n_noise}")
if n_clusters_dbscan > 1:
silhouette_dbscan = silhouette_score(X_scaled[dbscan_clusters != -1],
dbscan_clusters[dbscan_clusters != -1])
print(f"DBSCAN轮廓系数: {silhouette_dbscan:.4f}")
层次聚类
# 层次聚类
agg_clustering = AgglomerativeClustering(n_clusters=best_k)
agg_clusters = agg_clustering.fit_predict(X_scaled)
# 评估
silhouette_agg = silhouette_score(X_scaled, agg_clusters)
print(f"层次聚类轮廓系数: {silhouette_agg:.4f}")
# 与K-means比较
ari = adjusted_rand_score(clusters, agg_clusters)
print(f"调整兰德指数 (K-means vs 层次聚类): {ari:.4f}")
降维可视化
PCA降维
# PCA降维到2D
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
print(f"PCA解释方差比例: {pca.explained_variance_ratio_}")
print(f"累计解释方差: {sum(pca.explained_variance_ratio_):.4f}")
# 可视化
plt.figure(figsize=(10, 6))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=clusters,
cmap='viridis', alpha=0.6, s=50)
plt.xlabel(f'主成分1 ({pca.explained_variance_ratio_[0]:.2%})')
plt.ylabel(f'主成分2 ({pca.explained_variance_ratio_[1]:.2%})')
plt.title('PCA降维后的聚类结果')
plt.colorbar(scatter, label='聚类')
plt.grid(True, alpha=0.3)
plt.show()
聚类结果解释
客户画像
# 创建客户画像
cluster_profiles = df.groupby('聚类').agg({
'年龄': ['mean', 'std'],
'收入': ['mean', 'std'],
'消费评分': ['mean', 'std'],
'会员年限': ['mean', 'std'],
'购买频率': ['mean', 'std']
}).round(2)
print("各聚类客户画像:")
print(cluster_profiles)
# 可视化聚类特征
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# 年龄分布
for cluster in range(best_k):
cluster_data = df[df['聚类'] == cluster]['年龄']
axes[0].hist(cluster_data, bins=15, alpha=0.5, label=f'聚类 {cluster}')
axes[0].set_xlabel('年龄')
axes[0].set_ylabel('客户数量')
axes[0].set_title('各聚类年龄分布')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 收入分布
for cluster in range(best_k):
cluster_data = df[df['聚类'] == cluster]['收入']
axes[1].hist(cluster_data, bins=15, alpha=0.5, label=f'聚类 {cluster}')
axes[1].set_xlabel('收入')
axes[1].set_ylabel('客户数量')
axes[1].set_title('各聚类收入分布')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# 消费评分分布
for cluster in range(best_k):
cluster_data = df[df['聚类'] == cluster]['消费评分']
axes[2].hist(cluster_data, bins=15, alpha=0.5, label=f'聚类 {cluster}')
axes[2].set_xlabel('消费评分')
axes[2].set_ylabel('客户数量')
axes[2].set_title('各聚类消费评分分布')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
实际应用
客户细分策略
# 根据聚类结果制定营销策略
print("客户细分策略建议:")
print("=" * 50)
for cluster in range(best_k):
cluster_df = df[df['聚类'] == cluster]
print(f"\n聚类 {cluster} (共 {len(cluster_df)} 位客户):")
print(f" 平均年龄: {cluster_df['年龄'].mean():.1f}岁")
print(f" 平均收入: {cluster_df['收入'].mean():.0f}元")
print(f" 平均消费评分: {cluster_df['消费评分'].mean():.1f}")
print(f" 平均会员年限: {cluster_df['会员年限'].mean():.1f}年")
print(f" 平均购买频率: {cluster_df['购买频率'].mean():.1f}次")
# 根据特征给出建议
if cluster_df['消费评分'].mean() > 70:
print(" 营销策略: 高价值客户,提供VIP服务和专属优惠")
elif cluster_df['年龄'].mean() < 30:
print(" 营销策略: 年轻客户,推送时尚产品和社交媒体营销")
elif cluster_df['收入'].mean() > 60000:
print(" 营销策略: 高收入客户,推荐高端产品和服务")
else:
print(" 营销策略: 普通客户,提供常规促销和会员福利")
总结
通过这个完整的聚类分析案例,我们掌握了:
- 客户数据的探索和可视化
- 确定最佳聚类数量的方法(肘部法则、轮廓系数)
- K-means聚类的应用和评估
- 其他聚类算法的比较(DBSCAN、层次聚类)
- 降维可视化(PCA)
- 聚类结果的解释和应用
聚类分析是无监督学习的核心技术,掌握聚类分析对于客户细分、市场分析等应用至关重要。