聚類問題---航空公司客戶價值挖掘與分析研究

前言

當然,這次的分析還是接的一個單子的研究項目。當時我聽到這個名字的時候就想到數據分析的那個經典例子,沒錯就是張良均老師的數據挖掘實戰這本書中的案例,只不過我增加了一些模型分析結論以及可視化而已。

使用到的算法:PCA、TSNE、在這裏插入圖片描述KMeans、層次聚類、DBSCAN、GAN

開始動手

1.導庫,導數據

import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import sklearn
import seaborn as sns
import re
plt.rcParams['font.sans-serif']='FangSong'
plt.rcParams['axes.unicode_minus']=False

data=pd.read_csv('./chapter7/demo/data/air_data.csv')
data.info()
data.head(10)

在這裏插入圖片描述

2.數據清洗及預處理

# 除去票價爲空
data=data[data['SUM_YR_1'].notnull()&data['SUM_YR_2'].notnull()]
# 票價爲0但是飛行數大於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]

# 去除工作省份中的缺失值以及異常符號
data.dropna(subset=['WORK_PROVINCE'],inplace=True)
data.replace(['\*','\-','\.','\/','r\[0-9]','='],"",regex=True,inplace=True)
# 再刪除所有空值、異常值
data.dropna(inplace=True)

# 再次查看一下數據
data.info()
data.head()

在這裏插入圖片描述
我們大概刪除了四千條數據,對於六萬條數據來說影響不大

3.簡單的探索性分析(一小部分)

#查看數據集中性別比例
gender=data.groupby(['GENDER'],as_index=False)['FFP_TIER'].count()
gender.columns=['性別','人數']
gender.head()

plt.figure()
plt.pie(gender['人數'],labels=gender['性別'],autopct='%1.2f%%') #畫餅圖(數據,數據對應的標籤,百分數保留兩位小數點)
plt.title("性別分佈圖")
plt.show()

在這裏插入圖片描述

#再來看看數據集不同性別的年齡均值分佈
age=data.groupby(['GENDER'],as_index=False)['AGE'].mean()
age.head()

sns.barplot(age['GENDER'],age['AGE'])
plt.title("性別年齡圖")
plt.show()

在這裏插入圖片描述

# 再來看看這些數據之間的相關性
corr=data.corr()
corr.head()

# 繪製熱力圖
sns.heatmap(corr,linewidths = 0.05,cmap='rainbow')
plt.title("相關性熱力圖")
plt.show()

在這裏插入圖片描述
可以看出來數據特徵之間的相關性關係,所以應該使用那些相關性較強的特徵來建模

4.開始選擇需要的特徵,重新構建數據集

這裏我選擇的是第一年總票價、第二年總票價、總飛行公里數、飛行次數、平均乘機時間間隔、最大乘機間隔、入會時間、結束時間、平均折扣率這9個特徵

理由:

  1. 首先是由於上圖看出這幾項特徵的相關性較高
  2. 出於實際考慮,航空公司的用戶價值肯定與飛行時間、里程數、間隔、票價等關係有關
mydata=data[[ "FFP_DATE", "LOAD_TIME", "FLIGHT_COUNT", "SUM_YR_1", "SUM_YR_2", "SEG_KM_SUM", "AVG_INTERVAL" , "MAX_INTERVAL", "avg_discount"]]
mydata.head()


# 對特徵進行變換
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" : "平均折扣率"})
mydata = deal_data[["入會時間", "飛行次數", "平均每公里票價", "總里程", "時間間隔差值", "平均折扣率"]]
print(mydata[0:5])
mydata['入會時間'] = mydata['入會時間'].astype(np.int64)/(60*60*24*10**9)
mydata.info()
mydata.head()

在這裏插入圖片描述

# 對自己構建的數據集再進行標準化處理
from sklearn.preprocessing import StandardScaler

ss=StandardScaler()
stdmydata=ss.fit_transform(mydata)
print(stdmydata)

5.使用kmeans對構建的數據集聚類

from sklearn.cluster import KMeans
from sklearn.decomposition import PCA

#先計算不同主成分下的方差貢獻
exvr=[]
for i in np.arange(1,7):
    pca=PCA(n_components=i)
    m=pca.fit_transform(stdmydata)
    print("累計方差貢獻率爲:",np.sum(pca.explained_variance_ratio_))
    exvr.append(np.sum(pca.explained_variance_ratio_))
    
plt.plot(np.arange(1,7),exvr,'g-o')
plt.title("不同PCA方差貢獻率")
plt.xlabel("PCA數")
plt.ylabel("方差貢獻率")
plt.grid()
plt.show()

在這裏插入圖片描述

#繪圖找出最好的聚類k值
k=np.arange(1,8)
error=[]
for i in k:
    kmeans=KMeans(n_clusters=i,random_state=1)
    kmeans.fit(stdmydata)
    error.append(kmeans.inertia_)
plt.figure()
plt.plot(k,error,"r-o")
plt.xlabel("聚類數目")
plt.ylabel("類內誤差平方和")
plt.title("K-Means聚類")
plt.xticks(np.arange(1,8,2))
plt.grid()
plt.show()

在這裏插入圖片描述
從圖中我們沒有看到我們期望的拐點,或者說肘部。但是我們可以看出最好的k值大概是在4,5,6這三個數中,所以還要繼續研究到底k值爲多少的時候最合適,這時候就需要可視化聚類結果來分析了。

# 首先是聚類爲4類的結果
kmodel = KMeans(n_clusters=4, n_jobs=4)
kmodel.fit(stdmydata)
# 簡單打印結果
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(mydata.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()

在這裏插入圖片描述
同樣的,聚類爲5,6的雷達圖
在這裏插入圖片描述
在這裏插入圖片描述
從聚類雷達圖結果來看,分爲4類特徵不夠突出;分爲六類有的特徵又過於極端;所以分爲5類較爲合理。

# 對數據降維,觀察分爲5類時的二維分佈情況
from sklearn.manifold import TSNE

model1 = KMeans(n_clusters=5, n_jobs=4)
model1.fit(stdmydata)
y =model1.labels_


digits_proj = TSNE(random_state=2).fit_transform(stdmydata)

def scatter(x, colors):
    palette = np.array(sns.color_palette("hls", 5))
 
    f = plt.figure(figsize=(8, 8))
    ax = plt.subplot(aspect='equal')
    sc = ax.scatter(x[:,0], x[:,1], lw=0, s=40,
                    c=palette[colors.astype(np.int)])
    plt.xlim(-25, 25)
    plt.ylim(-25, 25)
    ax.axis('off')
    ax.axis('tight')
 
    #給類羣點加文字說明
    txts = []
    for i in range(5):
        xtext, ytext = np.median(x[colors == i, :], axis=0)    #中心點
        txt = ax.text(xtext, ytext, str(i), fontsize=24)
        txts.append(txt)
    return f, ax, sc, txts
 
scatter(digits_proj, y)
plt.show()

在這裏插入圖片描述

6.對數據使用層次聚類看看效果

from sklearn.cluster import AgglomerativeClustering

pca=PCA(n_components=2)
newdata=pca.fit_transform(stdmydata)

hicl=AgglomerativeClustering(n_clusters=5)
hicl_pre=hicl.fit_predict(newdata[:500])

#可視化
plt.figure()
plt.scatter(newdata[:500][hicl_pre==0,0],newdata[:500][hicl_pre==0,1],c="g",alpha=1,marker="s")
plt.scatter(newdata[:500][hicl_pre==1,0],newdata[:500][hicl_pre==1,1],c="r",alpha=1,marker="*")
plt.scatter(newdata[:500][hicl_pre==2,0],newdata[:500][hicl_pre==2,1],c="b",alpha=1,marker="d")
plt.scatter(newdata[:500][hicl_pre==3,0],newdata[:500][hicl_pre==3,1],c="k",alpha=1,marker="^")
plt.scatter(newdata[:500][hicl_pre==4,0],newdata[:500][hicl_pre==4,1],c="y",alpha=1,marker="o")
plt.xlabel("主成分1")
plt.ylabel("主成分2")
plt.title("層次聚類")
plt.show()

在這裏插入圖片描述

#繪製聚類樹
from scipy.cluster.hierarchy import dendrogram,linkage
z=linkage(newdata[:500],method='ward',metric='euclidean')
fig=plt.figure(figsize=(30,12))
irisdn=dendrogram(z)
plt.axhline(y=10,color='k',linestyle='solid',label='five class')
plt.axhline(y=20,color='g',linestyle='dashdot',label='four class')
plt.title("層次聚類樹")
plt.xlabel("ID")
plt.ylabel("距離")
plt.legend(loc=1)
plt.show()

在這裏插入圖片描述

7.使用DBSCAN進行聚類

from sklearn.cluster import DBSCAN

dbscan=DBSCAN()

dbscan.fit(stdmydata)
print(dbscan.labels_)

for i in range(0,500):
    if dbscan.labels_[i]==-1:
        c1=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='r',marker="+")
    elif dbscan.labels_[i]==0:
        c2=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='g',marker="o")
    elif dbscan.labels_[i]==1:
        c3=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='b',marker="*")
        
plt.legend([c1,c2,c3],['類1','類2','類3'])
plt.title("使用DBSCAN聚類")
plt.show()

在這裏插入圖片描述

8.GAN實現聚類

import tensorflow as tf
import keras
from keras.layers import Input
from keras.models import Model, Sequential
from keras.layers.core import Reshape, Dense, Dropout, Flatten
from keras.layers.advanced_activations import LeakyReLU
from keras.layers.convolutional import Convolution2D, UpSampling2D
from keras.layers.normalization import BatchNormalization
from keras.datasets import mnist
from keras.optimizers import Adam
from keras import backend as K
from keras import initializers
from sklearn.model_selection import train_test_split
from tqdm import tqdm
import warnings
warnings.filterwarnings('ignore')

x_train,x_test=train_test_split(stdmydata,test_size=0.2,random_state=2)
print('---> x_train shape: ', x_train.shape)


x_train = x_train.reshape((x_train.shape[0], -1))  
x_test = x_test.reshape((x_test.shape[0], -1))  
print(x_train.shape)  
print(x_test.shape)  

K.image_data_format=='channels_first'

np.random.seed(1000)


#優化器
adam = Adam(lr=0.0002, beta_1=0.5)
 
generator = Sequential()
generator.add(Dense(16, input_dim=6, kernel_initializer=initializers.RandomNormal(stddev=0.02)))
generator.add(LeakyReLU(0.2))
generator.add(Dense(8))
generator.add(LeakyReLU(0.2))
generator.add(Dense(4))
generator.add(LeakyReLU(0.2))
generator.add(Dense(6, activation='tanh'))
generator.compile(loss='binary_crossentropy', optimizer=adam)
 
discriminator = Sequential()
discriminator.add(Dense(1, input_dim=6, kernel_initializer=initializers.RandomNormal(stddev=0.02)))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Dropout(0.3))
discriminator.add(Dense(16))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Dropout(0.3))
discriminator.add(Dense(8))
discriminator.add(LeakyReLU(0.2))
discriminator.add(Dropout(0.3))
discriminator.add(Dense(1, activation='sigmoid'))
discriminator.compile(loss='binary_crossentropy', optimizer=adam)
 

discriminator.trainable = False
ganInput = Input(shape=(6,))
x = generator(ganInput)
ganOutput = discriminator(x)
gan = Model(inputs=ganInput, outputs=ganOutput)
gan.compile(loss='binary_crossentropy', optimizer=adam)
 
dLosses = []
gLosses = []

def plotLoss(epoch):
    plt.figure(figsize=(10, 8))
    plt.plot(dLosses, label='Discriminitive loss')
    plt.plot(gLosses, label='Generative loss')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.legend()
    plt.savefig('./gan_loss_epoch_%d.png' % epoch)


    
def saveModels(epoch):
    generator.save('models/gan_generator_epoch_%d.h5' % epoch)
    discriminator.save('models/gan_discriminator_epoch_%d.h5' % epoch)
    
    
def train(epochs=3, batchSize=128):
    batchCount = int(x_train.shape[0] / batchSize)
    print('Epochs:', epochs)
    print('Batch size:', batchSize)
    print('Batches per epoch:', batchCount)
 
    for e in range(1, epochs+1):
        print('-'*15, 'Epoch %d' % e, '-'*15)
        for _ in tqdm(range(batchCount)):
            # Get a random set of input noise and images
            noise = np.random.normal(0, 1, size=[batchSize,6])
            codeBatch = x_train[np.random.randint(0, x_train.shape[1], size=batchSize)]
 
            # 用 Generator 生成假數據
            generated = generator.predict(noise)
            # 將假數據與真實數據進行混合在一起
            X = np.concatenate([codeBatch, generated])
            # 標記所有數據都是假數據
            yDis = np.zeros(2*batchSize)
            # 按真實數據比例,標記前半數據爲 0.9 的真實度
            yDis[:batchSize] = 0.9
            # 先訓練 Discriminator 讓其具有判定能力,同時Generator 也在訓練,也能更新參數。
            discriminator.trainable = True
            dloss = discriminator.train_on_batch(X, yDis)
            # 然後訓練 Generator, 注意這裏訓練 Generator 時候,把 Generator 生成出來的結果置爲全真,及按真實數據的方式來進行訓練。
            # 先生成相應 batchSize 樣本 noise 數據
            noise = np.random.normal(0, 1, size=[batchSize,6])
            # 生成相應的 Discriminator 輸出結果
            yGen = np.ones(batchSize)
            # 將 Discriminator 設置爲不可訓練的狀態
            discriminator.trainable = False
           # 訓練整個 GAN 網絡即可訓練出一個能生成真實樣本的 Generator
            gloss = gan.train_on_batch(noise, yGen)
 
        dLosses.append(dloss)
        gLosses.append(gloss)
 
        if e == 1 or e % 10 == 0:
            saveModels(e)
    plotLoss(e)
    

train(20, 128)

在這裏插入圖片描述

pred=generator.predict(x_test)
print(pred)

res=np.argmax(pred,axis=1)

for i in range(0,500):
    if res[i]==0:
        c1=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='r',marker="+")
    elif res[i]==1:
        c2=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='g',marker="o")
    elif res[i]==2:
        c3=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='b',marker="*")
    elif res[i]==3:
        c4=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='k',marker="d")
    elif res[i]==4:
        c5=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='y',marker="^")
    elif res[i]==5:
        c6=plt.scatter(newdata[:500][i,0],newdata[:500][i,1],c='pink',marker="h")
        
plt.legend([c1,c2,c3,c4,c5,c6],['類1','類2','類3','類4','類5','類6'])
plt.title("使用GAN聚類")
plt.show()

在這裏插入圖片描述

9.總結

通過以上這麼多聚類模型結果,可以看出kmeans實現聚類效果比較理想,所以接下來就用kmeans的結果進行分析

cluster_list=list(model1.labels_)
cluster_value=pd.value_counts(cluster_list)
fig = plt.figure(figsize=[7, 5])
clu = model1.cluster_centers_
x = [1,2,3,4,5,6]
colors = ['red','green','yellow','blue','black']
for i in range(5):
    plt.plot(x, clu[i],label='cluster'+str(i)+' '+str(cluster_value[i]), color=colors[i], marker='o')
plt.legend()
plt.xlabel('L R F M C')
plt.ylabel('values')
plt.show()

在這裏插入圖片描述
在這裏,我們由經濟學中的RFM模型進行拓展,衍生出LRFMC模型

首先,明確目標是客戶價值識別

識別客戶價值,應用最廣泛的模型是三個指標(消費時間間隔(Recency),消費頻率(Frequency),消費金額(Monetary))

以上指標簡稱RFM模型,作用是識別高價值的客戶

消費金額,一般表示一段時間內,消費的總額。但是,因爲航空票價收到距離和艙位等級的影響,同樣金額對航空公司價值不同

因此,需要修改指標。選定變量,艙位因素=艙位所對應的折扣係數的平均值=C,距離因素=一定時間內積累的飛行里程=M

再考慮到,航空公司的會員系統,用戶的入會時間長短能在一定程度上影響客戶價值,所以增加指標L=入會時間長度=客戶關係長度

總共確定了五個指標,消費時間間隔R,客戶關係長度L,消費頻率F,飛行里程M和折扣係數的平均值C

以上指標,作爲航空公司識別客戶價值指標,記爲LRFMC模型。

對應到上圖中的5,1,2,4,6這幾個橫座標對應的值,下面我來對聚類結果進行分析總結。

  • 對於客戶羣0:L很高,飛行里程M、消費間隔也不低,說明較長時間沒有乘坐本公司飛機,爲重要挽留客戶,需要增加互動延長客戶週期(13579人)
  • 對於客戶羣1:R高,而且其他數值也較高,雖然現在價值不明顯,但是卻有很大的發展潛力,爲重要發展客戶(10152人)
  • 對於客戶羣2:消費頻率和飛行里程都很高,說明經常乘坐本公司飛機,所以是重要客戶,對他們要好好服務,提高他們的滿意度(5078人)
  • 對於客戶羣3:C很高,但是其他幾項都比較低,很可能是碰到打折時纔會選擇乘坐本公司飛機,爲一般與較低價值客戶(8540人)
  • 對於客戶羣4:所有指標都低於其他羣體,表明飛機幾乎不是他們的出行選擇,屬於低價值客戶(20666人)

由上面的分析總結可以看出,大多數還是較低或低價值的客戶,所以我們更要好好把握住有價值的客戶羣。對於不同客戶羣應該採取不同的措施,比如對重要客戶的VIP服務、對發展客戶的優惠折扣、對挽留客戶的互動交流……對於公司來說,維持住老客戶的成本肯定低於吸引更多的新顧客,所以保持優質老客戶是十分重要的。我們在制定營銷策略時可以根據客戶價值排名,再綜合成本、管理等因素,制定出能最大維持住客戶的最低成本策略。

改進與不足

不足:

  1. 沒有太多對模型中的參數的調整,所以層次聚類和DBSCAN的效果可能不是最好的
  2. 對於GAN網絡的搭建還不太熟練,而且神經網絡對於圖像這種高維度複雜的數據有更好的效果(有更多的特徵選擇不那麼容易過擬合(或者欠擬合)),所以一般也沒有拿GAN來對簡單數據特徵聚類。

改進:

  1. 之後還想嘗試一下ClusterGAN,也就是再加上編碼機制從而實現潛在空間聚類
  2. 對於各種機器學習模型的算法還想繼續研究更深刻一點,畢竟人工智能現在是熱門話題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章