基于RFM模型的客户分群和K-Means聚类分析

项目描述

数据源是来自Kaggle的一个跨国数据集,其中包含2010年12月12日至2011年12月9日期间发生的所有在英国注册的非商店在线零售业务的交易。该公司主要销售独特的全场礼品,并且大部分客户是批发商。分析目的是按照RFM模型对客户进行分级,以用户的实际购买行为数据作为基础,进行用户群体的划分,再基于不同分类信息,分解成不同群体针对运营,从而使企业能更有效的获取客户、使客户更加满意、留住客户成为高价值客户、避免客户流失。
数据一览
数据形状为:542k 行x 8列,8个字段分别为发票号,发票日期,商品码,商品描述,数量,单价,顾客ID,国家。

数据

分析思路

R(Recency): 表示客户最近一次购买的时间距离现在有多远
F(Frequency): 表示用户在定义时间段内购买产品或服务的次数
M(Monetary): 表示用户在定义时间段内购买产品或服务的金额
按照每个指标取值不同分为八类客户,包括重要价值客户、重要发展客户、重要保持客户、重要挽留客户、一般价值客户、一般发展客户、一般保持客户、一般挽留客户等八类用户

数据清洗

0.计算每单的总价,添加Amount列。查看整体数据情况,发现描述和顾客ID列有缺失值,顾客ID类型为浮点型不符合业务逻辑

整体的数据信息

1.把顾客ID类型转为字符串
df['CustomerID']=df['CustomerID'].astype(str)
2.已知订单号中有“c”的是取消订单,需要删除这部分
df=df.drop(df[df['InvoiceNo'].str.contains('C')].index)
3.给顾客ID缺失的行加上NULL值
df['CustomerID'] = df['CustomerID'].fillna('NULL')
4.将订单时间数据类型转为日期
df['InvoiceDate']=pd.to_datetime(df['InvoiceDate'])
5.计算订单日期距离所有订单日期最大值的天数
df['Diff']=max(df['InvoiceDate'])-df['InvoiceDate']
df['Diff']=df['Diff'].dt.days
6.删除不需要的商品描述特征
df.drop(['Description'],axis=1,inplace=True)
7.以客户ID分组,求最近消费时间距离时间基线的最小值,得到R值

df_r=df.groupby('CustomerID')['Diff'].min()
df_r=df_r.reset_index()
df_r=df_r.drop(df_r[df_r['CustomerID']=='NULL'].index)

8.求出每位顾客在时间周期内消费次数,得到F值,(数据源中一个订单会包含多种产品,但是每种产品订单都会产生一条记录,注意去重)

#选出求消费金额需要的列
df_f_1=df.loc[:,['InvoiceNo','CustomerID','Amount']]
#删除客户ID为空的记录
df_f_1=df_f_1.drop(df_f_1[df_f_1['CustomerID']=='NULL'].index)
#以订单号分组,求每笔订单总金额
df_f_2=df_f_1.groupby('InvoiceNo')['Amount'].sum()
df_f_2=df_f_2.reset_index()
#删除Amount小于0的
df_f_2=df_f_2.drop(df_f_2[(df_f_2['Amount']<=0)].index)
# InvoiceNo相同的算一次购买频次,去除InvoiceNo相同的行
df_f_3=df_f_1.loc[:,['InvoiceNo','CustomerID']]
df_f_3=df_f_3.drop_duplicates()
#在整理的时候看到unique可以进行分组后去重统计,这里还可以简化一下
df_f=df_f_3.groupby("CustomerID")['InvoiceNo'].count()
df_f=df_f.reset_index()

9.表合并
df_data=pd.merge(df_f_2,df_f_3,on='InvoiceNo',how='left')

订单号,金额,顾客ID表

10.计算顾客消费金额,得到M值,最后合并得到RFM值表

df_m=df_data.groupby('CustomerID')['Amount'].sum()
df_m=df_m.reset_index()
#合并三张表
df_rf=pd.merge(df_r,df_f,on='CustomerID')
df_rfm=pd.merge(df_rf,df_m,on='CustomerID')
df_rfm.columns = ['CustomerID','R','F','M']
df_rfm.tail()
RFM表

11.对得到三个指标进行分区,映射级别

# 分级,cut函数把数据离散化,按照指定区间划分数据组,打上标签
R_bins = [0,30,90,180,360,720]
F_bins = [0,10,20,50,100,250]
M_bins = [0,500,5000,10000,30000,300000]
R_score=pd.cut(df_rfm['R'],R_bins,right='False',labels=[5,4,3,2,1],include_lowest=True)
F_score=pd.cut(df_rfm['F'],F_bins,right='False',labels=[1,2,3,4,5])
M_score=pd.cut(df_rfm['M'],M_bins,right='False',labels=[1,2,3,4,5])
#连接数据源和标签数据
df_rfm_1=pd.concat([df_rfm,R_score,F_score,M_score],axis=1)
df_rfm_1.columns=['CustomerID','R','F','M','R_score','F_score','M_score']
#求每个指标的均值,作为评判标准
R_mean=df_rfm_1['R_score'].astype(float).mean()
F_mean=df_rfm_1['F_score'].astype(float).mean()
M_mean=df_rfm_1['M_score'].astype(float).mean()
#分类函数,映射
def rank_R(x):
    if x>R_mean:
        return '高'
    else:
        return '低'
df_rfm_1['R_rank']=df_rfm_1['R_score'].apply(rank_R)
#得到最后的客户分级
df_rfm['value']=df_rfm['R_rank'].str[:]+df_rfm['F_rank'].str[:]+df_rfm['M_rank'].str[:]
df_rfm.head()
RFM分值

按照RFM分值对顾客分类

def trans_value(x):
    if x=='高高高':
        return '高价值客户'
    elif x=='高低高':
        return '重点深耕客户'
    elif x=='低高高':
        return '重点唤回客户'
    elif x=='低低高':
        return '重点挽留客户'
    elif x=='高高低':
        return '潜力客户'
    elif x=='高低低':
        return '新客户'
    elif x=='低高低':
        return '一般保持客户'
    else:
        return '流失客户'
分类完成
  1. 客户类型占比
C_type = df_rfm['Rank'].value_counts()
c_num=C_type.tolist()
c_type = C_type.index.tolist()
pie = (
    charts.Pie()
    .add("",[list(z) for z in zip(c_type, c_num)],
         rosetype="radius",
         radius=["30%","55%"],
         label_opts=opts.LabelOpts(formatter='占比{d}%'),
         center=["50%","40%"])
#          label_opts=opts.LabelOpts(is_show=False))
    .set_global_opts(
        title_opts=opts.TitleOpts(title="客户类型",pos_left="80%",pos_top="5%"),
        legend_opts=opts.LegendOpts(type_="scroll", pos_left="80%", orient="vertical",pos_top="15%"),
    )
    
)
pie.render_notebook()
客户类型占比情况

2.客户消费情况

bar = (
    charts.Bar()
    .add_xaxis(c_sales['分级'].tolist())
    .add_yaxis("",c_sales['M'].round(2).tolist())
    .set_global_opts(
        title_opts = opts.TitleOpts(title = '客户消费金额分布',pos_left = '50%',pos_top='5%'),
        xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-20))
    )
    .set_series_opts(label_opts = opts.LabelOpts(formatter = '{c}'))
)
bar.render_notebook()  
客户消费金额
pie2_type = c_sales['分级'].tolist()
pie2_value = c_sales['M'].round(2).tolist()
pie2_datapair = [list(z) for z in zip(pie2_type,pie2_value)]
pie2 = (
    charts.Pie()
    .add("",pie2_datapair,radius=["30%","55%"],center=["50%","40%"],rosetype="radius",
        label_opts = opts.LabelOpts(formatter='占比{d}%')
        )
    .set_global_opts(
        title_opts = opts.TitleOpts(title = "客户消费金额分布",pos_left='80%',pos_top='5%'),
        legend_opts=opts.LegendOpts(type_="scroll", pos_left="80%", orient="vertical",pos_top="15%")
    )

)
pie2.render_notebook()
客户消费占比

3.每个指标的分布情况

#删除R,F,M每列的异常值
q1 = df_rfm['R'].quantile(0.05)
q3 = df_rfm['R'].quantile(0.95)
iqr = q3-q1
df_cluster = df_cluster[(df_cluster["R"]>=q1-1.5*iqr) & (df_cluster["R"]<=q3+1.5*iqr)]
df_cluster.head()
#看指标的箱型图
boxplot4_r = [df_cluster['R'].tolist()]
boxplot4 = (
    charts.Boxplot()
    .add_xaxis(["R"])
    .add_yaxis("R",boxplot4.prepare_data(boxplot4_r))
)
boxplot4.render_notebook()
箱型图 R

箱型图 F

箱型图 M

RFM分析结论

1.客户类型分析
  • 经过第一步的数据清理,最终有4000+条记录,以R,F,M三个指标的不同取值对客户分群,其中重点深耕客户(最近有消费记录且消费金额大,但消费次数不多)占比近四成,流失客户(最近一年无消费,历史消费金额低,消费次数少)占比两成,重点关注这两类人数较多的客户。

  • 在客户总数中占比为7.61%的高价值客户和占比39.83%的重点深耕客户贡献了85.8%的销售额,距离各行业都适用的二八法则差距较大,接下来要提高高价值客户和重点深耕客户的消费金额,或是把重点深耕客户转化为高价值客户,即提升该客户群(总计1728个)的消费频次。

  • 重要挽留客户(消费金额高,但消费频次不高,最近无消费记录),人数511,贡献销售额占比8.75%,首先考虑是否在计算周期内有大型促销,降价活动,如果有,那么要转化这部分客户的难度会大一些,如果没有,则需要分析这类顾客的购买产品特性(是否是日常消费品,利润空间,季节性),首先考虑提高消费频次,培养客户在本平台的消费习惯。

  • 新客户(最近有消费,但消费金额不大,消费次数少),人数835,贡献销售额2.6%,因为需要先把新客户转化为留存用户,在实际运营过程中,可以考虑把这部分客户和重要挽留客户放在一起转化,提升消费次数。

  • 流失客户(最近一年无消费,历史消费金额低,消费次数少),人数927,贡献销售金额占比低,需要继续分析这类客户群体的消费商品和消费时间,但人数较多,分析过程比较耗时可作为提升销量的次要考虑因素 。

  • 重要唤回客户(最近无消费,但过往消费频次和消费金额都很高),人数7,这类客户人数较少,但消费意愿和消费价值较高,可作为短期提升销量的重点关注对象,通过运营活动push,优惠活动刺激消费。

2.R,F,M三个指标数值分布分析
  • F指标:有一半的数据集中在0-5区间,有25%的数据分布在4-30区间;M指标:一半的数据集中在0-2500区间,25%的数据在2500-14000区间,两个指标的中位数都偏低,极差较大;R指标分布相对较均匀,一半的数分布在0-150内;总体的消费数据可以看出:客户消费频次整体不高,金额不大,但最近消费情况还不错,即消费意愿高,重点是拉新和提升消费次数,在提升客户消费金额方面应首先考虑消费金额大,但最近无消费的客户。

K-Means聚类

最优k值的确定方法-手肘法和轮廓系数法
核心指标:SSE(sum of the squared errors,误差平方和)



Ci是第i个簇,p是Ci中的样本点,mi是Ci的质心(Ci中所有样本的均值),SSE是所有样本的聚类误差,代表了聚类效果的好坏。

  • 随着聚类数k的增大,样本划分会更加精细,每个簇的聚合程度会逐渐提高,那么误差平方和SSE自然会逐渐变小。
  • 当k小于真实聚类数时,由于k的增大会大幅增加每个簇的聚合程度,故SSE的下降幅度会很大,而当k到达真实聚类数时,再增加k所得到的聚合程度回报会迅速变小,所以SSE的下降幅度会骤减,然后随着k值的继续增大而趋于平缓,也就是说SSE和k的关系图是一个手肘的形状,而这个肘部对应的k值就是数据的真实聚类数
#数据标准化,这里用的是均值-标准差归一化,尽量将数据转化为均值为零,方差为一的数据,形如标准正态分布(高斯分布)
df_cluster_scaled = df_cluster[['R','F','M']]
scaler = StandardScaler()
df_cluster_scaled = scaler.fit_transform(df_cluster_scaled)
df_cluster_scaled = pd.DataFrame(df_cluster_scaled)
df_cluster_scaled.columns = ['R','F','M']
df_cluster_scaled.head()
#测试应该分为几簇,肘部对于的k值为3(曲率最高),故对于这个数据集的聚类而言,最佳聚类数应该选3
ssd = []
range_n_clusters = [2,3,4,5,6,7,8]
for num_clusters in range_n_clusters:
    kmeans = KMeans(n_clusters = num_clusters,max_iter = 50)
    kmeans.fit(df_cluster_scaled)
    ssd.append(kmeans.inertia_)
plt.plot(ssd)
分簇

也使用轮廓系数(silhouette coefficient)来确定K最优值,选择使系数较大所对应的k值

  • 轮廓系数范围在[-1,1]之间。该值越大,越合理。
    si接近1,则说明样本i聚类合理;
    si接近-1,则说明样本i更应该分类到另外的簇;
    若si 近似为0,则说明样本i在两个簇的边界上。
  • 所有样本的s i 的均值称为聚类结果的轮廓系数,是该聚类是否合理、有效的度量。
  • 使用轮廓系数(silhouette coefficient)来确定,选择使系数较大所对应的k值
for num_clusters in range_n_clusters:
    kmeans = KMeans(n_clusters = num_clusters,max_iter = 50)
    kmeans.fit(df_cluster_scaled)
    cluster_labels = kmeans.labels_
    silhouette_avg = silhouette_score(df_cluster_scaled,cluster_labels)
    print('For n_clusters = {0},the silhouette score is {1}'.format(num_clusters,silhouette_avg))
不同k值对应的轮廓系数

综上确定聚类K值为3

Kmeans = KMeans(n_clusters=3,max_iter=50)
Kmeans.fit(df_cluster_scaled)
df_cluster['Cluster_ID'] = Kmeans.labels_
cluster = df_cluster[['CustomerID','R','F','M','Rank','Cluster_ID']]
cluster.head()
聚类结果(tableau)

用K-Means进行无监督聚类后,可以看出整体分为三类,与RFM模型分类结果较为相似,可以重点关注偏离集群的几个点,以及蓝色类别中出现的几个红色类别数据,这部分与RFM模型的差异可能是由于RFM模型判断时间的主观性造成的,在实际建模的过程中需要再考虑一下RFM的分级条件。

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