資料探勘——航空公司客戶價值分析(程式碼完整)
最近在閱讀張良均、王路等人出版的書《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 |
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人, 總里程和飛行次數都是最多的,而且平均每公里票價也較高,是重點保持物件