聚类问题---航空公司客户价值挖掘与分析研究

前言

当然,这次的分析还是接的一个单子的研究项目。当时我听到这个名字的时候就想到数据分析的那个经典例子,没错就是张良均老师的数据挖掘实战这本书中的案例,只不过我增加了一些模型分析结论以及可视化而已。

使用到的算法: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. 对于各种机器学习模型的算法还想继续研究更深刻一点,毕竟人工智能现在是热门话题。
发布了85 篇原创文章 · 获赞 55 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/shelgi/article/details/104804289
今日推荐