RFM模型分析实战

导语:

RMF模型可以用来管理客户价值,通过R、F、M三个维度对用户进行分类,以便做更加精细化运营。具体解释大家可以在各大平台查阅,本文将RFM模型用于在线音乐产品中,以此记录。真诚欢迎拍砖!

一、数据监控

新版本迭代后增加vip歌单功能,用户需购买vip成为会员后方可进行播放行为。因此,主要对vip用户数、付费率进行监控。

二、发现问题

自上线以来,付费率无明显提升。想着看下哪些用户适合定向推送vip内容,哪些适合赠送vip以增加粘性,因此有了这次RFM探索性分析模型。

常用的探索性分析方法包括:RFM分析、聚类分析、因子分析、对应分析等

三、设立解决目标

通过RFM模型,探索用户价值,将用户分重要价值用户、重要发展用户、重要保持用户、重要挽回用户等8个层级进行精细化运营。目的是提高产品付费率。

四、数据探索与分析***

1.数据收集

1) RFM三个维度

虽然RFM通常指发生交易的用户,但由于业务不同,在线音乐产品的交易行为即是播放行为。

R:最近一次发起播放的日期(原:最近一次消费到当前的时间间隔)
F:近半年发起播放的总次数(原:固定时间内购买次数)
M:近半年发起播放的总播放时长(原:固定时间内消费总额)

2) Mysql取数据

  1. 如何取R:用row_number() over函数取将uid近半年发起播放的期降序排列,并取出top1即为最近一次发起播放的日期
select t.uid,t.cdate as R
from
(select uid,cdate,
row_number() over (partition by uid order by cdate desc) as rank
from 表名
where 日期区间 )t
where rank=1
  1. 如何取F:以uid为分组,计算近半年内日期出现的次数
select uid,count(distinct cdate) as F
from 表名
where 日期区间
group by uid
  1. 如何取 M:以uid为分组,计算近半年内日期播放总时长(分钟为单位)
select uid,sum(pt)/60 as pt
fromfrom 表名
where 日期区间
group by uid

3) python了解并清洗数据

  1. 导入csv文件
import os
import pandas as pd
import numpy as np
import datetime
os.chdir('/Users/xy/Desktop/RFM')
file = pd.read_csv('RFM_data.csv',engine = 'python') #104w行
  1. 了解文件格式
print(file.shape)#df的形状 104w条记录
print(file.head(10))#获取前10行
print(file.tail(10))#获取后10行
print(file.dtypes)#获取各列类型

print(file.describe())#了解各字段描述性统计

104w条记录,F最大值183,最小值1,还算正常;M最大值730w分钟,均值267分钟,需要清洗;R日期正常。
uid设备id也存在null和不符合规范的情况,也需要清洗。

  1. 清洗数据
file2 = file[file['uid'].str.len()>=15]#一般来说,uid长度不能少于15个字符,90w行
file3 = file2[file2['playsum']<=263520]#总播放时长清洗,90w
file4 = file3[file3['frequency']>=1]#播放次数清洗,90w
file4.reset_index(drop = True,inplace=True)#清洗后重新设置行索引
df['recently']=pd.to_datetime(df['recently'],format='%Y%m%d')#将int转换成日期类型
df = file4
df['playsum'] =(df['playsum']).astype(np.int) #对播放时长取整数
df['playsum'].replace(0,np.NaN,inplace=True)
df.dropna(axis=0,how='any',inplace=True)#将播放时长为0行删除
df.reset_index(drop = True,inplace=True)#重新索引,还剩75w行
df['recently_diff'] = (pd.to_datetime(df['recently'])-pd.to_datetime('2020-03-01')).map(lambda x:x.days)#计算最近播放日期与3月1日之间的距离
  1. 分析数据
df2 = pd.DataFrame({'recently_counts':df['recently_diff'].value_counts(),
                   'recently_sum':df['recently_diff'].value_counts().sum()})
df2['recently_%'] = (df2['recently_counts']/df2['recently_sum']*100).round(1)
df2.reset_index(inplace = True)
plt.bar(df2['index'],df2['recently_%'])
plt.xlabel('最近一次播放日期距今天天数')
plt.ylabel('占总体百分比(单位:%)')

最近一次播放日期距今天天数占比情况.png

图上处于波浪趋势,天数越久远波动越小。

df3 = pd.DataFrame({'frequency_counts':df['frequency'].value_counts(),
                   'frequency_sum':df['frequency'].value_counts().sum()})
df3['frequency_%'] = (df3['frequency_counts']/df3['frequency_sum']*100).round(1)
df3.reset_index(inplace = True)
plt.bar(df3['index'][:60],df3['frequency_%'][:60])#后60个忽略不计
plt.xlabel('近半年播放次数')
plt.ylabel('占总体百分比(单位:%)')
近半年播放次数占比情况.png

柱形图呈长尾分布,说明更多用户近半年仅播放1次就流失了,大于60以外的用户属于高频播放用户,稍后在分箱时尤其注意。播放日期也是同样

#直方图看R分布,能用等频分箱
ax_R = plt.subplot(223)
ax_R.hist(df['recently_diff'])
df['recently_cut'] = pd.qcut(df['recently_diff'],10,duplicates='drop',labels = [1,2,3,4,5,6,7,8,9,10])
print(df['recently_cut'].value_counts()) #验证

#直方图看F分布,长尾分布,播放次数较少的频次较大,采用自定义分箱方法
ax_F = plt.subplot(222)
plt.title('近半年播放次数频次分布')
y2 = df['frequency'].value_counts()
x2 = y2.index
ax_F.hist(df['frequency'])

#直方图看M分布,长尾分布,播放时长较少的频次较大,采用自定义分箱方法
ax_M = plt.subplot(222)
plt.title('近半年播放时长频次分布')
y2 = df['playsum'].value_counts()
x2 = y1.index
ax_M.hist(df['playsum'])
#开始分箱,分为5个档位
#R近半年最近一次播放日期距当今天数5-1分,反向打分
df['recently_cut'] = pd.qcut(df['recently_diff'],5,duplicates='drop',labels = [1,2,3,4,5])
df['recently_cut'] = 6-df['recently_cut']

#F近半年播放次数1-5分,正向打分
binsF = [0,1,12,40,114,200]
df['frequency_cut'] = pd.cut(df['frequency'],bins = binsF,labels = [1,2,3,4,5])

#M近半年播放总时长1-5分,正向打分
binsM = [0,12,99,600,2000,26000]
df['playsum_cut'] = pd.cut(df['playsum'],bins = binsM,labels = [1,2,3,4,5])
df_new = df[['uid','recently_cut','frequency_cut','playsum_cut']]

五、分析结果

8种用户分层.png

这里有个默认的说法:
最近有过播放行为的客户,再次播放几率更高;
播放次数高的客户比播放次数低的客户更有可能再次播放;
播放时长较长的客户更有可能再次播放;

将RFM三个字段以3位分界线,大于等于3值认为是高等级变更为1,小于3认为低等级变更为0

def f(x):
    if x>=3:
        return 1
    else:
        return 0
def ff(x):
    if x=='111':
        return '重要价值客户'
    elif x=='011':
        return '重要唤回客户'
    elif x=='101':
        return '重要深耕客户'
    elif x=='001':
        return '重要挽回客户'
    elif x=='110':
        return '潜力客户'
    elif x=='100':
        return '新客户'
    elif x=='010':
        return '一般维持客户'
    else:
        return '流失客户'
df_new['recently_score'] = df_new['recently_cut'].apply(lambda x:f(x))
df_new['frequency_score'] = df_new['frequency_cut'].apply(lambda x:f(x))
df_new['playsum_score'] = df_new['playsum_cut'].apply(lambda x:f(x))
df_new['scores'] = df_new['recently_score'].map(str)+df_new['frequency_score'].map(str)+df_new['playsum_score'].map(str)
df_new['customer_type'] = df_new['scores'].apply(lambda x:ff(x))
print(df_new.tail(100))
部分结果.png

可视化

user_count = df_new.groupby('customer_types')['uid'].count()#各层级用户个数
play_mean = df_new.groupby('customer_types')['playsum'].mean()#各层级播放总时长
freq_mean = df_new.groupby('customer_types')['frequency'].mean()#各层级播放总次数
df_new2 = pd.DataFrame({'各层级用户个数':user_count,
                       '各层级播放总时长':play_mean.astype(np.int),
                       '各层级播放总次数':freq_mean.astype(np.int)})
df_new2['各层级用户个数占比'] = (df_new2['各层级用户个数']/df_new2['各层级用户个数'].sum()).apply(lambda x:format(x,'.1%'))
df_new2['各层级播放总时长占比'] = (df_new2['各层级播放总时长']/df_new2['各层级播放总时长'].sum()).apply(lambda x: format(x, '.1%'))
df_new2['各层级播放总次数占比'] = (df_new2['各层级播放总次数']/df_new2['各层级播放总次数'].sum()).apply(lambda x: format(x, '.1%'))
print(df_new2 )

各层级用户个数及占比情况
#设置字体、图形样式
sns.set_style("whitegrid")
matplotlib.rcParams['font.sans-serif'] = ['SimHei']
matplotlib.rcParams['font.family']='sans-serif'
matplotlib.rcParams['axes.unicode_minus'] = False
matplotlib.fontsize='15'
 
#取做图数据
x=range(len(df_new2.index))
y1=df_new2['各层级用户个数']
y2=((df_new2['各层级用户个数']/df_new2['各层级用户个数'].sum())*100).round(1)

 
#设置图形大小
plt.rcParams['figure.figsize'] = (20.0,10.0) 
 
fig = plt.figure()
 
#画柱子
ax1 = fig.add_subplot(111)
ax1.bar(x, y1,alpha=.7,color='cadetblue')
ax1.set_ylabel('各层级用户个数',fontsize='15')
ax1.set_title("各层级用户个数及占比情况",fontsize='20')  
plt.yticks(fontsize=15)
plt.xticks(x,df_new2.index)
plt.xticks(fontsize=15)
 
# 画折线图
ax2 = ax1.twinx()  # 这个很重要噢
ax2.plot(x, y2, 'blueviolet',marker='*',ms=10)
 
ax2.set_ylabel('各层级用户个数占比(%)',fontsize='15')
ax2.set_xlabel('各层级用户个数占比(%)')
 
# 纵轴标签
plt.yticks(fontsize=15)
plt.xticks(x,df_new2.index)
plt.xticks(fontsize=15)
plt.grid(False)


#添加数据标签
for x, y ,z in zip(x,y2,y1):
        plt.text(x, y+0.8, str(y), ha='center', va='bottom', fontsize=14,rotation=0)
        plt.text(x, z-z, str(z), ha='center', va='bottom', fontsize=14,rotation=0)
     

各层级用户个数及占比情况.png
各层级播放总时长占比情况
#取做图数据
x=range(len(df_new2.index))
y1=df_new2['各层级播放总时长']
y2=((df_new2['各层级播放总时长']/df_new2['各层级播放总时长'].sum())*100).round(1)

 
#设置图形大小
plt.rcParams['figure.figsize'] = (20.0,10.0) 
 
fig = plt.figure()
 
#画柱子
ax1 = fig.add_subplot(111)
ax1.bar(x, y1,alpha=.7,color='cadetblue')
ax1.set_ylabel('各层级播放总时长',fontsize='15')
ax1.set_title("各层级播放总时长占比情况",fontsize='20')  
plt.yticks(fontsize=15)
plt.xticks(x,df_new2.index)
plt.xticks(fontsize=15)
 
# 画折线图
ax2 = ax1.twinx()  # 这个很重要噢
ax2.plot(x, y2, 'blueviolet',marker='*',ms=10)
 
ax2.set_ylabel('各层级播放总时长占比(%)',fontsize='15')
ax2.set_xlabel('各层级播放总时长占比(%)')
 
# 纵轴标签
plt.yticks(fontsize=15)
plt.xticks(x,df_new2.index)
plt.xticks(fontsize=15)
plt.grid(False)


#添加数据标签
for x, y ,z in zip(x,y2,y1):
        plt.text(x, y+0.8, str(y), ha='center', va='bottom', fontsize=14,rotation=0)
        plt.text(x, z-z-1, str(z), ha='center', va='bottom', fontsize=14,rotation=0)
各层级播放总时长占比情况.png
各层级播放总次数占比情况
#取做图数据
x=range(len(df_new2.index))
y1=df_new2['各层级播放总次数']
y2=((df_new2['各层级播放总次数']/df_new2['各层级播放总次数'].sum())*100).round(1)

 
#设置图形大小
plt.rcParams['figure.figsize'] = (20.0,10.0) 
 
fig = plt.figure()
 
#画柱子
ax1 = fig.add_subplot(111)
ax1.bar(x, y1,alpha=.7,color='cadetblue')
ax1.set_ylabel('各层级播放总次数',fontsize='15')
ax1.set_title("各层级播放总次数占比情况",fontsize='20')  
plt.yticks(fontsize=15)
plt.xticks(x,df_new2.index)
plt.xticks(fontsize=15)
 
# 画折线图
ax2 = ax1.twinx()  # 这个很重要噢
ax2.plot(x, y2, 'blueviolet',marker='*',ms=10)
 
ax2.set_ylabel('各层级播放总次数占比(%)',fontsize='15')
ax2.set_xlabel('各层级播放总次数占比(%)')
 
# 纵轴标签
plt.yticks(fontsize=15)
plt.xticks(x,df_new2.index)
plt.xticks(fontsize=15)
plt.grid(False)


#添加数据标签
for x, y ,z in zip(x,y2,y1):
        plt.text(x, y+0.8, str(y), ha='center', va='bottom', fontsize=14,rotation=0)
        plt.text(x, z-z+1, str(z), ha='center', va='bottom', fontsize=14,rotation=0)
各层级播放总次数占比情况.png

六、制作模型

后续补充

七、结论

  1. 经过分析可以知道一般发展(新用户)和一般挽留客户(流失用户)占比最高,一般挽留指最近未发起播放,播放次数极少,播放时长极少的用户,属于流失用户,由于占比较大,因此可以对这些用户建立流失模型,分析流失用户与留存用户的区别,采取相应措施避免流失,进一步在预算可观的前提下挽留部分值得挽留的流失用户。文章请戳:用户流失预测模型
  1. 一般发展(新用户)客户指最近有过播放,但是播放时长和播放次数较少,这类用户可能多数为新客。对于任何产品来说,新用户在初期属于探索阶段,可以让用户尝到一些甜头培养使用产品的习惯,建议赠送VIP活动

  2. 一般价值(潜力用户)客户指最近有过播放和一定的播放次数,但是播放时长较少,这类属于有潜力的用户,建议通过运营手段延长听歌时间

  3. 重要发展(深耕)用户与重要价值用户个数相似,但是在播放次数和播放总时长相差甚远,要想让重要发展用户成为价值用户,可以通过分析两个层级用户在产品中行为不同,或着重对播放数据进行分析,比如:两类用户播放路径、播放内容的差别,找到重要价值用户的aha时刻对重要发展用户进行培养推广

  4. 重要保持用户指最近未播放但近半年播放次数和播放时长占比较高的用户,这种用户在我们的产品中有一定财富积累,最近没有播放可能是竞品影响或没有唤起播放习惯。因此对这类用户进行唤回,建议根据用户播放记录结合VIP文案推送个性化推荐内容的push

  5. 重要价值用户是我们的核心用户,可以通过分析这类用户具体行为得到很多增长点,可以分析这类用户的行为路径或进行用户访谈,了解产品的吸引力或优化之处。同时他们购买vip的概率也是极高的,建议对这类用户重点推广VIP付费

  6. 重要挽留播放次数较少但是在播放时长上贡献较大,说明用户一旦活跃就会为发生长时间播放,需要为其培养活跃习惯,建议可以对这类用户运营连续播放打卡赠1个月VIP活动等。(与新用户不同的是,重要挽留客户用户获得VIP活动需要付出一定时间)

八、实施方案

事后运营同学针对不同用户有针对性的开展相应的活动:

image.png

从数据上看,音乐能量活动效果很好,新客送vip活动需要改进。
后续对付费率的监控,本次活动使付费率(实际付费,除赠送)提升0.8个点

总结:

做RFM模型时,最让我头疼之处在于分箱,因为真实的业务数据中播放时长和播放次数肯定是小占大量,也就是长尾分布,查了很多有关分箱的资料。经过试验发现并不适合等频和等距,于是只能用自定义分箱,这里不确定自定义划分的准确性,如果以后在学到可用的资料,我会及时更新本文。卡方分箱看到一篇好文章,准备试验一下~感谢,听我的碎碎念!!!Peace and love❤️

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

推荐阅读更多精彩内容