Python聚类模型原理及应用--批发商客户分群

2019-01-08 08:24:25来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

前言

在前面介绍的线性回归, 岭回归, Lasso回归, 逻辑回归均是监督学习, 下面将要介绍一种无监督学习—“聚类"

目录

 1. 划分聚类

 2. k-means算法

 3. 优缺点及注意事项

 4. 实际案例应用

正文

 “物以类聚,人以群分”, 所谓聚类就是将相似的元素分到一"类"(有时也被称为"簇"或"集合"), 簇内元素相似程度高, 簇间元素相似程度低. 常用的聚类方法有划分聚类, 层次聚类, 密度聚类, 网格聚类, 模型聚类等. 我们这里重点介绍划分聚类.

1. 划分聚类

划分聚类, 就是给定一个样本量为N的数据集, 将其划分为K个簇(K<N), 每一个簇中至少包含一个样本点.

大部分的划分方法是基于距离的, 即簇内距离最小化, 簇间距离最大化. 常用的距离计算方法有: 曼哈顿距离和欧几里得距离. 坐标点(x1, y1)到坐标点(x2, y2)的曼哈顿距离和欧几里得距离分别表示为:

为了达到全局最优解, 传统的划分法可能需要穷举所有可能的划分点, 这计算量是相当大的. 而在实际应用中, 通常会通过计算到均值或中心点的距离进行划分来逼近局部最优, 把计算到均值和到中心点距离的算法分别称为K-MEANS算法和K-MEDOIDS算法, 在这里只介绍K-MEANS算法.

2. K-MEANS算法

K-MEANS算法有时也叫快速聚类算法, 其大致流程为:

第一步: 随机选取K个点, 作为初始的K个聚类中心, 有时也叫质心.

第二步: 计算每个样本点到K个聚类中心的距离, 并将其分给距离最短的簇, 如果k个簇中均至少有一个样本点, 那么我们就说将所有样本点分成了K个簇.

第三步: 计算K个簇中所有样本点的均值, 将这K个均值作为K个新的聚类中心.

第四步: 重复第二步和第三步, 直到聚类中心不再改变时停止算法, 输出聚类结果.

显然, 初始聚类中心的选择对迭代时间和聚类结果产生的影响较大, 选不好的话很有可能严重偏离最优聚类. 在实际应用中, 通常选取多个不同的K值以及初始聚类中心, 选取其中表现最好的作为最终的初始聚类中心. 怎么算表现最好呢? 能满足业务需求的, 且簇内距离最小的.

簇内距离可以簇内离差平方和表示: 

其中, K表示K个簇, nj表示第j个簇中的样本个数, xi表示样本, uj表示第j个簇的质心, K-means算法中质心可以表示为:

3. 优缺点及注意事项

优点:

1. 原理简单, 计算速度快

2. 聚类效果较理想.

缺点:

1. K值以及初始质心对结果影响较大, 且不好把握.

2. 在大数据集上收敛较慢.

3. 由于目标函数(簇内离差平方和最小)是非凸函数, 因此通过迭代只能保证局部最优.

4. 对于离群点较敏感, 这是由于其是基于均值计算的, 而均值易受离群点的影响.

5. 由于其是基于距离进行计算的, 因此通常只能发现"类圆形"的聚类.

注意事项:

1. 由于其是基于距离进行计算的, 因此通常需要对连续型数据进行标准化处理来缩小数据间的差异.(对于离散型, 则需要进行one-hot编码)

2. 如果采用欧几里得距离进行计算的话, 单位的平方和的平方根是没有意义的, 因此通常需要进行无量纲化处理

4. 实际案例应用

1. 数据来源及背景

数据来源: http://archive.ics.uci.edu/ml/machine-learning-databases/00292/, 它是某批发经销商的客户数据, 其中包含440个样本以及8个特征,

下面将通过划分聚类中的k-means算法对这些客户进行细分.

2. 数据概览

1. 前2行和后2行

import pandas as pd
df = pd.read_csv(r'D:\aPython\Data\DataVisualization\Wholesale customers data.csv')
pd.set_option('display.max_rows', 4)
df

8个字段分别表示批发商客户: 渠道, 地区, 以及每年分别在新鲜产品, 奶制品, 食品杂货, 冷冻产品, 洗涤剂和纸制品, 熟食这6种产品上的类别上的消费支出.

2. 查看数据整体信息

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 440 entries, 0 to 439
Data columns (total 8 columns):
Channel             440 non-null int64
Region              440 non-null int64
Fresh               440 non-null int64
Milk                440 non-null int64
Grocery             440 non-null int64
Frozen              440 non-null int64
Detergents_Paper    440 non-null int64
Delicassen          440 non-null int64
dtypes: int64(8)
memory usage: 27.6 KB

没有缺失值, 且均为整数型.

3. 描述性统计

df.describe()

有两种渠道来源, 和三种地区, 各自分别占多少还需要进一步探索. 

新鲜产品每年支出: 范围3~112151, 中位数为8504, 均值为12000, 呈现右偏分布

奶制品每年支出: 范围55~73498, 中位数为3627, 均值为5796, 呈现右偏分布

食品杂货每年支出: 范围3~92780, 中位数为4756, 均值为7951, 呈现右偏分布

冷冻产品每年支出: 范围25~60869, 中位数为1526, 均值为3072, 呈现右偏分布

洗涤剂和纸制品每年支出: 范围3~40827, 中位数为817, 均值为2881, 呈现右偏分布

熟食每年支出: 范围3~47943, 中位数为965, 均值为1525, 呈现右偏分布

4. 偏态和峰态

for i in range(2, 8):
    name = df.columns[i]
    print(name)
    print('    偏态系数为 {0}, 峰态系数为 {1}'.format(df[name].skew(), df[name].kurt()))
Fresh
    偏态系数为 2.561322751927935, 峰态系数为 11.536408493056006
Milk
    偏态系数为 4.053754849210881, 峰态系数为 24.669397750673077
Grocery
    偏态系数为 3.5874286903915453, 峰态系数为 20.914670390919653
Frozen
    偏态系数为 5.9079856924559575, 峰态系数为 54.68928069737255
Detergents_Paper
    偏态系数为 3.6318506306913645, 峰态系数为 19.009464335418212
Delicassen
    偏态系数为 11.151586478906117, 峰态系数为 170.69493933454066

可以看到均表现为尖峰, 高度偏态分布.

3. 数据预处理

没有缺失值, 因此不用缺失值处理

1. 异常值

在处理异常值之前, 先来通过箱线图看看异常值.

import seaborn as sns
import matplotlib.pyplot as plt
def get_boxplot(data, start, end):
    fig, ax = plt.subplots(1, end-start, figsize=(24, 4))
    for i in range(start, end):
        sns.boxplot(y=data[data.columns[i]], data=data, ax=ax[i-start])
get_boxplot(df, 2, 8)

 

 可以看到以上6个连续型变量均有不同程度的异常值, 由于k-means算法对异常值较敏感, 因此选择剔除它

def drop_outlier(data, start, end):
    for i in range(start, end):
        field = data.columns[i]
        Q1 = np.quantile(data[field], 0.25)
        Q3 = np.quantile(data[field], 0.75)
        deta = (Q3 - Q1) * 1.5
        data = data[(data[field] >= Q1 - deta) & (data[field] <= Q3 + deta)]
    return data
del_df = drop_outlier(df, 2, 8)
print("原有样本容量:{0}, 剔除后样本容量:{1}".format(df.shape[0], del_df.shape[0]))
get_boxplot(del_df, 2, 8)
原有样本容量:440, 剔除后样本容量:318

 

在剔除一次异常值之后, 6个连续变量的波动幅度也都都大致接近, 你可能会问为什么还有异常值存在? 现在的异常值是相对于新数据集产生的, 而我们把原数据集中的异常值已经剔除了, 通常来说, 对于异常值只需剔除一次即可, 如果彻底剔除的话, 样本容量可能会有大幅度的变化, 比如:

df_new = df.copy()
#直到第10次的时候图像上才没有出现异常值
for i in range(10):
    df_new = drop_outlier(df_new, 2, 8)
print("原有样本容量:{0}, 彻底剔除后样本容量:{1}".format(df.shape[0], df_new.shape[0]))
get_boxplot(df_new, 2, 8)
原有样本容量:440, 彻底剔除后样本容量:97

可以看到现在的数据集中已经不存在异常了, 但是样本容量也从440大幅度下降为97, 因此这里不建议彻底删除.

4. 可视化分析

这里直接采用seaborn的pairplot方法

import seaborn as sns
sns.pairplot(del_df)

变量之间存在不同程度的相关关系, 以及在剔除异常值之后连续型变量依然表现为高度偏态分布.

5. 特征工程

1. 离散型变量

将离散型变量处理成哑变量.

del_df['Channel'] = del_df.Channel.astype(str)
del_df['Region'] = del_df.Region.astype(str)
del_df = pd.get_dummies(del_df)

2. 连续型变量

 由于连续型变量的数值范围有大有小, 为消除其对聚类结果的影响, 这里采用z-score进行归一化处理

for i in range(6):
    field = del_df.columns[i]
    del_df[field] = del_df[field].apply(lambda x: (x - del_df[field].mean()) / del_df[field].mean())

6. 聚类模型

1. 构建K=2的聚类模型

from sklearn.cluster import KMeans
km = KMeans(n_clusters=2, random_state=10)
km.fit(del_df)
print(km.cluster_centers_) 
print(km.labels_)
[[ 0.08057098 -0.36005276 -0.42021772  0.11899282 -0.66737726 -0.10885484
   0.97333333  0.02666667  0.2         0.10222222  0.69777778]
 [-0.19492979  0.8710954   1.01665578 -0.28788585  1.61462241  0.26335849
   0.13978495  0.86021505  0.10752688  0.07526882  0.8172043 ]]
[1 1 0 1 1 1 0 1 1 0 1 1 1 0 1 1 0 0 0 1 0 0 0 0 0 0 0 1 1 1 0 1 1 1 0 0 1
 0 0 1 0 1 1 1 1 0 0 1 0 0 1 0 0 0 0 1 1 0 1 0 0 1 0 1 0 0 0 1 1 1 0 0 1 1
 0 1 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 1 0 1 1 1 0 0 1 1 0 0 0 1 0 0 1 0 0 0 0 1 0 0 0 1 1 1 0 0 1 0 1 0 0 0
 0 0 1 0 0 0 1 1 1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0
 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 1 0 0 0 1 1 0 1 1 1
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0
 0 0 1 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 1
 0 1 0 1 1 1 1 0 1 1 0 0 1 1 0 0 0 0 0 0 0 0]

将客户分为2类到底合不合适呢? 通过迭代的方式来选择.

2. 迭代选择合适的k值

import matplotlib.pyplot as plt
K = range(1, 10)
sse = []
for k in K:
    km = KMeans(n_clusters=k, random_state=10)
    km.fit(del_df)
    sse.append(km.inertia_)
plt.figure(figsize=(8, 6))
plt.plot(K, sse, '-o', alpha=0.7)
plt.xlabel("K")
plt.ylabel("SSE")
plt.show()

 

根据肘部法则, 选择K=2, 也就是说将客户分成两群.

3. 客户分群

from pandas.plotting import parallel_coordinates
#训练模型
km = KMeans(n_clusters=2, random_state=10)
km.fit(del_df)
centers = km.cluster_centers_ 
labels =  km.labels_
customer = pd.DataFrame({'0': centers[0], "1": centers[1]}).T
customer.columns = del_df.keys()
df_median = pd.DataFrame({'2': del_df.median()}).T
customer = pd.concat([customer, df_median])
customer["category"] = ["customer_1", "customer_2", 'median']
#绘制图像
plt.figure(figsize=(12, 6))
parallel_coordinates(customer, "category", colormap='flag'')
plt.xticks(rotation = 15)
plt.show()

从6种产品每年消费支出来看, 客户群1在冷冻产品上最高, 在洗涤剂和纸制品上最低, 而客户群2则在冷冻产品上最低, 在洗涤剂和纸制品上最高, 且客户群2在6种产品的消费支出均高于中位数水平, 因此客户群2为重要客户, 客户群1则是一般客户.

4. 最终分群结果

#将聚类后的标签加入数据集
del_df['category'] = labels
del_df['category'] = np.where(del_df.category == 0, 'customer_1', 'customer_2')
customer = pd.DataFrame({'0': centers[0], "1": centers[1]}).T
customer["category"] = ['customer_1_center', "customer_2_center"]
customer.columns = del_df.keys()
del_df = pd.concat([del_df, customer])
#对6类产品每年消费水平进行绘制图像
df_new = del_df[['Fresh', 'Milk', 'Grocery', 'Frozen', 'Detergents_Paper', 'Delicassen', 'category']]
plt.figure(figsize=(18, 6))
parallel_coordinates(df_new, "category", colormap='cool')
plt.xticks(rotation = 15)
plt.show()

 通过下图可以看到, 最终的聚类效果较理想, 其中客户群1(一般客户)在图像上表现较为密集, 代表其数量多, 而客户群2(重要客户)在图像上则较稀疏, 数量较少.

参考资料:

网易云课堂《吴恩达机器学习》

《数据分析与挖掘实战》

 

声明: 本文仅用于学习交流

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:Python|一文简单看懂 深度&amp;广度 优先算法

下一篇:flask二