1. 程式人生 > >資料探勘——航空公司客戶價值分析(程式碼完整)

資料探勘——航空公司客戶價值分析(程式碼完整)

最近在閱讀張良均、王路等人出版的書《python資料分析與挖掘實戰》,其中有個案例是介紹航空公司客戶價值的分析,其中用到的聚類方法是K-Means方法,我一直把學習的重心放在監督學習上,今天就用這個案例練習一下非監督學習。由於書上將這個案例介紹的比較詳細,導致網上的好多部落格都是直接將程式碼複製到部落格上甚至是直接截圖貼上,還都說是自己原創, 真好笑。本文只是部分參考,不喜勿噴。

書中給出了關於62988個客戶的基本資訊和在觀測視窗內的消費積分等相關資訊,其中包含了會員卡號、入會時間、性別、年齡、會員卡級別、在觀測視窗內的飛行公里數、飛行時間等44個特徵屬性。

為了便於觀察資料,採用anaconda的notebook進行分析及視覺化

首先匯入分析中用到的各種第三方工具包

import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

接著將資料讀取到程式中,並檢視每個特徵屬性的相關資訊,以便對“髒”資料進行處理

datafile = "air_data.csv"
data = pd.read_csv(datafile, encoding="utf-8")
print(data.shape)
print(data.info())
(62988, 44)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 62988 entries, 0 to 62987
Data columns (total 44 columns):
MEMBER_NO                  62988 non-null int64
FFP_DATE                   62988 non-null object
FIRST_FLIGHT_DATE          62988 non-null object
GENDER                     62985 non-null object
FFP_TIER                   62988 non-null int64
WORK_CITY                  60719 non-null object
WORK_PROVINCE              59743 non-null object
WORK_COUNTRY               62962 non-null object
AGE                        62568 non-null float64
LOAD_TIME                  62988 non-null object
FLIGHT_COUNT               62988 non-null int64
BP_SUM                     62988 non-null int64
EP_SUM_YR_1                62988 non-null int64
EP_SUM_YR_2                62988 non-null int64
SUM_YR_1                   62437 non-null float64
SUM_YR_2                   62850 non-null float64
SEG_KM_SUM                 62988 non-null int64
WEIGHTED_SEG_KM            62988 non-null float64
LAST_FLIGHT_DATE           62988 non-null object
AVG_FLIGHT_COUNT           62988 non-null float64
AVG_BP_SUM                 62988 non-null float64
BEGIN_TO_FIRST             62988 non-null int64
LAST_TO_END                62988 non-null int64
AVG_INTERVAL               62988 non-null float64
MAX_INTERVAL               62988 non-null int64
ADD_POINTS_SUM_YR_1        62988 non-null int64
ADD_POINTS_SUM_YR_2        62988 non-null int64
EXCHANGE_COUNT             62988 non-null int64
avg_discount               62988 non-null float64
P1Y_Flight_Count           62988 non-null int64
L1Y_Flight_Count           62988 non-null int64
P1Y_BP_SUM                 62988 non-null int64
L1Y_BP_SUM                 62988 non-null int64
EP_SUM                     62988 non-null int64
ADD_Point_SUM              62988 non-null int64
Eli_Add_Point_Sum          62988 non-null int64
L1Y_ELi_Add_Points         62988 non-null int64
Points_Sum                 62988 non-null int64
L1Y_Points_Sum             62988 non-null int64
Ration_L1Y_Flight_Count    62988 non-null float64
Ration_P1Y_Flight_Count    62988 non-null float64
Ration_P1Y_BPS             62988 non-null float64
Ration_L1Y_BPS             62988 non-null float64
Point_NotFlight            62988 non-null int64
dtypes: float64(12), int64(24), object(8)
memory usage: 21.1+ MB
None
print(data[0:5])

   MEMBER_NO    FFP_DATE FIRST_FLIGHT_DATE GENDER  FFP_TIER    WORK_CITY  \
0      54993  2006/11/02        2008/12/24      男         6            .   
1      28065  2007/02/19        2007/08/03      男         6          NaN   
2      55106  2007/02/01        2007/08/30      男         6            .   
3      21189  2008/08/22        2008/08/23      男         5  Los Angeles   
4      39546  2009/04/10        2009/04/15      男         6           貴陽   

  WORK_PROVINCE WORK_COUNTRY   AGE   LOAD_TIME       ...         \
0            北京           CN  31.0  2014/03/31       ...          
1            北京           CN  42.0  2014/03/31       ...          
2            北京           CN  40.0  2014/03/31       ...          
3            CA           US  64.0  2014/03/31       ...          
4            貴州           CN  48.0  2014/03/31       ...          

   ADD_Point_SUM  Eli_Add_Point_Sum  L1Y_ELi_Add_Points  Points_Sum  \
0          39992             114452              111100      619760   
1          12000              53288               53288      415768   
2          15491              55202               51711      406361   
3              0              34890               34890      372204   
4          22704              64969               64969      338813   

   L1Y_Points_Sum  Ration_L1Y_Flight_Count  Ration_P1Y_Flight_Count  \
0          370211                 0.509524                 0.490476   
1          238410                 0.514286                 0.485714   
2          233798                 0.518519                 0.481481   
3          186100                 0.434783                 0.565217   
4          210365                 0.532895                 0.467105   

   Ration_P1Y_BPS Ration_L1Y_BPS  Point_NotFlight  
0        0.487221       0.512777               50  
1        0.489289       0.510708               33  
2        0.481467       0.518530               26  
3        0.551722       0.448275               12  
4        0.469054       0.530943               39  

[5 rows x 44 columns]

通過觀測可知,資料集中存在票價為零但是飛行公里大於零的不合理值,但是所佔比例較小,這裡直接刪去

data = data[data["SUM_YR_1"].notnull() & data["SUM_YR_2"].notnull()]
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]
print(data.shape)
(62044, 44)

刪除後剩餘的樣本值是62044個,可見異常樣本的比例不足1.5%,因此不會對分析結果產生較大的影響。

原始資料集的特徵屬性太多,而且各屬性不具有降維的特徵,故這裡選取幾個對航空公司來說比較有價值的幾個特徵進行分析,這裡並沒有完全按照書中的做法選取特徵,最終選取的特徵是第一年總票價、第二年總票價、觀測視窗總飛行公里數、飛行次數、平均乘機時間間隔、觀察視窗內最大乘機間隔、入會時間、觀測視窗的結束時間、平均折扣率這八個特徵。下面說明這麼選的理由:

  • 選取的特徵是第一年總票價、第二年總票價、觀測視窗總飛行公里數是要計算平均飛行每公里的票價,因為對於航空公司來說並不是票價越高,飛行公里數越長越能創造利潤,相反而是那些近距離的高等艙的客戶創造更大的利益。
  • 當然總飛行公里數、飛行次數也都是評價一個客戶價值的重要的指標
  • 入會時間可以看出客戶是不是老使用者及忠誠度
  • 通過平均乘機時間間隔、觀察視窗內最大乘機間隔可以判斷客戶的乘機頻率是不是固定
  • 平均折扣率可以反映出客戶給公里帶來的利益,畢竟來說越是高價值的客戶享用的折扣率越高
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[0:5]
FFP_DATE LOAD_TIME FLIGHT_COUNT SUM_YR_1 SUM_YR_2 SEG_KM_SUM AVG_INTERVAL MAX_INTERVAL avg_discount
0 2006/11/02 2014/03/31 210 239560.0 234188.0 580717 3.483254 18 0.961639
1 2007/02/19 2014/03/31 140 171483.0 167434.0 293678 5.194245 17 1.252314
2 2007/02/01 2014/03/31 135 163618.0 164982.0 283712 5.298507 18 1.254676
3 2008/08/22 2014/03/31 23 116350.0 125500.0 281336 27.863636 73 1.090870
4 2009/04/10 2014/03/31 152 124560.0 130702.0 309928 4.788079 47 0.970658

對特徵進行變換:
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[["入會時間", "飛行次數", "平均每公里票價", "總里程", "時間間隔差值", "平均折扣率"]]
print(filter_data[0:5])
filter_data['入會時間'] = filter_data['入會時間'].astype(np.int64)/(60*60*24*10**9)
print(filter_data[0:5])
print(filter_data.info())
       入會時間  飛行次數   平均每公里票價     總里程     時間間隔差值     平均折扣率
0 2706 days   210  0.815798  580717  14.516746  0.961639
1 2597 days   140  1.154043  293678  11.805755  1.252314
2 2615 days   135  1.158217  283712  12.701493  1.254676
3 2047 days    23  0.859648  281336  45.136364  1.090870
4 1816 days   152  0.823617  309928  42.211921  0.970658
     入會時間  飛行次數   平均每公里票價     總里程     時間間隔差值     平均折扣率
0  2706.0   210  0.815798  580717  14.516746  0.961639
1  2597.0   140  1.154043  293678  11.805755  1.252314
2  2615.0   135  1.158217  283712  12.701493  1.254676
3  2047.0    23  0.859648  281336  45.136364  1.090870
4  1816.0   152  0.823617  309928  42.211921  0.970658
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 62988 entries, 0 to 62987
Data columns (total 6 columns):
入會時間       62988 non-null float64
飛行次數       62988 non-null int64
平均每公里票價    62299 non-null float64
總里程        62988 non-null int64
時間間隔差值     62988 non-null float64
平均折扣率      62988 non-null float64
dtypes: float64(4), int64(2)
memory usage: 2.9 MB
None
沒找到更好的處理timedatle的方法,這裡自己用笨方法找了一下規律,暫且這樣處理吧。

由於不同的屬性相差範圍較大,這裡進行標準化處理

filter_zscore_data = (filter_data - filter_data.mean(axis=0))/(filter_data.std(axis=0))
filter_zscore_data[0:5]

入會時間 飛行次數 平均每公里票價 總里程 時間間隔差值 平均折扣率
0 1.441178 14.104488 0.609218 26.887901 -0.975255 1.294751
1 1.312523 9.122093 1.806504 13.193844 -1.006818 2.862354
2 1.333768 8.766208 1.821278 12.718386 -0.996389 2.875087
3 0.663343 0.794378 0.764434 12.605032 -0.618769 1.991687
4 0.390687 9.976218 0.636894 13.969099 -0.652816 1.343389
對於K-Means方法,k的取值是一個難點,因為是無監督的聚類分析問題,所以不尋在絕對正確的值,需要進行研究試探。這裡採用計算SSE的方法,嘗試找到最好的K數值。編寫函式如下:
def distEclud(vecA, vecB):
    """
    計算兩個向量的歐式距離的平方,並返回
    """
    return np.sum(np.power(vecA - vecB, 2))

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)
        # 簇中心
        cluster_ceter_list = kmodel.cluster_centers_
        # 個樣本屬於的簇序號列表
        cluster_list = kmodel.labels_.tolist()
        for index in  range(len(data)):
            cluster_num = cluster_list[index]
            sse += distEclud(data_train[index, :], cluster_ceter_list[cluster_num])
        print("簇數是",num , "時; SSE是", sse)
        SSE.append(sse)
    return nums, SSE

nums, SSE = test_Kmeans_nclusters(filter_zscore_data)
簇數是 2 時; SSE是 296587.688611
簇數是 3 時; SSE是 245317.292202
簇數是 4 時; SSE是 209299.798194
簇數是 5 時; SSE是 183885.938906
簇數是 6 時; SSE是 167465.10385
簇數是 7 時; SSE是 151869.163041
簇數是 8 時; SSE是 142922.824005
簇數是 9 時; SSE是 135003.92238

#畫圖,通過觀察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時的程式碼如下
kmodel = KMeans(n_clusters=4, 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取值5,6時的程式碼與上述類似,不再給出,直接給出結果圖:

通過觀察可知:

當k取值4時,每個人群包含的資訊比較複雜,且特徵不明顯

當k取值5時,分析的結果比較合理,分出的五種型別人群都有自己的特點又不相互重複

當k取值6時,各種人群也都有自己的特點,但是第4簇人群完全在第5簇人群特徵中包含了,有點冗餘的意思

綜上,當k取值為5時,得到最好的聚類效果,將所有的客戶分成5個人群,再進一步分析可以得到以下結論:

  • 1.第一簇人群,10957人,最大的特點是時間間隔差值最大,分析可能是“季節型客戶”,一年中在某個時間段需要多次乘坐飛機進行旅行,其他的時間則出行的不多,這類客戶我們需要在保持的前提下,進行一定的發展;
  • 2.第二簇人群,14732人,最大的特點就是入會的時間較長,屬於老客戶按理說平均折扣率應該較高才對,但是觀察視窗的平均折扣率較低,而且總里程和總次數都不高,分析可能是流失的客戶,需要在爭取一下,儘量讓他們“回心轉意”;
  • 3.第三簇人群,22188人,各方面的資料都是比較低的,屬於一般或低價值使用者
  • 4.第三簇人群,8724人,最大的特點就是平均每公里票價和平均折扣率都是最高的,應該是屬於乘坐高等艙的商務人員,應該重點保持的物件,也是需要重點發展的物件,另外應該積極採取相關的優惠政策是他們的乘坐次數增加
  • 5.第五簇人群,5443人, 總里程和飛行次數都是最多的,而且平均每公里票價也較高,是重點保持物件
分析完畢,結果暗合市場的二八法則的,價值不大的第二三簇的客戶數最多,而價值較大的第四五簇的人數較少。