结合航空公司客户价值分析的案例介绍K-Means聚类算法在客户价值分析中的应用。此外,介绍基于RFM客户价值分析模型的不足,使用K-Means算法构建航空客户价值分析LRFMC模型,详细的描述数据分析的整个过程。
(1) 了解RFM模型的基本原理。
(2) 掌握K-Means算法的基本原理与使用方法。
(3) 比较不同类别客户的客户价值,制定相应的营销策略。
(1) RFM模型的基本原理。
(2) K-Means算法的基本原理与使用方法。
(3) 比较不同类别客户的客户价值,制定相应的营销策略。
(1) 航空客户价值分析的步骤和流程。
(2) RFM模型的基本原理。
(3) K-Means算法的基本原理与使用方法。
(4) 比较不同类别客户的客户价值。
(1) RFM模型的基本原理。
(2) KMeans算法的基本原理与使用方法。
(1) 分析航空公司现状。
(2) 认识客户价值分析。
(3) 熟悉航空客户价值分析的步骤与流程。
(4) 处理缺失值与异常值。
(5) 构建爱你航空客户价值分析关键特征。
(6) 标准化LRFMC 5个特征。
(7) 了解K-Means聚类算法。
(8) 分析聚类结果。
(9) 模型应用。
(1) 处理数据缺失值与异常值。
(2) 构建航空客户价值分析的关键特征。
(3) 标准化LRFMC 5个特征。
(4) 构建K-Means聚类模型。
(5) 评价K-Means聚类模型。
首先调用describe()函数对数据进行一个大致的了解,主要是查看缺失值和异常值。
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
airline_data = pd.read_csv("../data/air_data.csv",
encoding="gb18030") # 导入航空数据
print('原始数据的形状为:', airline_data.shape)
print(type(airline_data))
print(airline_data.describe().T)
explore = airline_data.describe().T # 对数据的统计性描述,T是我进行了转置
explore['null'] = len(airline_data) - explore['count']
df = explore[['max', 'min', 'null']]
df.to_excel("数据描述.xls")
根据数据的格式,发现数据的行少列多,无法全部显示,因此使用原Dataframe的转置,显示结果如下
第一列表示最大值,第二列最小值,第三列为空值个数,可以发现空值数量不多,可以直接删除这些无效数据。
exp1 = airline_data["SUM_YR_1"].notnull()
exp2 = airline_data["SUM_YR_2"].notnull()
exp = exp1 & exp2
'''
多条件筛选 使用&,DF6[(DF6.B>1) & (DF6.D > 10)]
两个筛选结果与好像也可以
'''
print('exp的形状是:', exp.shape)
airline_notnull = airline_data.loc[exp, :]
丢弃第一年或第二年票价为0,平均折扣率不为0且总飞行公里数大于零的记录。
index1 = airline_notnull['SUM_YR_1'] != 0
index2 = airline_notnull['SUM_YR_2'] != 0
index3 = (airline_notnull['SEG_KM_SUM'] > 0) & \
(airline_notnull['avg_discount'] != 0)
airline = airline_notnull[(index1 | index2) & index3]
评估航空公司客户价值通常根据LRFMC模型:
客户关系长度L、消费时间间隔R、消费频率F、飞行里程M、折扣系数的平均值C
与其相关的只有六个属性:
LOAD_TIME、FFP_DATE、LAST_TO_END、FLIGHT_COUNT、SEG_KM_SUMAVG_DISCOUNT。
关系如下:
L = LOAD_TIME - FFP_DATE
会员入会时间距观测窗口结束的月数=观测窗口结束的时间-入会时间
R=LAST_TO_END
客户最近一次乘坐公司飞机距观测窗口结束的月数=最后一次乘机时间至观察窗口末端时长[单位:月] F=FLIGHT_COUNT
客户在观测窗口内乘坐公司飞机的次数=观测窗口的飞行次数[单位:次]
M=SEG_KM_SUM
客户在观测时间内在公司累计的飞行里程=观测窗口的总飞行公里数[单位:公里]
C=AVG_DISCOUNT
折扣率
那么只要筛选这些数据即可
airline_selection = airline[["FFP_DATE", "LOAD_TIME",
"FLIGHT_COUNT", "LAST_TO_END",
"avg_discount", "SEG_KM_SUM"]]
'''
选取需要的特征值:入会时间、观测窗口结束时间(即本组数据的时间宽度)、飞行次数、
最后一次飞行时间到观测窗口结束的时间间隔、平局折扣率、本窗口内的总飞行距离
L(入会至当前时间的间隔,反映可能的活跃时长)、
R(最近消费时间距当前的间隔,反映当前的活跃状态)、
F(乘机次数,反映客户的忠诚度)、
M(飞行里程数,反映客户对乘机的依赖性)、
C(舱位等级对应的折扣系数,侧面反映客户价值高低)
'''
L = pd.to_datetime(airline_selection["LOAD_TIME"]) - \
pd.to_datetime(airline_selection["FFP_DATE"])
# print(L)
L = L.astype("str").str.split().str[0]
# print(L)
L = L.astype("int") / 30
'''
强制把datetime修改为str类型,以空格为分隔符进行分割,为什么?
打印之后发现数据格式为 2706 days,分隔之后去掉days,保留下来的就是天数
分析下来L就等于 (LOAD_TIME - FFP_DATE)/30,其中两个时间都是以天为单位
计算了用户如会到现在的月数
'''
除了L特征需要计算,其余都可以直接获得。
airline_features = pd.concat([L,airline_selection.iloc[:, 2:]],axis=1)
把计算得到的L特征和其他特征合并。
data = StandardScaler().fit_transform(airline_features)
数据标准化,降维,归一化等操作。
StandardScaler就是对数据进行归一化和标准化, 不仅计算训练数据的均值和方差,还会基于计算出来的均值和方差来转换训练数据,从而把数据转换成标准的正太分布,限制在固定的区间里。
airline_scale = np.load('../tmp/airline_scale.npz')['arr_0']
k = 5 # 聚类中心数
kmeans_model = KMeans(n_clusters=k, n_jobs=4, random_state=123)
# 聚类中心数,并行的CPU核的数量,随机数种子
fit_kmeans = kmeans_model.fit(airline_scale) # 模型训练
print(kmeans_model.cluster_centers_) # 查看聚类中心
print(kmeans_model.labels_) # 查看样本的类别标签
用kmeans分为五个聚类,每个聚类内部的数据为一个list,五个list组成聚类中心。
labels显示按照kmeans划分之后每个数据属于哪个聚类。
# 统计不同类别样本的数目
r1 = pd.Series(kmeans_model.labels_).value_counts()
print('最终每个类别的数目为:\n', r1)
result = kmeans_model.predict([[1.5, 1.5, 1.5, 1.5, 1.5]])
print(result)
# 最终确定在五个参数都是1.5的情况下的用户属于类别1
r1显示每个聚类内部的元素个数,同时测试一组特定特征值的数据会被分配到哪个组中。
对于之前建立的kmeans进行评价。
随机数种子,用于随机划分数据为训练集和测试集。
这种评价法应该是针对已经有标签的数据(label-true),采用不同的聚类数量进行计算,与源数据的标签进行匹配,得出训练模型与原先数据的匹配程度。
for i in range(2, 7):
kmeans = KMeans(n_clusters=i, random_state=123).fit(airline_scale)
# print(kmeans.labels_)
# print(type(kmeans.labels_))
score = fowlkes_mallows_score(kmeans_model.labels_, kmeans.labels_)
print('iris数据聚%d类FMI评价分值为:%f' % (i, score))
因为原先的数据没有标签,所以选取聚类数为5的数据作为基准,比较不同聚类数量的优越性。
可以总结出,聚类数为5是区间内的最优选择。
结合内聚度和分离度两种因素。
可以用来在相同原始数据的基础上用来评价不同算法、或者算法不同运行方式对聚类结果所产生的影响
1.计算样本i到同簇其他样本的平均距离ai。ai 越小,说明样本i越应该被聚类到该簇。
将ai 称为样本i的簇内不相似度,某一个簇C中所有样本的a i 均值称为簇C的簇不相似度
2.计算样本i到其他某簇Cj的所有样本的平均距离bij,称为样本i与簇Cj的不相似度。
某一个样本的簇间不相似度为该样本到所有其他簇的所有样本的平均距离中最小的那一个
si接近1,则说明样本i聚类合理;
si接近-1,则说明样本i更应该分类到另外的簇;
si 近似为0,则说明样本i在两个簇的边界上。
所有样本的s i 的均值称为聚类结果的轮廓系数,定义为S,是该聚类是否合理、有效的度量。聚类结果的轮廓系数的取值在【-1,1】之间,值越大,说明同类样本相距约近,不同样本相距越远,则聚类效果越好。
silhouettteScore = []
for i in range(2, 7):
kmeans = KMeans(n_clusters=i, random_state=123).fit(airline_scale)
score = silhouette_score(airline_scale, kmeans.labels_)
print('航空公司数据聚%d类silhouette评价分值为:%f' % (i, score))
silhouettteScore.append(score)
plt.figure(figsize=(10, 6))
plt.plot(range(2, 7), silhouettteScore, linewidth=1.5, linestyle="-")
plt.show()
Calinski-Harabasz分数值ss越大则聚类效果越好。
for i in range(2, 7):
# 构建并训练模型
kmeans = KMeans(n_clusters=i, random_state=123).fit(airline_scale)
score = calinski_harabasz_score(airline_scale, kmeans.labels_)
print('iris数据聚%d类calinski_harabaz指数为:%f' % (i, score))
score = fowlkes_mallows_score(kmeans_model.labels_, kmeans.labels_)
score = silhouette_score(airline_scale, kmeans.labels_)
score = calinski_harabasz_score(airline_scale, kmeans.labels_)
外部度量:需要知道真实标签的度量
对于不需要外部度量的情况下,可以直接使用kmeans算法自己产生的标签,而对于需要的FMI算法,必须要有本身带标签的数据,因此参数是前一次kmeans算法得到的数据。
工具:支持向量机SVC
支持向量机(support vector machine)是一种分类算法,也可以做回归,根据输入的数据不同可做不同的模型(若输入标签为连续值则做回归,若输入标签为分类值则用SVC()做分类)。
可以确定需要输入标签,即监督学习。
目的是测试之前聚类为的kmeans模型,因此输入参数是航空公司数据和标签。
airline_data_train, airline_data_test, \
airline_target_train, airline_target_test = \
train_test_split(airline_scale, kmeans_model.labels_,
test_size=0.2, random_state=22)
对数据进行标准化,训练和测试数据来自同一源,因此可以同同一个归一化函数。
stdScaler = StandardScaler().fit(airline_data_train)
airline_trainStd = stdScaler.transform(airline_data_train)
airline_testStd = stdScaler.transform(airline_data_test)
svm = SVC().fit(airline_trainStd, airline_target_train)
print('建立的SVM模型为:\n', svm)
# 预测训练集结果
airline_target_pred = svm.predict(airline_testStd)
print('预测前20个结果为:\n', airline_target_pred[:20])
datafile = 'standard_data.csv'
data = pd.read_csv(datafile)
r1 = pd.Series(kmeans_model.labels_).value_counts() # 聚类内部的个数
r2 = pd.DataFrame(kmeans_model.cluster_centers_) # 聚类的数学数值
r3 = pd.Series(['客户群1', '客户群2', '客户群3', '客户群4', '客户群5', ])
labels = np.array(list(data.columns)) # 标签
print(labels)
dataLenth = 5 # 数据个数
r4 = r2.T
r4.columns = list(data.columns)
fig = plt.figure()
y = []
for x in list(data.columns):
dt = r4[x]
dt = np.concatenate((dt, [dt[0]]))
y.append(dt)
ax = fig.add_subplot(111, polar=True)
angles = np.linspace(0, 2 * np.pi, dataLenth, endpoint=False)
angles = np.concatenate((angles, [angles[0]]))
# labels = np.concatenate(labels,[labels[0]])
labels = np.concatenate((labels, [labels[0]]))
ax.plot(angles, y[0], 'b-', linewidth=2)
ax.plot(angles, y[1], 'r-', linewidth=2)
ax.plot(angles, y[2], 'g-', linewidth=2)
ax.plot(angles, y[3], 'y-', linewidth=2)
ax.plot(angles, y[4], 'm-', linewidth=2)
plt.rcParams['font.sans-serif'] = ['SimHei']
ax.legend(r3, loc=1)
ax.set_thetagrids(angles * 180 / np.pi, labels, fontproperties="SimHei")
ax.set_title("matplotlib雷达图", va='bottom', fontproperties="SimHei")
ax.grid(True)
plt.show()
这里有一个细节,labels需要的是一个列表里的列表,单纯的列表是不能画图的。
上图可以很清晰的看到每个客户群的指标情况,将每个客户群的优势特征,劣势特征总结如下:
优势特征:
客户群1:R
客户群2:F、M、L
客户群3:L
客户群4:C
客户群5:无
劣势特征:
客户群1:F、M
客户群2:R
客户群3:R、F、M、C
客户群4:F、M
客户群5:R、L、C
基于LRFMC模型的具体含义,我们可以对这5个客户群进行价值排名。
同时,将这5个客户群重新定义为五个等级的客户类别:重要保持客户,重要挽留客户,重要发展客户,一般客户,低价值客户。
重要保持客户:这类客户平均折扣率(C)和入会员时间(L)都很高(入会员时间越长,会员级别越高,折扣越大),最近乘坐过本航班时间间隔(R)低,乘坐的次数(F)或(M)高。说明他们经常乘坐飞机,且有一定经济实力,是航空公司的高价值 客户。对应客户群2。
重要发展客户:这类客户平均折扣率高(C),最近乘坐过本航班时间间隔(R)短,但是乘坐的次数(F)和(M)都很低。说明这些乘客刚入会员不久,所以乘坐飞机次数少,是重要发展客户,对应客户群4。
重要挽留客户:这类客户入会时间长(L),最近乘坐过本航班时间间隔(R)较长,里程数和乘坐次数都变低,为重要挽留客户。对应客户群3
一般与低价值客户:这类客户乘坐时间间隔长(R)或乘坐次数(F)和总里程(M)低,平均折扣也很低。对应客户群5和客户群1.
tsne = TSNE(n_components=2, init='random',
random_state=177).fit(airline_scale)
# init:初始化,可以是PCA或random;随机数种子
df = pd.DataFrame(tsne.embedding_) # 将原始数据转换为DataFrame
print(df)
df['labels'] = kmeans_model.labels_ # 将聚类结果存储进df数据表
# 提取不同标签的数据
df1 = df[df['labels'] == 0]
df2 = df[df['labels'] == 1]
df3 = df[df['labels'] == 2]
df4 = df[df['labels'] == 3]
df5 = df[df['labels'] == 5]
# 绘制图形
fig = plt.figure(figsize=(9, 6)) # 设定空白画布,并制定大小
# 用不同的颜色表示不同数据
plt.plot(df1[0], df1[1], 'bo', df2[0], df2[1], 'r*',
df3[0], df3[1], 'gD',df4[0], df4[1], 'kD',df5[0], df5[1], 'lD')
plt.savefig('../tmp/聚类结果.png')
plt.show() # 显示图片
df1 = df[df[‘labels’] == 0]
字面上,K即原始数据最终被聚为K类或分为K类,Means即均值点。K-Means的核心就是将一堆数据聚集为K个簇,每个簇中都有一个中心点称为均值点,簇中所有点到该簇的均值点的距离都较到其他簇的均值点更近。
基于划分的方法(Partitional):K-means(K均值)
1 随机选取K个数据点作为‘种子’
2 根据数据点与‘种子’的距离大小进行类分配
3 更新类中心点的位置,以新的类中心点作为‘种子’
4 按照新的‘种子’对数据归属的类进行重新分配
5 更新类中心点(–>3–>4),不断迭代,直到类中心点变得很小
缺点:
K是事先给定的,K值选定难确定
对孤立点、噪声敏感
结果不一定是全局最优,只能保证局部最优。
包内模型是基于欧式距离的,如果要是用别的标准就要自己实现。
import numpy as np
import matplotlib.pyplot as plt
'''标志位统计递归运行次数'''
flag = 0
'''欧式距离'''
def ecludDist(x, y):
return np.sqrt(sum(np.square(np.array(x) - np.array(y))))
'''曼哈顿距离'''
def manhattanDist(x, y):
return np.sum(np.abs(x - y))
'''夹角余弦'''
def cos(x, y):
return np.dot(x, y)/(np.linalg.norm(x) * np.linalg.norm(y))
'''计算簇的均值点'''
def clusterMean(dataset):
return sum(np.array(dataset)) / len(dataset)
'''生成随机均值点'''
def randCenter(dataset, k):
temp = []
while len(temp) < k:
index = np.random.randint(0, len(dataset)-1)
if index not in temp:
temp.append(index)
return np.array([dataset[i] for i in temp])
'''以数据集的前k个点为均值点'''
def orderCenter(dataset, k):
return np.array([dataset[i] for i in range(k)])
'''聚类'''
def kMeans(dataset, dist, center, k):
global flag
#all_kinds用于存放中间计算结果
all_kinds = []
for _ in range(k):
temp = []
all_kinds.append(temp)
#计算每个点到各均值点的距离
for i in dataset:
temp = []
for j in center:
temp.append(dist(i, j))
all_kinds[temp.index(min(temp))].append(i)
#打印中间结果
for i in range(k):
print('第'+str(i)+'组:', all_kinds[i], end='\n')
flag += 1
print('************************迭代'+str(flag)+'次***************************')
#更新均值点
center_ = np.array([clusterMean(i) for i in all_kinds])
if (center_ == center).all():
print('结束')
for i in range(k):
print('第'+str(i)+'组均值点:', center_[i], end='\n')
plt.scatter([j[0] for j in all_kinds[i]], [j[1] for j in all_kinds[i]], marker='*')
plt.grid()
plt.show()
else:
#递归调用kMeans函数
center = center_
kMeans(dataset, dist, center, k)
def main(k):
'''生成随机点'''
x = [np.random.randint(0, 50) for _ in range(50)]
y = [np.random.randint(0, 50) for _ in range(50)]
points = [[i,j] for i, j in zip(x, y)]
plt.plot(x, y, 'b.')
plt.show()
initial_center = randCenter(dataset=points, k=k)
kMeans(dataset=points, dist=ecludDist, center=initial_center, k=k)
if __name__ == '__main__':
main(3)
如果使用夹角余弦的话,需要将kMeans()函数中all_kinds[temp.index(min(temp))].append(i)语句里的min改为max
欧氏距离和曼哈顿距离,都是计算结果越小表示两个点越相近,而夹角余弦则是计算结果越大表示两点越相近
最合适的K值
肘部法则(Elbow method):找到随着K值变大,损失函数的拐点。
损失函数:各个类畸变程度(distortions)之和
肘方法的核心指标是 SSESSE (sum of the squared errors,误差平方和),CiCi是第 ii 个簇,pp 是 CiCi 中的样本点,mimi是CiCi的质心(CiCi中所有样本的均值),SSE是所有样本的聚类误差,代表了聚类效果的好坏。
SSE是每个属性的SSE之和:
1. 对于所有的簇,某变量的SSE都很低,都意味着什么?
2. 如果只对一个簇很低,意味着什么?
3. 如果只对一个簇很高,意味着什么?
4. 如果对所有簇都很高,意味着什么?
5. 如何使用每个变量的SSE信息改进聚类?
解答:
1. 说明该属性本质上为常量,不能作为聚类依据。
2. 那么该属性有助于该簇的定义
3. 那么该属性为噪声属性
4. 那么该属性 与 定义该属性提供的信息不一致,也意味着该属性不利于簇的定义。
5. 消除对于所有簇都是 低的SSE(高的SSE)的属性。因为这些属性对聚类没有帮助,
这些属性在SSE的总和计算中引入了噪声。
也可以对其中某些属性用加权概率来计算,使该属性有助于该簇的定义,
去除某些不利于该簇定义的影响因子(那些可能是噪声)。从而更有利于簇的聚类。
K-means 附加问题
1.处理空簇:如果数据量少,寻找替补质心,使SSE最小。如果数据量大,保留该空簇
2.离群点:不能删除。建议聚类之前离群检测。分析看能否删除
3.降低SSE :将大的分散的簇再次拆开;引入新的簇将之前的大簇拆分。
4.增量更新质心:再次在质心附近寻找测试点,看能否再次找到更优的质心。