当前位置: 移动技术网 > IT编程>脚本编程>Python > Python聚类模型原理及应用--批发商客户分群

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

2019年01月07日  | 移动技术网IT编程  | 我要评论

青涩门,儿女传奇闹婚记,后羿SEO

前言

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

目录

 

 

 

正文

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

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(重要客户)在图像上则较稀疏, 数量较少.

参考资料:

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

《数据分析与挖掘实战》

 

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网