基于Kmean聚类分析航空公司客户价值


layout: post
title: 基于Kmean聚类分析航空公司客户价值
categories: Case
description: 利用无监督方法--聚类分析客户价值
keywords: 聚类,客户价值分析


利用无监督方法--聚类分析客户价值

背景

这次的分析课题是我研究生课程数据分析与决策的小组作业,首先感谢我的小伙伴们。
这一次数据分析完全是以研究问题驱动,也是我们几次小组讨论之后的结果。在拿到数据《某航空公司客户数据》时,我们计划以一个完整的数据挖掘流程去挖掘数据中的商业价值。流程如下:


数据预处理->聚类探索->数据可视化->航空公司客户价值分析

我们因为我主要负责实现客户聚类以及分析,所有还是打算把它写成一个完整的案例。

建模

1.基础模型(RFM)

基于客户价值分析有一个基础模型,即RFM模型(如下图1-1:RFM模型), RFM是衡量客户价值和客户创利能力的重要工具和手段。在众多的客户关系管理(CRM)的分析模式中,RFM模型是被广泛使用的。该模型通过一个客户的近期购买行为、购买的总体频率以及花了多少钱3项指标来描述该客户的价值状况,从而识别高价值的客户,即:
Recency: 最近消费时间间隔
Frequency: 消费频率
Monetary: 消费金额

2018-05-01-1.png

结合RFM模型我们选取了原始数据中的三个属性值[平均乘机间隔:AVG_INTERVAL,观测窗口内的飞行次数:FLIGHT_COUNT,总票价:SUM_PRICE]三个指标对用户进行聚类,其中总票价是一个新生成的特征:由两年票价总和而来,最终将客户聚为5类。对于RFM模型的聚类结果的类中心(如表1-1:RFM模型聚类的类中心)如下:
表1-1:RFM模型聚类的类中心

Category AVG_INTERVAL FLIGHT_COUNT SUM_PRICE
C_1 71.59704 9.454219 71189422
C_2 12.11986 94 1.29E+11
C_3 18.15115 47.36056 2.55E+09
C_4 7.213259 108.8333 2.55E+09
C_5 19.92913 113.5 2.14E+11

通过对类中心进行列项归一化(归一化的方法为线性转换:y=(x-MinValue)/(MaxValue-MinValue)),然后基于归一化后的数据画出了雷达图(如图1-2:RFM模型的雷达图)。

2018-05-01-2.png

从图中可以看到,每一类人数分别是:C_1:54292人,C_2:6人,C_3:3943人,C_4:32,C_5:2两人,从数据和图中可以看出,单纯依靠RFM三个指标分出的5类用户效果不佳,不仅每一类的人数分布差异很大,且C_2,C_5重叠很大。


2. 拓展模型-1(LRFMC)

虽然RFM模型可以衡量客户价值,但是对于这次的航空数据集,在聚类之后对于刻画不同的用户价值的效果并不理想,所以基于RFM模型可能不合适。由于同样的消费金额的不同旅客对航空公司的价值不同,例如买长航线、低等仓的旅客和买短航线、高等仓的旅客消费金额相同,但是价值却是不同的。显然后者更有价值。所以进一步地,选择客户在一定时间内的飞行里程M和乘坐舱位所对应的折扣系数C。同时,因为航空公司会员的加入时间一定程度上可以影响客户价值,所以我们在航空公司客户价值分析模型中添加客户关系长度 L,作为区分客户价值的另一个指标,所以我们构建出LRFMC 模型。


L:会员入会时间距观测窗口结束的时间(TIME_INTERVAL)
R:客户最近一次乘坐公司分级距观测窗口结束的时间(月数)(LAST_TO_END)
F:客户在观测窗口内乘坐公司飞机的次数(FLIGHT_COUNT)
M:客户在观测窗口内累计的飞行里程(SEG_KM_SUM)
C:客户在观测窗口内乘坐舱位所对应的折扣系数的平均值(avg_discount)


在LRFMC模型中,我们选择的特征包括了五类,分别是时间间隔:TIME_INTERVAL, 观测窗口内的飞行次数:FLIGHT_COUNT, 观测窗口总飞行公里数SEG_KM_SUM, 平均折扣率:avg_discount, 最后一次乘机时间至观测窗口结果时长(月)LAST_TO_END,其中时间间隔为会员入会时间距观测窗口结束的时间,聚类结果如下(表2-1 LRFMC聚类模型的类中心).

表2-1 LRFMC聚类模型的类中心

Category TIME_INTERVAL FLIGHT_COUNT SEG_KM_SUM avg_discount LAST_TO_END
C_1 1941.1565 51.8939 81022.5 0.78842 27.4025
C_2 1537.4712 15.1758 21362.8 0.72559 97.5557
C_3 1730.7709 30.1751 44550.5 0.75299 49.7244
C_4 1352.2401 4.97864 6373.01 0.70832 231.587
C_5 2130.6303 76.5931 152434. 0.84164 19.4043

通过对LRFMC聚类结果的类中心进行归一化,我们得到了如下雷达图(图2-1: LRFMC模型雷达图):

2018-05-01-3.png

通过对上图进行观察和分析,我们知道每一类别人数分别为:C_1:1867,C_2:14887,C_3:5831,C_4:35294,C_5:376, 其中C_5在时间间隔、观测窗口内的飞行次数、观测窗口总飞行公里数、平均折扣率、四个维度都比C_1、C_3、C_2更大,在最后一次乘机时间至观测窗口结果时长(月)这个维度上,C_4>C_2>C_3>C_1>C_5,根据雷达图的性质,面积越大,它的价值越大,所以价值大小排序为:C_5>C_1>C_3>C_2>C_4,依据客户价值排序我们可以将其分为重要保持客户、重要发展客户、重要挽留客户、普通价值客户、低价值客户:
表2-2:LRFMC模型的客户价值关系

Category Value Rating Headcount 客户价值
C_1 2 1867 重要发展客户
C_2 4 14887 普通价值客户
C_3 3 5831 重要挽留客户
C_4 5 35294 低价值客户
C_5 1 376 重要保持客户

3 拓展模型-2(LRFMC+2P模型)

在模型LRFMC模型中我们已经将基于五个指标(LRFMC)将用户划分为五类价值客户分别是重要发展客户、重要保持客户、重要挽留客户、普通价值客户、低价值客户,从雷达图可以比较清晰地观测和分析出客户价值,为了进一步探究是否其他属性会对客户价值聚类有正向作用,我们在LRFMC模型基础上增加了总积分Points_Sum和总票价SUM_PRICE两个属性从而提出了LRFMC+2P模型,以生活检验我们知道,总积分和总票价越高,那么客户在平台的消费力也越大,价值显然更高,因此我们提出的LRFMC+2P模型的参数如下:


L:会员入会时间距观测窗口结束的时间(TIME_INTERVAL)
R:客户最近一次乘坐公司分级距观测窗口结束的时间(月数)(LAST_TO_END)
F:客户在观测窗口内乘坐公司飞机的次数(FLIGHT_COUNT)
M:客户在观测窗口内累计的飞行里程(SEG_KM_SUM)
C:客户在观测窗口内乘坐舱位所对应的折扣系数的平均值(avg_discount)
2P: 客户在观测期的总积分和总票价("Points_Sum", "SUM_PRICE")


基于我们提出的LRFMC+2P模型,结合航空数据的属性,我们对其进行聚类分析,得到了相应的聚类结果,其中每类的类中心(表3-4:LRFMC+2P模型的类中心),同时,对类中心进行归一化处理,然后画出了7个属性的雷达图(如图3-4:LRFMC+2P模型的雷达图):
表3-1:LRFMC+2P模型的类中心

Category TIME_INTERVAL FLIGHT_COUNT SEG_KM_SUM avg_discount LAST_TO_END Points_Sum SUM_PRICE
C_1 1424. 9.5 13534.7 0.71191 181.5091 9259.56 72586528
C_2 2215.167 94 257112. 1.11308 20 323501 1.29E+1
C_3 1860.25 109 196685. 1.07389 6.583333 249070. 7.68E+1
C_4 1971.286 47.6 69385.5 0.83994 29.41255 59803.9 2.57E+1
C_5 1894 114 353033 1.09407 17.5 393372 2.14E+1
2018-05-01-4.png

通过对雷达图进行观察和分析,以及依据雷达图性质,我们可以得到五类客户价值以及人数分布(表3-5:LRFMC+2P模型的客户价值关系),可以明显看出每类价值分布太不均衡,因而并不适合用户刻画用户价值。
表3-2:LRFMC+2P模型的客户价值关系

Category Value Rating Headcount 客户价值
C_1 5 54326 低价值客户
C_2 2 6 重要发展价值
C_3 3 12 重要挽留客户
C_4 4 3909 普通价值客户
C_5 1 2 重要价值客户

综合以上三个模型,LRFMC的模型更具有实用性,LRFMC模型选择的三个用户属性不仅可以较好刻画用户价值,且每类用户分布较为合理。

代码附录

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from sklearn.cluster import KMeans
import pandas as pd
import datetime
import numpy as np
import matplotlib.pyplot as plt


def read_data(path):
    return pd.read_csv(path, sep=",")


class Model(object):
    def __init__(self, k):
        self.train_data = None
        self.k_cluster = k
        self.feature = {"feature_3": ["AVG_INTERVAL", "FLIGHT_COUNT", "SUM_PRICE"],
                        "feature_5": ["TIME_INTERVAL", "FLIGHT_COUNT", "SEG_KM_SUM", "avg_discount", "LAST_TO_END"],
                        "feature_7": ["TIME_INTERVAL", "FLIGHT_COUNT", "SEG_KM_SUM", "avg_discount", "LAST_TO_END",
                                      "Points_Sum", "SUM_PRICE"]}

    def new_train_data(self):
        new_train_data = self.train_data.drop(0)
        new_train_data["SUM_PRICE"] = new_train_data["SUM_YR_1"] + new_train_data["SUM_YR_2"]
        new_train_data["LOAD_TIME"] = new_train_data["LOAD_TIME"].apply(lambda x: x.split("/"))
        new_train_data["FFP_DATE"] = new_train_data["FFP_DATE"].apply(lambda x: x.split("/"))
        for i in new_train_data.index:
            year, month, day = int(new_train_data.ix[i, "LOAD_TIME"][0]), int(new_train_data.ix[i, "LOAD_TIME"][1]), \
                               int(new_train_data.ix[i, "LOAD_TIME"][2])
            year_2, month_2, day_2 = int(new_train_data.ix[i, "FFP_DATE"][0]), \
                                     int(new_train_data.ix[i, "FFP_DATE"][1]), int(new_train_data.ix[i, "FFP_DATE"][2])
            new_train_data.ix[i, "TIME_INTERVAL"] = (datetime.datetime(year, month, day) -
                                                     datetime.datetime(year_2, month_2, day_2)).days
        features = []
        for item in self.feature.values():
            for fea in item:
                if fea not in features:
                    features.append(fea)
        new_train_data = new_train_data.dropna()
        new_train_data.to_csv("../data/raw/target_data.csv", sep=",")
        return new_train_data

    def cluster(self, target, label):
        kmean_model = KMeans(self.k_cluster)
        kmean_model.fit(target)
        return [kmean_model.labels_, kmean_model.cluster_centers_]

    def graph(self, cluster_result, label, key):

        # 使用ggplot的绘图风格
        plt.style.use('ggplot')
        r1 = pd.Series(cluster_result[0]).value_counts()  # 统计各个类别的数目
        r2 = pd.DataFrame(cluster_result[1])
        r2 = r2.apply(lambda x: (x - np.min(x)) / (np.max(x) - np.min(x)))  # 找出聚类中心
        # 所有簇中心坐标值中最大值和最小值
        max = r2.values.max()
        min = r2.values.min()
        r = pd.concat([r2, r1], axis=1)  # 横向连接(0是纵向),得到聚类中心对应的类别下的数目
        r.columns = label + [u'类别数目']  # 重命名表头

        # 绘图
        fig = plt.figure(figsize=(10, 8))
        ax = fig.add_subplot(111, polar=True)
        center_num = r.values
        feature = label
        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="category_%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('The ' + key +' of users', 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()

    @staticmethod
    def combine_results(target_data, cluster_result, filename):
        path = "../data/results/"
        new_data = pd.DataFrame()
        new_data["Cluster_Results"] = pd.Series(cluster_result[0], index=target_data.index)
        new_data = target_data.join(new_data)
        new_data.to_csv(path + filename + "_cluster.csv", sep=",")
        cluster_center = pd.DataFrame(cluster_result[1])
        cluster_center.to_csv(path + filename + "_center.csv", sep=",")
        return new_data

    def run(self):
        self.train_data = read_data("../data/raw/new_data.csv")
        new_train_data = self.new_train_data()
        for key, value in self.feature.items():
            cluster_result = self.cluster(target=new_train_data[value], label=value)
            self.graph(cluster_result, label=value, key=key)
            self.combine_results(target_data=new_train_data, cluster_result=cluster_result, filename=key)
if __name__ == "__main__":
    mo = Model(k=5)
    mo.run()

总结

本次用户聚类是基于最基础的模型RFM,在RFM模型上进行扩展,选择了第一种扩展模型(LRFMC),实际上,此次建模这是对数据的探索。

如需获取本次案例的数据或者更加详细的分析报告,请下方关注微信公众号

2018-05-01-5.png

回复:某航空公司数据 | 某航空公司报告

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 源自《python数据分析与挖掘实战》 项目目标 借助航空公司客户数据,对客户进行分类 对不同的客户类别进行特征分...
    林清猫耳阅读 5,265评论 3 4
  • 本篇主要以《R语言数据分析与挖掘实战》中的第7章案例讲起。首先我们拿到的经加工的数据有: 观测窗口:已过去某个时间...
    晟文刀阅读 5,096评论 1 10
  • 目的 信息时代使得企业的营销焦点从产品中心转变为用户中心,维系“客户关系”成为企业核心问题。而这个核心问题最大的痛...
    神奇的考拉阅读 9,142评论 0 2
  • 盼情郎,年华似梦,十载荏苒已不在。 念娇娘,战火飘摇,陈约莫思无人量。 城门外,归郎卸甲返乡,旧模样。 深闺中,伊...
    Piiiiiiiiiiiiii阅读 182评论 0 0
  • 屋子里待得久了,很想出去透透气。外面下着簌簌小雨,空气清新却冰冷,没有打伞,就这样信步走到了街上。因为雨,街上少了...
    闲庭絮阅读 437评论 4 12