Bootstrap

Python数据分析案例-使用RFM模型与基于RFM的K-Means聚类算法实现电商用户价值分层

前言

本文通过使用真实电商订单数据,采用RFM模型与K-means聚类算法对电商用户按照其价值进行分层。

1. 案例介绍

该数据集为英国在线零售商在2010年12月1日至2011年12月9日间发生的所有网络交易订单信息。
该公司主要销售礼品为主,并且多数客户为批发商。
数据集介绍及来源:
https://www.kaggle.com/carrie1/ecommerce-data
https://archive.ics.uci.edu/ml/datasets/online+retail#

特征说明:

  • InvoiceNo:订单编号,由六位数字组成,退货订单编号开头有字幕’C’
  • StockCode:产品编号,由五位数字组成
  • Description:产品描述
  • Quantity:产品数量,负数表示退货
  • InvoiceDate:订单日期与时间
  • UnitPrice :单价(英镑)
  • CustomerID:客户编号,由5位数字组成
  • Country:国家

2. 操作环境

语言:Python 3
主要使用的库:numpy,pandas, matplotlib,seaborn,pyecharts,sklearn,scipy 等

3. 数据清洗

3.1 数据加载及预览

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.sans-serif'] = ['SimHei'] 
plt.rcParams['axes.unicode_minus'] = False  
import warnings
warnings.filterwarnings("ignore")

# 读取数据
data = pd.read_csv('data-2.csv')
data.head()

在这里插入图片描述

# 查看数据集基本信息
data.info()

在这里插入图片描述

3.2 重复值

# 删除重复值
data.drop_duplicates(inplace=True)
data.shape[0]  # 536641

删除了几千条重复值

3.3 缺失值

# 查看缺失值数量与比例
(
pd.DataFrame({
        "NaN_num": round(data.isnull().sum(),2),
        "NaN_percent":(data.isnull().sum()/data.shape[0]).apply(lambda x:str(round(x*100,2))+'%') ,
            })
  .sort_values('NaN_num', ascending=False)
)

在这里插入图片描述
因为本文是做用户价值分析,CustomerID的缺失会对结果产生影响,所以缺失值不能填补上的话需删除。
Description的缺失则没有影响不用删除。

# 查看用户编号为缺失值的数据,找到其InvoiceNo订单编号
CustomerID_isnull_list = data.loc[data['CustomerID'].isnull()].InvoiceNo.unique().tolist()
# 查看用户编号为非缺失值的列,找到其InvoiceNo订单编号
CustomerID_notnull_list = data.loc[~data['CustomerID'].isnull()].InvoiceNo.unique().tolist()
# 查看两者交集--即缺失值中没有订单编号与未缺失的相同
[i for i in CustomerID_isnull_list if i in CustomerID_notnull_list]

订单编号相同的为同一用户,以上的结果为空列表,表示缺失值中没有订单号与未缺失的相同,即无法通过订单编号来补充用户编号的缺失值。

# 删除CustomerID缺失值
data.dropna(subset=['CustomerID'], how='any', inplace=True)

3.4 异常值

3.4.1 日期
# 将日期字符串格式转换成时间格式
data['InvoiceDate'] = pd.to_datetime(data.InvoiceDate, format='%m/%d/%Y %H:%M')
# 查看是否有日期不在选定日期范围内的数据
data.query(" InvoiceDate < '2010-12-01' | InvoiceDate > '2011-12-10'")

没有查询到结果,表示所有日期均在选定范围内

3.4.2 价格与数量
# 描述性统计
data[['Quantity', 'UnitPrice']].describe()

在这里插入图片描述
价格没有负值,数量存在负值。

print('单价为0的数量:', data.query("UnitPrice == 0 ").shape[0])

单价为0的数量为40, 由于单价为0订单没有产生价值,所以删除此部分数据

# 删除单价为0的数据
data.drop(data.query("UnitPrice == 0 ").index, inplace=True)
# 查看数量为负且InvoiceNo订单编号没有C的数量
print(data.query("Quantity < 0 & ~InvoiceNo.str.contains('C')", engine='python').InvoiceNo.count())
# 查看数量为不为负且且InvoiceNo订单编号含有C的数量
print(data.query("Quantity >= 0 & InvoiceNo.str.contains('C')", engine='python').InvoiceNo.count())

以上两者数量皆为0,说明数量为负的与订单编号带有C的一致为退货订单

# 查看数量为负的订单
data.query(" Quantity < 0").head(3)

在这里插入图片描述
查看退货的订单编号是否两条信息,即一条是下单信息,另一条是退单信息。
即:上表第一行的C536379是否还存在订单编号为536379且数量为正数、其他信息一样的订单

# 筛选并拼接数量为负的订单信息
data_return = data.query(" Quantity < 0")
data_return['return'] = data_return.InvoiceNo.str.split("C", expand=True)[1] + data_return.StockCode + \
                        data_return.Description + abs(data_return.Quantity).astype("str") + \
                        data_return.UnitPrice.astype("str") + data_return.CustomerID.astype("str")
# 筛选并拼接数量为正的订单信息      
data_sale = data.query(" Quantity > 0").astype("str")
data_sale['sale'] = data_sale.InvoiceNo + data_sale.StockCode + data_sale.Description + data_sale.Quantity + \
                        data_sale.UnitPrice + data_sale.CustomerID           
# 利用intersection方法查看是否有交集
set(data_return['return'].tolist()).intersection(set(data_sale['sale'].tolist()))                              

没有交集说明不存在,即退货的订单不存在下单信息。

# 删除退货订单信息
data.drop(data.query("Quantity <= 0 ").index, inplace=True)

3.5 辅助列

# 添加总价列
data['Monetary'] = data['Quantity'] * data['UnitPrice']
# 添加年、月、日 、日期列
data['Date'] = data['InvoiceDate'].dt.date
data['Year'] = data['InvoiceDate'].dt.year
data['Month'] = data['InvoiceDate'].dt.month
data['Day'] = data['InvoiceDate'].dt.day
data.head()

查看提取的日期信息
在这里插入图片描述

4. RFM模型

# 定义一个分箱之后的统计函数
def rfm_bins_statistics(feature, scores, name,):
    feature_statistic = pd.concat([feature, data.groupby('CustomerID').Monetary.sum(), scores], axis=1)
    feature_statistic.columns = [name, 'Monetary', 'label']
    feature_bins = feature_statistic.groupby('label')[name].max().tolist()
    feature_bins_min = [-1]+ feature_bins[:-1]  # 辅助列
    feature_label_statistic = feature_statistic.groupby('label').agg({
            '{}'.format(name):['count',('占比', lambda x: "%.1f"%((x.count() / feature_statistic[name].count()*100)) + '%')],
            'Monetary':['sum',('占比', lambda x: "%.1f"%((x.sum() / feature_statistic.Monetary.sum()*100)) + '%')],
        }).assign(范围 = [str(i + 1) + '-' + str(j) for i,j in zip(feature_bins_min, feature_bins )] )
    return feature_statistic , feature_label_statistic, feature_bins

4.1 R

# 计算每个客户购买的最近日期
R = data.groupby('CustomerID')['Date'].max()
# 计算每位客户最近一次购买距离截止日期的天数
R_days = (data['Date'].max()-R).dt.days # .dt.days取出days
# 数据非正态分布,将客户最近一次购买天数按照中位数分5层,并依次评分
R_scores= pd.qcut(R_days, q=5, duplicates='drop',labels=[5,4,3,2,1])  # 上一次消费距离天数越近越好,所以labels为倒序

这里根据帕累托法则即20%的客户贡献了80%的财富,按照中位数分为5个层级,每层的数量接近相同。
实际可根据业务场景进行调整, 比如[0, 30, 90, 180, 360, 720]按照天数进行划分。下面的F与M同理

R_statistic, R_label_statistic, R_bins = rfm_bins_statistics(R_days, R_scores, 'Date')
R_label_statistic

在这里插入图片描述

from matplotlib.patches import Polygon
from matplotlib.collections import PatchCollection

fig,ax = plt.subplots(figsize=(18, 10), facecolor='#f4f4f4')

# 各天数的消费金额之和
Monetary_sum = pd.concat([R_statistic.Date.value_counts().sort_index(), R_statistic.groupby('Date').Monetary.sum()], axis=1)
# 层级消费金额按照天数范围从小到大的累计之和---sumsum不是sum
Monetary_range = R_label_statistic['Monetary', 'sum'].cumsum().tolist()
# 频数分布图
ax.hist(R_statistic.Date, bins=50, alpha=0.5)  
ax1 = ax.twinx()
ax1.plot(Monetary_sum.Monetary.cumsum(), color='#cc0033')
# 文字标注
for i in range(5):
    ax1.text(
        R_bins[i]/2,
        Monetary_range[i],
        '天数范围:' + str(R_label_statistic['范围'].tolist()[i])+ '\n' + '消费金额占比:' + str(R_label_statistic['Monetary','占比'].tolist()[i]),
        va='top',
        ha='left',
        bbox={'boxstyle': 'round',
              'edgecolor':'grey',
              'facecolor':'#d1e3ef',
              'alpha':0.5})
# # 矩形
# polygons = [Polygon(xy=np.array([(0, 0), (R_bins[0], 0), (R_bins[0], Monetary_range[0]), (0,Monetary_range[0])])),
#             Polygon(xy=np.array([(R_bins[0], Monetary_range[0]), (R_bins[1], Monetary_range[0]), (R_bins[1], Monetary_range[1]), (R_bins[0], Monetary_range[1])])),
#             Polygon(xy=np.array([(R_bins[1], Monetary_range[1]), (R_bins[2], Monetary_range[1]), (R_bins[2], Monetary_range[2]), (R_bins[1], Monetary_range[2])])),
#             Polygon(xy=np.array([(R_bins[2], Monetary_range[2]), (R_bins[3], Monetary_range[2]), (R_bins[3], Monetary_range[3]), (R_bins[2], Monetary_range[3])])),
#             Polygon(xy=np.array([(R_bins[3], Monetary_range[3]), (R_bins[4], Monetary_range[3]), (R_bins[4], Monetary_range[4]), (R_bins[3], Monetary_range[4])]))]

# ax1.add_collection(PatchCollection(polygons, facecolor='grey', alpha=0.3));

ax.set_xlabel('距离最近一次的购买天数')
ax.set_ylabel('频数')
ax1.set_ylabel('消费累计金额')
ax.set_facecolor('#f4f4f4')
plt.show()

在这里插入图片描述
观察以上图表可得,距离最近一次购买天数在0-12天时,其数量占所有顾客的20%,贡献了52.4%的消费。
距离最近一次购买天数越大,其贡献的消费越低。

4.2 F

# 计算每个客户购买的频次---一天内多次消费算一次,按照天数计次
F = data.groupby('CustomerID')['Date'].nunique()
# 查看消费次数占比
pd.DataFrame({'count':F.value_counts(),
             'percent':F.value_counts()/F.value_counts().sum()}).head(5)

在这里插入图片描述
可以看到消费次数为1的超过三分之一

# 自定义边界分箱
F_scores = pd.cut(F,[1, 2, 3, 5, 8, F.max()+1], labels=[1, 2, 3, 4, 5], right=False)  # 消费频次越大越好,所以labels为顺序
# 查看相关统计
F_statistic, F_label_statistic, F_bins = rfm_bins_statistics(F, F_scores, 'Freq')
F_label_statistic

在这里插入图片描述

# 绘图
fig,ax = plt.subplots(figsize=(18, 10), facecolor='#f4f4f4')
# 各天数的消费金额之和
Monetary_sum = pd.concat([F_statistic.Freq.value_counts().sort_index(), F_statistic.groupby('Freq').Monetary.sum()], axis=1)
# 层级消费金额按照天数范围从小到大的累计之和---sumsum不是sum
Monetary_range = F_label_statistic['Monetary', 'sum'].cumsum().tolist()
# 频数分布图
ax.hist(F_statistic.Freq, bins=50, alpha=0.5)  
ax1 = ax.twinx()
ax1.plot(Monetary_sum.Monetary.cumsum(), color='#cc0033')
# 文字标注
for i in range(5):
    ax1.text(
        F_bins[i]/2,
        Monetary_range[i],
        '购买次数范围:' + str(F_label_statistic['范围'].tolist()[i])+ '\n' + '消费金额占比:' + str(F_label_statistic['Monetary','占比'].tolist()[i]),
        va='top',
        ha='left',
        bbox={'boxstyle': 'round',
              'edgecolor':'grey',
              'facecolor':'#d1e3ef',
              'alpha':0.5})
ax.set_xlabel('购买次数')
ax.set_ylabel('频数')
ax1.set_ylabel('消费累计金额')
ax.set_facecolor('#f4f4f4')
plt.show()

在这里插入图片描述
观察以上图表可以看到,消费频率最高的11.1%客户贡献了53.9%的消费。

4.3 M

# 将每层按照中位数分5层,并依次评分
M = data.groupby('CustomerID')['Monetary'].sum()  
M_scores =  pd.qcut(M, q=5, duplicates='drop',labels=[1,2,3,4,5])  # 消费金额越大越好,所以labels为顺序
# 查看相关统计
M_statistic = pd.concat([data.groupby('CustomerID').Monetary.sum(), M_scores], axis=1)
M_statistic.columns = ['Monetary', 'label']
M_label_statistic = M_statistic.groupby('label').agg({
            'label':['count',('占比', lambda x: x.count() / M_statistic.label.count())],
            'Monetary':['sum',('占比', lambda x: x.sum() / M_statistic.Monetary.sum())],
        }).sort_values(('Monetary','sum'),ascending = False).round(2)
M_label_statistic

在这里插入图片描述

# 绘图
fig,ax = plt.subplots(figsize=(18, 10), facecolor='#f4f4f4')

label_count_cumsum = M_label_statistic.cumsum()['label', 'count'].values
label_percent_cumsum = M_label_statistic.cumsum()['label', '占比'].values
Monetary_sum_cumsum = M_label_statistic.cumsum()['Monetary', 'sum'].values
Monetary_percent_cumsum = M_label_statistic.cumsum()['Monetary', '占比'].values

ax.plot(M_statistic.Monetary.sort_values(ascending=False).cumsum().values)
for i in range(5):
    ax.text(label_count_cumsum[i],
            Monetary_sum_cumsum[i],
            '数量占比:%.0f%%'%(label_percent_cumsum[i]*100)+'\n'+'金额占比:%.0f%%'%(Monetary_percent_cumsum[i]*100), 
            va='center', 
            ha='right',
            bbox={
                'boxstyle': 'round',
                'edgecolor':'grey',
                'facecolor':'#d1e3ef',
                'alpha':0.5 })
ax.set_xlabel('顾客数量')
ax.set_ylabel('消费累计金额')
ax.set_facecolor('#f4f4f4')
plt.show()

在这里插入图片描述

至此,R、F、M已分箱完毕,各分5层,除了个别外数量大致相近。
各特征的头部客户均贡献了超过50%的消费。

4.4 分值计算

# 合并
RFM = pd.concat([R_scores,F_scores,M_scores],axis=1)
RFM.columns = ['R_scores', 'F_scores', 'M_scores']
# 绘制R F M 得分交叉表
pd.pivot_table(RFM, index = ['R_scores','F_scores'], columns=['M_scores'], aggfunc=len)

在这里插入图片描述

可以通过上表看到RFM分值的详细分布

# 格式转换
for i in RFM.columns:
    RFM[i] = RFM[i].astype(float)

# 将每个值按照R/F/M均值大小分别定义其价值高低
for i,j in enumerate(['R', 'F', 'M']):
    RFM[j] = np.where(RFM.iloc[:,i] > RFM.iloc[:,i].mean(), '高', '低')

# 创造综合价值变量
RFM['Value'] = RFM['R'] + RFM['F'] +RFM['M'] 

map_dict = {'高高高':'重要价值客户', '高低高':'重要发展客户', '低高高':'重要保持客户', '低低高':'重要挽留客户','高高低':'一般价值客户', '高低低':'一般发展客户', '低高低':'一般保持客户', '低低低':'流失客户'}
RFM['CustmerLevel'] = RFM['Value'].map(map_dict)
RFM.head()

在这里插入图片描述

4.5 分析

# 计算出每个客户的消费总金额,下单总数,下单的产品总数
data_temp = data.groupby('CustomerID').agg({'Monetary':np.sum, 'Quantity':np.sum, 'InvoiceNo':'nunique'})
RFM_data = pd.concat([RFM['CustmerLevel'], data_temp, R_days, F],axis=1)
RFM_data.columns = ['客户等级','消费金额', '购买商品总量', '订单总量', '最近消费天数', '消费次数']
RFM_data.head()

在这里插入图片描述

# 定义一个统计函数
def customer_level_statistic(customer_data):
    customer_level = (customer_data
                 .groupby('客户等级')
                 .agg({
                        '消费金额':[('均值', 'mean'),
                                    ('总量', 'sum'),
                                    ('占比', lambda x: "%.1f"%((x.sum()/ customer_data['消费金额'].sum()*100))+'%')], 
                        '购买商品总量':[('均值', 'mean'),
                                        ('总量', 'sum'),
                                       ('占比', lambda x:"%.1f"%((x.sum()/ customer_data['购买商品总量'].sum()*100))+'%')],
                        '订单总量':[('均值', 'mean'),
                                    ('总量', 'sum'),
                                    ('占比', lambda x:"%.1f"%((x.sum()/ customer_data['订单总量'].sum()*100))+'%')],
                        '客户等级':[('数量', 'count'),
                                    ('占比', lambda x:"%.1f"%((x.count()/ customer_data['客户等级'].count()*100))+'%')],
                        '最近消费天数':[('均值','mean')],
                        '消费次数':[('均值','mean')],
                    })
                 .sort_values(('消费金额','总量'),ascending=False)
                 .assign(客单价 = lambda x : x['消费金额','总量'] / x['订单总量','总量'])
                 .round(1)
                 )
    customer_level.columns = pd.Index(customer_level.columns[:-1].tolist() + [('客单价', '均值')])
    return customer_level

RFM_level = customer_level_statistic(RFM_data)
RFM_level

在这里插入图片描述

相关指标的可视化

# 饼图
from pyecharts import options as opts
from pyecharts.charts import Pie
from pyecharts.globals import ThemeType

def customer_level_pie(value, name):
    type = value.index.tolist()
    Monetary = value['消费金额','总量'].astype(float).round(1).tolist()
    ProductQuantity = value['购买商品总量','总量'].astype(int).tolist()
    OrderNunique = value['订单总量','总量'].astype(int).tolist()
    Num = value['客户等级','数量'].astype(int).tolist()

    c = (
        Pie(init_opts=opts.InitOpts(bg_color="#f4f4f4", width="1600px", height="900px"))
        .add(
            "",
            [list(z) for z in zip(type, Monetary )],
            center=["25%", "30%"],
            radius="32%",
            label_opts=opts.LabelOpts(formatter=" {b} : {d}%"),
        )
        .add(
            "",
            [list(z) for z in zip(type, ProductQuantity )],
            center=["60%", "30%"],
            radius="32%",
            label_opts=opts.LabelOpts(formatter=" {b} : {d}%"),
        ).add(
            "",
            [list(z) for z in zip(type, OrderNunique)],
            center=["25%", "76%"],
            radius="32%",
            label_opts=opts.LabelOpts(formatter=" {b} : {d}%"),
        )
        .add(
            "",
            [list(z) for z in zip(type, Num)],
            center=["60%", "76%"],
            radius="32%",
            label_opts=opts.LabelOpts(formatter=" {b} : {d}%"),
        )
        .set_global_opts(
            title_opts=opts.TitleOpts(title="{}每层客户各项指标对比图".format(name),pos_left="30%",pos_top="2%", 
                                      title_textstyle_opts=opts.TextStyleOpts(font_size=30),),
            legend_opts=opts.LegendOpts(
                type_="scroll", pos_top="30%", pos_left="80%", orient="vertical"
                ),             
            graphic_opts=[
                opts.GraphicGroup(
                    graphic_item=opts.GraphicItem(
                        left="21%",
                        top="7%",
                    ),
                    children=[
                        # opts.GraphicText控制文字的显示
                        opts.GraphicText(
                            graphic_textstyle_opts=opts.GraphicTextStyleOpts(
                                text='消费金额对比' + '  '*43 + '购买商品总量对比' + '\n'*24 + ' 订单总量对比' +'  '*43 + ' 客户数量对比',
                                font="18px Microsoft YaHei",
                            )
                        )
                    ]
                )
            ],
        ).set_series_opts(
            tooltip_opts=opts.TooltipOpts(
                trigger="item", formatter="{b} \n 数量: {c} \n 占比:{d}%"
            ),

        )
        .render("{}指标对比图.html".format(name))
    )

customer_level_pie(RFM_level, 'RFM')

在这里插入图片描述

# 均值对比
def customer_level_barh(data_level):
    fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(18, 12), facecolor='#f4f4f4', dpi=300)
    plt.style.use('seaborn-dark')
    plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
    plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

    columns_list = [['消费金额', '客单价'], ['最近消费天数', '消费次数']]
    for i in [0,1]:
        for j in [0,1]:
            index = data_level[columns_list[i][j], '均值'].sort_values().index
            values = data_level[columns_list[i][j], '均值'].sort_values().values
            ax[i][j].barh(index, values)

            for k in range(len(data_level)):
                ax[i][j].text(values[k], k-0.1, values[k], size=13 )  

            ax[i][j].set_title(columns_list[i][j]+'均值对比')
            ax[i][j].set_facecolor('#f4f4f4')
            ax[i][j].set_xticks([])  

            for m in ['left', 'right', 'top', 'bottom']:
                ax[i][j].spines[m].set_color("#f4f4f4")  

            ax[i][j].tick_params(axis='y', which='major',color='#f4f4f4')  

customer_level_barh(RFM_level)

在这里插入图片描述
根据R、F、M高低区分的8类客户,可以根据其各自特点给出针对性的营销策略。

RFM客户类型行为特征营销策略
重要价值客户近期购买过,购买频率高,消费高,为主要消费客户升级为VIP客户,提供个性化服务,倾斜较多的资源
重要发展客户近期购买过,购买频率低,客单价高,可能是新的批发商或企业采购者提供会员积分服务,给与一定程度的优惠来提高留存率
重要保持客户近期没有购买,购买频率高,消费较高通过短信邮件等介绍最新产品/功能,来促进消费
重要挽留客户近期没有购买,购买频率低,客单价高,即将流失通过短信、邮件、电话等介绍最新产品/功能/升级服务促销折扣等,避免流失
一般价值客户近期购买过,购买频率高,消费较低潜力股, 提供社群服务,介绍新产品/功能促进消费
一般发展客户近期购买过,购买频率低,消费较低,可能是新客户提供社群服务,介绍新产品/功能,提供折扣等提高留存率
一般保持客户近期没有购买,购买频率高,消费低,介绍新产品/功能等方式唤起此部分用户
流失客户近期没有购买,购买频率低,消费低,已流失促销折扣等方式唤起此部分用户,当资源分配不足时可以暂时放弃此部分用户

4.6 加权得分

RFM模型的假设:

  • R:用户最近一次购买离得越久就越有流失风险
  • F:用户消费频次越高越忠诚
  • M:用户消费越多越有价值

但并不是所有的场景都能够满足这三个假设。
服装等跟着季节走,礼物饰品等看节日,而手机平板等购买间隔时间参考产品更新周期。而诸如电器、家具等耐消品,分析R的意义就不大了。像母婴奶粉等产品有生命周期,当到了周期结束后,分析M已没有意义。还有其他促销活动、节假日等因素造成的分析场景受限等。

所以在R、F、M统一量纲之后,某些场景下比如需要弱化某一因素的对于分层的影响,可以需根据不同业务场景设置不同的权重。
本次分析的网站主要产品是非季节性礼物,权重3:3:4仅供参考。
权重的设置可以依据个人经验或者AHP法。

# 按照R、F、M权重3:3:4综合评分 
RFM_weight = RFM[['R_scores', 'F_scores', 'M_scores']]
RFM_weight['Scores'] = 0.3 * RFM_weight['R_scores'] + 0.3 * RFM_weight['F_scores'] + 0.4 * RFM_weight['M_scores']

def score2value(x):
    if x >= 4.2:
        x = '高价值客户'
    elif 4.2 > x >= 3.4:
        x = '较高价值客户'
    elif 3.4 > x >= 2.6:
        x = '中等价值客户'
    elif 2.6 > x >= 1.8:
        x = '一般价值客户'
    else:
        x = '低价值客户'
    return x
RFM_weight['Value'] = RFM_weight['Scores'].apply(lambda x : score2value(x))
# 选出每个客户的消费总金额,下单总数,下单的产品总数
RFM_weight = pd.concat([RFM_weight.Value, data_temp, R_days, F],axis=1)
RFM_weight.columns = ['客户等级', '消费金额', '购买商品总量', '订单总量', '最近消费天数', '消费次数']
RFM_weight_level = customer_level_statistic(RFM_weight)
RFM_weight_level

在这里插入图片描述

# 绘制饼图
customer_level_pie(RFM_weight_level, 'RFM加权')
# 绘制柱状图
customer_level_barh(RFM_weight_level)

在这里插入图片描述

在这里插入图片描述
观察结果可以看到,

  • 17%的高价值客户贡献了网站62.6%的消费。
  • 而24.1%的低价值客户仅贡献了2.6%的消费。

分层具有一定的效果,各层客户间有明显的差别。

5.聚类

# 合并数据
k_data = pd.concat([R_days, F, M], axis=1)
k_data.columns = ['R', 'F', 'M']

5.1 特征工程

查看数据的正态性

from scipy import stats
from scipy.stats import norm, skew 
plt.style.use('fivethirtyeight')

def draw_dist_prob(data):    
    fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(24, 12), dpi=300)
    
    for i,j in enumerate(['R', 'F', 'M']):
        sns.distplot(data[j], fit=norm, ax=ax[0][i])
        (mu, sigma) = norm.fit(data[j])
        ax[0][i].legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],loc='best')
        ax[0][i].set_ylabel('数量')
        ax[0][i].set_title('{} 频数图'.format(j))
    
        stats.probplot(data[j], plot=ax[1][i])

draw_dist_prob(k_data)

在这里插入图片描述

  • 可以看到数据均呈现长尾分布,且存在一定的异常值
  • 异常值的处理需要根据业务情况判断,电商数据需要慎重对待。处理方法可参考《使用K-Means聚类算法检测离群点》,本文暂不作处理。
# 查看偏度、峰度
pd.DataFrame([i for i in zip(k_data.columns, k_data.skew(), k_data.kurt())], 
             columns=['特征', '偏度', '峰度'])

在这里插入图片描述
可以看到峰度与偏度均较大。

5.1.1 box-cox转换
# R中存在0值,进行box-cox转换时存在0值会将其转变成无穷大,所以将所有的值加上一个很小的数全部变成正数
k_data.R = k_data.R + 0.0001
# boxcox转换
k_data_bc = k_data.copy()
for i in k_data_bc.columns:  # 自动计算λ
    k_data_bc[i], _ = stats.boxcox(k_data_bc[i])
# 查看偏度、峰度
pd.DataFrame([i for i in zip(k_data_bc.columns, k_data_bc.skew(), k_data_bc.kurt())], 
             columns=['特征', '偏度', '峰度'])

在这里插入图片描述
对于一组数据来说,如果计算出来的偏度和峰度都在0附近,那么可以初步判断其分布服从正态分布。

draw_dist_prob(k_data_bc)

在这里插入图片描述
数据分布相较于变换之前更符合正态分布。

5.1.2 标准化
# K-means 的本质是基于欧式距离的数据划分算法,均值和方差大的维度将对数据的聚类产生决定性影响。
# 使用标准化对数据进行预处理可以减小不同量纲的影响。
from sklearn.preprocessing import StandardScaler

standard_scaler = StandardScaler()
standard_scaler.fit(k_data_bc)
data_scaler = standard_scaler.transform(k_data_bc)
k_data_scaler = pd.DataFrame(data_scaler, columns = ['R', 'F', 'M'], index = k_data.index)
k_data_scaler.head()

在这里插入图片描述

5.2 K-Means建模

5.2.1 k值选取
5.2.1.1 肘部法则选取k
from sklearn.cluster import KMeans
# 选择K的范围 ,遍历每个值进行评估
inertia_list = []
for k in range(1,10):
    model = KMeans(n_clusters = k, max_iter = 500, random_state = 12)
    kmeans = model.fit(k_data_scaler)
    inertia_list.append(kmeans.inertia_)

# 绘图
fig,ax = plt.subplots(figsize=(8,6))    
ax.plot(range(1,10), inertia_list, '*-', linewidth=1)
ax.set_xlabel('k')
ax.set_ylabel("inertia_score") 
ax.set_title('inertia变化图')
plt.show()

在这里插入图片描述
观察上图可知K=2时有明显的拐点,但实际业务中2类并不能够很好地满足需求,所以接下来使用廓系数评估。

5.2.1.2 轮廓系数

使用轮廓系数评估聚类效果—轮廓系数的区间为:[-1, 1]。
-1代表分类效果差,1代表分类效果好。0代表聚类重叠,没有很好的划分聚类。

from sklearn import metrics

label_list = []
silhouette_score_list = []
for k in range(2,10):
    model = KMeans(n_clusters = k, max_iter = 500, random_state=123 )
    kmeans = model.fit(k_data_scaler)
    silhouette_score = metrics.silhouette_score(k_data_scaler, kmeans.labels_)  # 轮廓系数
    silhouette_score_list.append(silhouette_score)
    label_list.append({k: kmeans.labels_})
    
# 绘图
fig,ax = plt.subplots(figsize=(8,6))    
ax.plot(range(2,10), silhouette_score_list, '*-', linewidth=1)
ax.set_xlabel('k')
ax.set_ylabel("silhouette_score") 
ax.set_title('轮廓系数变化图')
plt.show()

在这里插入图片描述
观察轮廓系数可得,分为2类的效果最好,3-6类的效果相近,7-9类的效果最差。

5.2.1.3 calinski-harabaz Index

calinski-harabaz Index:适用于实际类别信息未知的情况,为群内离散与簇间离散的比值,值越大聚类效果越好。

calinski_harabaz_score_list = []
for i in range(2,10):
    model = KMeans(n_clusters = i, random_state=1234)
    kmeans = model.fit(k_data_scaler)
    calinski_harabaz_score = metrics.calinski_harabasz_score(k_data_scaler, kmeans.labels_)
    calinski_harabaz_score_list.append(calinski_harabaz_score)

# 绘图
fig,ax = plt.subplots(figsize=(8,6))    
ax.plot(range(2,10), calinski_harabaz_score_list, '*-', linewidth=1)
ax.set_xlabel('k')
ax.set_ylabel("calinski_harabaz_score") 
ax.set_title('calinski_harabaz_score变化图')
plt.show()

在这里插入图片描述
分2类的效果最好,其次是3类,效果随着k增大而变差

综上,分为2类的效果最好,但是不符合业务诉求,分3类效果次之,所以退而求其次本文将其分为3类。

5.2.2 效果可视化
# 分为3类
model = KMeans(n_clusters = 3, random_state=12345)
kmeans = model.fit(k_data_scaler)
k_data['label'] = kmeans.labels_
k_data_scaler['label'] = kmeans.labels_
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(12, 8), dpi=200)
ax = Axes3D(fig, rect = [0,0,.95, 1], elev = 30, azim = -30)

ax1 = ax.scatter(k_data_scaler.query("label == 0").R, k_data_scaler.query("label == 0").F, 
                     k_data_scaler.query("label == 0").M, edgecolor = 'k', color = 'r')
ax2 = ax.scatter(k_data_scaler.query("label == 1").R, k_data_scaler.query("label == 1").F, 
                     k_data_scaler.query("label == 1").M, edgecolor = 'k', color = 'b')
ax3 = ax.scatter(k_data_scaler.query("label == 2").R, k_data_scaler.query("label == 2").F, 
                     k_data_scaler.query("label == 2").M, edgecolor = 'k', color = 'c')
ax.legend([ax1, ax2, ax3], ['Cluster 1', 'Cluster 2', 'Cluster 3'])
ax.invert_xaxis()
ax.set_xlabel('R')
ax.set_ylabel('F')
ax.set_zlabel('M')
ax.set_title('K-Means Clusters')
plt.show()

在这里插入图片描述

5.3 分析

# 选出每个客户的消费总金额,下单总数,下单的产品总数
data_temp = data.groupby('CustomerID').agg({'Quantity':np.sum, 'InvoiceNo':'nunique'})
kmeans_data = pd.concat([k_data, data_temp],axis=1)
kmeans_data.columns = ['最近消费天数', '消费次数', '消费金额', '客户等级', '购买商品总量','订单总量']
kmeans_level = customer_level_statistic(kmeans_data)
kmeans_level

在这里插入图片描述

  • 观察上表可以看到:
    • 1类客户数量近四分之一的数量贡献了四分之三的价值,近似符合帕累托法则,为主要消费客户
    • 2类客户对平台具有一定的价值,其最近消费的平均天数在三个月以内,可以采取一定措施来挽回此类客户。
    • 0类客户消费人数距离最近购买的平均天数超过5个月,且其价值最低,已基本流失。
kmeans_level.index = ['重要价值客户', '中等价值客户', '低价值客户']
customer_level_pie(kmeans_level, 'K-Means')
customer_level_barh(kmeans_level)

在这里插入图片描述
在这里插入图片描述

6. 总结

无论是RFM模型还是基于RFM加权得分或者聚类算法得到的结果,都能够都可以在一定程度上较好的将客户分层。但三种方法都有使用场景,也都有局限性,

  • RFM模型得到的不同层级的客户,可以采取针对性措施进行营销,但是销售场景受限。
  • 加权得分可以根据不同的业务场景灵活的调整比重来划分客户层级,但权重的分配具有一定的主观性。
  • 聚类则可以较好的区分出各层客户,对于业务来说解释性不够,数据更新前后的两次聚类结果,有的客户会被分为不同的层级,对于后续的操作比较麻烦。
;