为航空公司进行客户价值分析
一. 业务目标
- 借助航空公司的客户数据,对客户进行分类
- 对不同的客户类别进行特征分析,比较不同类客户的客户价值
- 对不同价值的客户类别提供个性化服务,制定相应的营销策略
二. 已有数据
- 客户基本信息:会员卡号、入会时间、第一次飞行日期、性别、年龄、会员卡级别、工作地城市、工作地所在省份、工作地所在国家
- 乘机信息:周期内的飞行次数、周期的结束时间、最后一次乘机时间到此周期结束的时长、平均折扣率、周期内的票价收入、周期内的总飞行公里数、末次飞行日期、平均乘机间隔时间、最大乘机间隔
- 积分信息:积分兑换次数,总精英积分、促销积分、合作伙伴积分、总累计积分、非乘机的积分变动次数、总基本积分
三. 分析思路
分析客户价值的标准模型师RFM模型,参见:http://baike.baidu.com/link?url=KZP2fpmmzOhzrGM_rHN2SSFBo4ilAbvLCgqS8P1QQQo5vlO7xIY0TedyRzLu7V4nCSXyeD4TodSAlqyFDypNTa 即:R:最近一次消费,F:消费频率,M:消费金额 考虑到航空公司的业务特征:
- 同样的消费金额,客户价值可能不同。比如说长航线经济舱的客户跟短航线头等舱的客户虽然金额一样,单后者显然更有价值。所以我们需要考虑客户的里程跟机票的折扣。
- 会员时间越长,越忠诚,价值越高。所以要考虑是不是会员以及入会时间。
所以最终设计从5个指标来评价客户价值:
- L:会员入会时间距观测窗口结束时的月数
- R:客户最后一次乘坐公司飞机距观测窗口结束的月数
- F:客户在观测周期内乘坐公司飞机的次数
- M:客户在观测窗口内累计的飞行里程
- C:客户在观测窗口内乘坐舱位所对应的折扣系数均值
四. 算法选择
选择k-means聚类算法
五. 挖掘过程
1.数据抽取
选取一段时间内的数据,比如2012年到2014年两年的数据,作为分析样本。将客户基本信息,乘机信息,积分信息的44个属性形成一张大宽表,即air_data.csv。
2.数据探索
对原始数据进行初步的感知。识别出有没有缺失值和明显的异常值。
In [1]:
import pandas as pd |
In [2]:
datafile = '/Users/frontc/book/ppdam/air_data.csv' |
In [3]:
data = pd.read_csv(datafile,encoding='utf-8') |
In [4]:
len(data) |
Out[4]:
62988 |
In [5]:
data.describe().T |
Out[5]:
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
MEMBER_NO | 62988.0 | 31494.500000 | 18183.213715 | 1.00 | 15747.750000 | 31494.500000 | 47241.250000 | 62988.000000 |
FFP_TIER | 62988.0 | 4.102162 | 0.373856 | 4.00 | 4.000000 | 4.000000 | 4.000000 | 6.000000 |
AGE | 62568.0 | 42.476346 | 9.885915 | 6.00 | NaN | NaN | NaN | 110.000000 |
FLIGHT_COUNT | 62988.0 | 11.839414 | 14.049471 | 2.00 | 3.000000 | 7.000000 | 15.000000 | 213.000000 |
BP_SUM | 62988.0 | 10925.081254 | 16339.486151 | 0.00 | 2518.000000 | 5700.000000 | 12831.000000 | 505308.000000 |
EP_SUM_YR_1 | 62988.0 | 0.000000 | 0.000000 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
EP_SUM_YR_2 | 62988.0 | 265.689623 | 1645.702854 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 74460.000000 |
SUM_YR_1 | 62437.0 | 5355.376064 | 8109.450147 | 0.00 | NaN | NaN | NaN | 239560.000000 |
SUM_YR_2 | 62850.0 | 5604.026014 | 8703.364247 | 0.00 | NaN | NaN | NaN | 234188.000000 |
SEG_KM_SUM | 62988.0 | 17123.878691 | 20960.844623 | 368.00 | 4747.000000 | 9994.000000 | 21271.250000 | 580717.000000 |
WEIGHTED_SEG_KM | 62988.0 | 12777.152439 | 17578.586695 | 0.00 | 3219.045000 | 6978.255000 | 15299.632500 | 558440.140000 |
AVG_FLIGHT_COUNT | 62988.0 | 1.542154 | 1.786996 | 0.25 | 0.428571 | 0.875000 | 1.875000 | 26.625000 |
AVG_BP_SUM | 62988.0 | 1421.440249 | 2083.121324 | 0.00 | 336.000000 | 752.375000 | 1690.270833 | 63163.500000 |
BEGIN_TO_FIRST | 62988.0 | 120.145488 | 159.572867 | 0.00 | 9.000000 | 50.000000 | 166.000000 | 729.000000 |
LAST_TO_END | 62988.0 | 176.120102 | 183.822223 | 1.00 | 29.000000 | 108.000000 | 268.000000 | 731.000000 |
AVG_INTERVAL | 62988.0 | 67.749788 | 77.517866 | 0.00 | 23.370370 | 44.666667 | 82.000000 | 728.000000 |
MAX_INTERVAL | 62988.0 | 166.033895 | 123.397180 | 0.00 | 79.000000 | 143.000000 | 228.000000 | 728.000000 |
ADD_POINTS_SUM_YR_1 | 62988.0 | 540.316965 | 3956.083455 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 600000.000000 |
ADD_POINTS_SUM_YR_2 | 62988.0 | 814.689258 | 5121.796929 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 728282.000000 |
EXCHANGE_COUNT | 62988.0 | 0.319775 | 1.136004 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 46.000000 |
avg_discount | 62988.0 | 0.721558 | 0.185427 | 0.00 | 0.611997 | 0.711856 | 0.809476 | 1.500000 |
P1Y_Flight_Count | 62988.0 | 5.766257 | 7.210922 | 0.00 | 2.000000 | 3.000000 | 7.000000 | 118.000000 |
L1Y_Flight_Count | 62988.0 | 6.073157 | 8.175127 | 0.00 | 1.000000 | 3.000000 | 8.000000 | 111.000000 |
P1Y_BP_SUM | 62988.0 | 5366.720550 | 8537.773021 | 0.00 | 946.000000 | 2692.000000 | 6485.250000 | 246197.000000 |
L1Y_BP_SUM | 62988.0 | 5558.360704 | 9351.956952 | 0.00 | 545.000000 | 2547.000000 | 6619.250000 | 259111.000000 |
EP_SUM | 62988.0 | 265.689623 | 1645.702854 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 74460.000000 |
ADD_Point_SUM | 62988.0 | 1355.006223 | 7868.477000 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 984938.000000 |
Eli_Add_Point_Sum | 62988.0 | 1620.695847 | 8294.398955 | 0.00 | 0.000000 | 0.000000 | 345.000000 | 984938.000000 |
L1Y_ELi_Add_Points | 62988.0 | 1080.378882 | 5639.857254 | 0.00 | 0.000000 | 0.000000 | 0.000000 | 728282.000000 |
Points_Sum | 62988.0 | 12545.777100 | 20507.816700 | 0.00 | 2775.000000 | 6328.500000 | 14302.500000 | 985572.000000 |
L1Y_Points_Sum | 62988.0 | 6638.739585 | 12601.819863 | 0.00 | 700.000000 | 2860.500000 | 7500.000000 | 728282.000000 |
Ration_L1Y_Flight_Count | 62988.0 | 0.486419 | 0.319105 | 0.00 | 0.250000 | 0.500000 | 0.711111 | 1.000000 |
Ration_P1Y_Flight_Count | 62988.0 | 0.513581 | 0.319105 | 0.00 | 0.288889 | 0.500000 | 0.750000 | 1.000000 |
Ration_P1Y_BPS | 62988.0 | 0.522293 | 0.339632 | 0.00 | 0.258150 | 0.514252 | 0.815091 | 0.999989 |
Ration_L1Y_BPS | 62988.0 | 0.468422 | 0.338956 | 0.00 | 0.167954 | 0.476747 | 0.728375 | 0.999993 |
Point_NotFlight | 62988.0 | 2.728155 | 7.364164 | 0.00 | 0.000000 | 0.000000 | 1.000000 | 140.000000 |
从上面可以看到,总样本数为62988,但是部分字段的count值小于62988,说明此字段存在缺失值。看到字段的最小值和最大值,比如票价的最小值、折扣率、里程的最小值存在为0的。 所以接下来的数据清洗阶段需要处理这些异常值和缺失值,以免干扰到正常结果。
3.数据预处理
- 数据清洗 在数据探索过程中发现了,有缺失值和票价为0、折扣率为0、总飞行公里数为0的记录,考虑到样本总数比较多,这类数据占比比较小,所以决定对他们进行丢弃处理,具体处理策略:丢弃票价为空的记录,丢弃票价为0但是折扣率不为0总飞行里程数也不为0的记录。
In [6]:
data = data[data['SUM_YR_1'].notnull()&data['SUM_YR_2'].notnull()] # 丢弃票价为空的记录 |
In [7]:
index1 = data['SUM_YR_1']!=0 |
In [8]:
index2 = data['SUM_YR_2']!=0 |
In [9]:
index3 = (data['avg_discount']==0) & (data['SEG_KM_SUM']==0) |
In [10]:
data = data[index1 | index2 | index3 ] # 其实这里更符合自然语义的是做差集。暂时没找到dataframe做差集的方式 |
此时数据已经是经过清洗的了。接下来是做属性规约,也就是去除不相关的、弱相关的或冗余的列.
- 属性规约 根据前期确定的lrfmc五个指标,我们保留有用的字段:MEMBER_NO是主键,ffp_date入会时间,load_time观测窗口的结束时间,last_to_end最后一次乘机时间至观测窗口结束的时长,FLIGHT_COUNT观测窗口内的飞行次数,SEG_KM_SUM观测窗口内的总里程数,avg_discount折扣系数
In [11]:
data.columns |
Out[11]:
Index([u'MEMBER_NO', u'FFP_DATE', u'FIRST_FLIGHT_DATE', u'GENDER', u'FFP_TIER', |
In [12]:
data = data.loc[:,['MEMBER_NO','FFP_DATE','LOAD_TIME','LAST_TO_END','FLIGHT_COUNT','SEG_KM_SUM','avg_discount']] |
In [13]:
data[:10] |
Out[13]:
MEMBER_NO | FFP_DATE | LOAD_TIME | LAST_TO_END | FLIGHT_COUNT | SEG_KM_SUM | avg_discount | |
---|---|---|---|---|---|---|---|
0 | 54993 | 2006/11/02 | 2014/03/31 | 1 | 210 | 580717 | 0.961639 |
1 | 28065 | 2007/02/19 | 2014/03/31 | 7 | 140 | 293678 | 1.252314 |
2 | 55106 | 2007/02/01 | 2014/03/31 | 11 | 135 | 283712 | 1.254676 |
3 | 21189 | 2008/08/22 | 2014/03/31 | 97 | 23 | 281336 | 1.090870 |
4 | 39546 | 2009/04/10 | 2014/03/31 | 5 | 152 | 309928 | 0.970658 |
5 | 56972 | 2008/02/10 | 2014/03/31 | 79 | 92 | 294585 | 0.967692 |
6 | 44924 | 2006/03/22 | 2014/03/31 | 1 | 101 | 287042 | 0.965347 |
7 | 22631 | 2010/04/09 | 2014/03/31 | 3 | 73 | 287230 | 0.962070 |
8 | 32197 | 2011/06/07 | 2014/03/31 | 6 | 56 | 321489 | 0.828478 |
9 | 31645 | 2010/07/05 | 2014/03/31 | 15 | 64 | 375074 | 0.708010 |
- 数据变换 将数据加工为制定的五个指标,L,R,F,M,C
In [14]:
import datetime |
In [15]:
data['FFP_DATE']=data['FFP_DATE'].map(lambda x:datetime.datetime.strptime(x,'%Y/%m/%d')) |
In [16]:
data['LOAD_TIME']=data['LOAD_TIME'].map(lambda x:datetime.datetime.strptime(x,'%Y/%m/%d')) |
In [17]:
data['L'] = data['LOAD_TIME']-data['FFP_DATE'] |
In [18]:
data['L'] = data['L'].map(lambda x: x.days/30.0 ) # 会员加入的时长,单位是月 |
In [19]:
data['R'] = data['LAST_TO_END']/30.0 # 最近一次乘坐飞机,转换到月 |
In [20]:
data['F'] = data['FLIGHT_COUNT'] |
In [21]:
data['M'] = data['SEG_KM_SUM'] |
In [22]:
data['C'] = data['avg_discount'] |
In [23]:
data = data.loc[:,['MEMBER_NO','L','R','F','M','C']] |
In [24]:
data.describe().T |
Out[24]:
count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|
MEMBER_NO | 62044.0 | 31485.237928 | 18188.650537 | 1.000000 | 15715.750000 | 31476.500000 | 47247.250000 | 62988.000000 |
L | 62044.0 | 49.623036 | 28.262697 | 12.166667 | 24.500000 | 42.600000 | 72.733333 | 114.566667 |
R | 62044.0 | 5.751090 | 6.050872 | 0.033333 | 0.966667 | 3.500000 | 8.666667 | 24.366667 |
F | 62044.0 | 11.971359 | 14.110619 | 2.000000 | 3.000000 | 7.000000 | 15.000000 | 213.000000 |
M | 62044.0 | 17321.694749 | 21052.728111 | 368.000000 | 4874.000000 | 10200.000000 | 21522.500000 | 580717.000000 |
C | 62044.0 | 0.722180 | 0.184833 | 0.136017 | 0.613085 | 0.712162 | 0.809293 | 1.500000 |
In [28]:
outfile = '/Users/frontc/book/ppdam/air_data2.xls' |
可以看见这5个指标的量级不在一个量级,为了消除影响,需要对数据进行标准化处理
In [38]:
data = pd.read_excel(outfile,index_col='MEMBER_NO') |
In [39]:
data[:3] |
Out[39]:
L | R | F | M | C | |
---|---|---|---|---|---|
MEMBER_NO | |||||
54993 | 90.200000 | 0.033333 | 210 | 580717 | 0.961639 |
28065 | 86.566667 | 0.233333 | 140 | 293678 | 1.252314 |
55106 | 87.166667 | 0.366667 | 135 | 283712 | 1.254676 |
In [40]:
data = (data - data.mean(axis=0)) / (data.std(axis=0)) # 零均值规范化:即均值为0,标准差为1,axis=0是指定操作的轴 |
In [41]:
data[:3] |
Out[41]:
L | R | F | M | C | |
---|---|---|---|---|---|
MEMBER_NO | |||||
54993 | 1.435707 | -0.944948 | 14.034016 | 26.761154 | 1.295540 |
28065 | 1.307152 | -0.911894 | 9.073213 | 13.126864 | 2.868176 |
55106 | 1.328381 | -0.889859 | 8.718869 | 12.653481 | 2.880950 |
完成了数据预处理之后,接下来进入模型构建阶段
4. 模型构建
- 使用kmeans聚类算法对客户数据进行客户分群
In [42]:
from sklearn.cluster import KMeans |
In [43]:
kmodel = KMeans(n_clusters=5,n_jobs=4) # 构建模型 |
In [44]:
kmodel.fit(data) # 训练 |
Out[44]:
KMeans(copy_x=True, init='k-means++', max_iter=300, n_clusters=5, n_init=10, |
In [45]:
kmodel.cluster_centers_ # 查看聚类的中心 |
Out[45]:
array([[-0.0059132 , 0.00754052, -0.25161715, -0.26149989, 2.06062997], |
In [53]:
r=pd.concat([data,pd.Series(kmodel.labels_,index=data.index)],axis=1) # 将原始数据与聚类训练得到的类别拼接,用index连接 |
In [54]:
r.columns=list(data.columns)+[u'聚类类别'] # 给新增加的聚类类别列加个列名 |
In [55]:
r[:10] |
Out[55]:
L | R | F | M | C | 聚类类别 | |
---|---|---|---|---|---|---|
MEMBER_NO | ||||||
54993 | 1.435707 | -0.944948 | 14.034016 | 26.761154 | 1.295540 | 3 |
28065 | 1.307152 | -0.911894 | 9.073213 | 13.126864 | 2.868176 | 3 |
55106 | 1.328381 | -0.889859 | 8.718869 | 12.653481 | 2.880950 | 3 |
21189 | 0.658476 | -0.416098 | 0.781585 | 12.540622 | 1.994714 | 3 |
39546 | 0.386032 | -0.922912 | 9.923636 | 13.898736 | 1.344335 | 3 |
56972 | 0.887281 | -0.515257 | 5.671519 | 13.169947 | 1.328291 | 3 |
44924 | 1.701075 | -0.944948 | 6.309337 | 12.811656 | 1.315599 | 3 |
22631 | -0.043274 | -0.933930 | 4.325015 | 12.820586 | 1.297873 | 3 |
32197 | -0.543344 | -0.917403 | 3.120249 | 14.447881 | 0.575103 | 3 |
31645 | -0.145883 | -0.867824 | 3.687198 | 16.993157 | -0.076664 | 3 |
In [56]:
r.to_excel('/Users/frontc/book/ppdam/air_data3.xls') # 输出到文件中 |
- 客户特征分析
In [68]:
r.groupby([u'聚类类别'])['L','R','F','M','C'].mean() |
Out[68]:
L | R | F | M | C | |
---|---|---|---|---|---|
聚类类别 | |||||
0 | -0.001236 | 0.007194 | -0.250158 | -0.260104 | 2.071453 |
1 | -0.700570 | -0.416815 | -0.158244 | -0.157254 | -0.271261 |
2 | 1.163792 | -0.378502 | -0.085133 | -0.092826 | -0.160045 |
3 | 0.484903 | -0.799925 | 2.483400 | 2.424450 | 0.314712 |
4 | -0.310388 | 1.691860 | -0.574608 | -0.536513 | -0.186893 |
通过每个类别的均值去分析一下每个类别的特点。 第一类优势是在c,说明消费价格不敏感。但是入会时间短,里程和频率低,但是很长时间没坐过了。所以属于高潜力,需挽留的客户。定位:重要挽留客户 第二类无优势项目,定位:低价值客户 第三类入会时间最长,但贡献较低,定位:一般客户 第四类入会时间较长,里程数,频率,折扣系数均较好,而且最近还乘坐过。定位:重要保持客户 第五类刚刚入会,乘坐有一段时间了,频率笑历程笑,系数也很低,说明这是打折机票才会去乘坐的客户。定位:低价值客户 下面通过雷达图的形式展示特征。其中雷达图代码借鉴:http://www.cnblogs.com/hhh5460/p/4361610.html
In [109]:
import numpy as np |
In [110]:
for i in r.groupby([u'聚类类别'])['L','R','F','M','C'].mean().as_matrix(): |