一、项目背景
客户关系管理是信息时代的企业核心问题。
客户关系管理的关键在于客户分类
- 用以区分无价值客户、高价值客户
- 用以区分无价值客户、高价值客户
- 针对不同价值的客户制定个性化服务方案
通过建立客户价值评估模型
- 对客户进行分群
- 制定相应营销策略。
某航空公司积累了大量的会员信息档案和其航班记录,根据这些数据,实现以下目标:
1.根据航空公司客户数据,对客户进行分类
2.对不同的客户类别进行特征分析,比较不同类客户的价值所在
3.对不同价值的客户类别提供个性化服务,制定相应的营销策略
本练习选用数据说明:
- 以2014-03-31为结束时间,选择窗口宽度为两年。
- 抽取公司系统内的客户基本信息、乘机信息及积分信息等
- 根据末次飞行日期(LAST_FLIGHT_DATE)抽取2012年-04-01至2-14-03-31时间段内所有乘客详细信息
- 抽取的数据中包含会员卡号、入会时间、性别、年龄等44个属性
二、数据分析
本次练习的主要目标是客户价值识别。
常用的客户价值分析模型RFM(消费时间间隔-Recency、消费频率-Frequency、消费金额-Monetary)在本次练习中,稍有不适,因此,基于RFM模型,进行一定的改动。
- 由于航空票价受运输距离、舱位等级等因素影响(一位长距离低等级舱位票的乘客,其购票金额大于一位短距离高等级舱位的乘客票价,但后者价值更高)
- 因此,在本次练习中,选择客户在一定时间内积累的飞行里程M和客户在一定时间内乘坐舱位所对应的折扣系数的平均值C两个指标代替消费金额。
- 另外,航空公司会员的入会时间长短,在一定程度上也能够影响客户价值,所以,在模型中再增加一个客户关系长度L
因此,本次练习中,将采用RFM的优化模型 – LRFMC模型
1.数据清洗
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
import pandas as pd
data = pd.read_csv('./air_data.csv',encoding='utf-8')
display(data.describe(percentiles=[],include='all').T )# 获取所有列的基础描述性信息,并转置,方便观察
data.isnull().sum()
# 丢弃票价为空的记录
data = data[data['SUM_YR_1'].notnull() * data['SUM_YR_2'].notnull()]
# 保留票价非零,但平均折扣率与总飞行里程同时为0的记录
index1 = data['SUM_YR_1'] != 0
index2 = data['SUM_YR_2'] != 0
index3 = (data['SEG_KM_SUM'] == 0) & (data['avg_discount'] == 0)
data = data[index1 | index2 | index3]
删除后剩余的样本值是62044个,可见异常样本的比例不足1.5%,因此不会对分析结果产生较大的影响。
2.数据处理
原始数据集的特征属性太多,且很多特征的意义重复,故从这里选取几个适合LRFMC模型的,对航空公司来说比较有价值的几个特征进行分析,这里并没有完全按照一般的做法选取特征,最终选取的特征是第一年总票价、第二年总票价、观测窗口总飞行公里数、飞行次数、平均乘机时间间隔、观察窗口内最大乘机间隔、入会时间、观测窗口的结束时间、平均折扣率这八个特征。下面说明这么选的理由:
- 选取的特征是第一年总票价、第二年总票价、观测窗口总飞行公里数是要计算平均飞行每公里的票价,因为对于航空公司来说并不是票价越高,飞行公里数越长越能创造利润,相反而是那些近距离的高等舱的客户创造更大的利益。
- 当然总飞行公里数、飞行次数也都是评价一个客户价值的重要的指标
入会时间可以看出客户是不是老用户及忠诚度。 - 通过平均乘机时间间隔、观察窗口内最大乘机间隔可以判断客户的乘机频率的波动程度。
- 平均折扣率可以反映出客户给公里带来的利益,毕竟来说越是高价值的客户享用的折扣率越高。
filter_data = data[[ "FFP_DATE", "LOAD_TIME", "FLIGHT_COUNT", "SUM_YR_1", "SUM_YR_2", "SEG_KM_SUM", "AVG_INTERVAL" , "MAX_INTERVAL", "avg_discount"]]
filter_data.head(5)
对特征进行变换:
data["LOAD_TIME"] = pd.to_datetime(data["LOAD_TIME"])
data["FFP_DATE"] = pd.to_datetime(data["FFP_DATE"])
data["入会时间"] = data["LOAD_TIME"] - data["FFP_DATE"]
data["平均每公里票价"] = (data["SUM_YR_1"] + data["SUM_YR_2"]) / data["SEG_KM_SUM"]
data["时间间隔差值"] = data["MAX_INTERVAL"] - data["AVG_INTERVAL"]
deal_data = data.rename(
columns = {"FLIGHT_COUNT" : "飞行次数", "SEG_KM_SUM" : "总里程", "avg_discount" : "平均折扣率"},
inplace = False
)
filter_data = deal_data[["入会时间", "飞行次数", "平均每公里票价", "总里程", "时间间隔差值", "平均折扣率"]]
display(filter_data[0:5])
filter_data['入会时间'] = filter_data['入会时间'].astype(np.int64)/(60*60*24*10**9)
display(filter_data[0:5])
print(filter_data.info())
为了消除数据量纲的影响,这里选用zscore的方法进行数据标准化
# zscore数据标准化
from sklearn import preprocessing
zscore = preprocessing.StandardScaler()
filter_data_zscore = zscore.fit_transform(filter_data)
filter_data_zscore = pd.DataFrame(filter_data_zscore,
columns=["入会时间", "飞行次数", "平均每公里票价", "总里程", "时间间隔差值", "平均折扣率"])
filter_data_zscore.head(5)
3.数据建模
def test_Kmeans_nclusters(data_train):
"""
计算不同的k值时,SSE的大小变化
"""
data_train = data_train.values
nums=range(2,10)
SSE = []
for num in nums:
sse = 0
kmodel = KMeans(n_clusters=num, n_jobs=4)
kmodel.fit(data_train)
sse = kmodel.inertia_
print("簇数是",num , "时; SSE是", sse)
SSE.append(sse)
return nums, SSE
nums, SSE = test_Kmeans_nclusters(filter_zscore_data)
簇数是 2 时; SSE是 296587.83405503206
簇数是 3 时; SSE是 245317.3781816485
簇数是 4 时; SSE是 209299.71846504003
簇数是 5 时; SSE是 183886.25644561942
簇数是 6 时; SSE是 167465.10737832927
簇数是 7 时; SSE是 151869.14245056748
簇数是 8 时; SSE是 142922.45949377582
簇数是 9 时; SSE是 135003.3612680939
#画图,通过观察SSE与k的取值尝试找出合适的k值
# 中文和负号的正常显示
plt.rcParams['font.sans-serif'] = 'SimHei'
plt.rcParams['font.size'] = 12.0
plt.rcParams['axes.unicode_minus'] = False
# 使用ggplot的绘图风格
plt.style.use('ggplot')
## 绘图观测SSE与簇个数的关系
fig=plt.figure(figsize=(10, 8))
ax=fig.add_subplot(1,1,1)
ax.plot(nums,SSE,marker="+")
ax.set_xlabel("n_clusters", fontsize=18)
ax.set_ylabel("SSE", fontsize=18)
fig.suptitle("KMeans", fontsize=20)
plt.show()
观察图像,并没有的所谓的“肘”点出现,是随k值的增大逐渐减小的,这里选取当k分别取4, 5, 6时进行,看能不能通过分析结果来反向选取更合适的值,k取值4时的代码如下
for num in range(4,7):
kmodel = KMeans(n_clusters=num, n_jobs=4)
kmodel.fit(filter_zscore_data)
# 简单打印结果
r1 = pd.Series(kmodel.labels_).value_counts() #统计各个类别的数目
r2 = pd.DataFrame(kmodel.cluster_centers_) #找出聚类中心
# 所有簇中心坐标值中最大值和最小值
max = r2.values.max()
min = r2.values.min()
r = pd.concat([r2, r1], axis = 1) #横向连接(0是纵向),得到聚类中心对应的类别下的数目
r.columns = list(filter_zscore_data.columns) + [u'类别数目'] #重命名表头
# 绘图
fig=plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, polar=True)
center_num = r.values
feature = ["入会时间", "飞行次数", "平均每公里票价", "总里程", "时间间隔差值", "平均折扣率"]
N =len(feature)
for i, v in enumerate(center_num):
# 设置雷达图的角度,用于平分切开一个圆面
angles=np.linspace(0, 2*np.pi, N, endpoint=False)
# 为了使雷达图一圈封闭起来,需要下面的步骤
center = np.concatenate((v[:-1],[v[0]]))
angles=np.concatenate((angles,[angles[0]]))
# 绘制折线图
ax.plot(angles, center, 'o-', linewidth=2, label = "第%d簇人群,%d人"% (i+1,v[-1]))
# 填充颜色
ax.fill(angles, center, alpha=0.25)
# 添加每个特征的标签
ax.set_thetagrids(angles * 180/np.pi, feature, fontsize=15)
# 设置雷达图的范围
ax.set_ylim(min-0.1, max+0.1)
# 添加标题
plt.title('客户群特征分析图', fontsize=20)
# 添加网格线
ax.grid(True)
# 设置图例
plt.legend(loc='upper right', bbox_to_anchor=(1.3,1.0),ncol=1,fancybox=True,shadow=True)
# 显示图形
plt.show()
通过观察可知:
当k取值4时,每个人群包含的信息比较复杂,且特征不明显
当k取值5时,分析的结果比较合理,分出的五种类型人群都有自己的特点又不相互重复
当k取值6时,各种人群也都有自己的特点,但是第4簇人群完全在第5簇人群特征中包含了,有点冗余的意思
综上,当k取值为5时,得到最好的聚类效果,将所有的客户分成5个人群,再进一步分析可以得到以下结论:
第一簇人群,10957人
最大的特点是时间间隔差值最大,分析可能是“季节型客户”,一年中在某个时间段需要多次乘坐飞机进行旅行,其他的时间则出行的不多,这类客户我们需要在保持的前提下,进行一定的发展;
第二簇人群,14732人
最大的特点就是入会的时间较长,属于老客户按理说平均折扣率应该较高才对,但是观察窗口的平均折扣率较低,而且总里程和总次数都不高,分析可能是流失的客户,需要在争取一下,尽量让他们“回心转意”;
第三簇人群,22188人
各方面的数据都是比较低的,属于一般或低价值用户
第三簇人群,8724人
最大的特点就是平均每公里票价和平均折扣率都是最高的,应该是属于乘坐高等舱的商务人员,应该重点保持的对象,也是需要重点发展的对象,另外应该积极采取相关的优惠政策使他们的乘坐次数增加
第五簇人群,5443人
总里程和飞行次数都是最多的,而且平均每公里票价也较高,是重点保持对象
4.模型总结
总体而言,通过最终模型的对比,上述特征比起一般做法选取的总里程以及最近乘机间隔最后的聚类效果表现更有区别性,个人认为是更好一些的。
当然,总体数据中还有一些其他数据可以替代本文选用的特征或者添加,使用不同维度或更多维的特征来训练模型,在测试时也选用其他特征,最终表现效果大致相同,但是还是可以得出一些用户行为的区别,以及最终划分的用户分群数量也有数千人不等的变动,可以在实际运营中分别设置对应的用户群去测试分群的用户表现。