前面的话
非常感谢这两篇高质量文章的作者,本文所有的理论部分都来自第一篇文章,部分 Python 数据预处理代码来自第二篇文章:
本文与其他文章区别在哪里?
如果你擅长使用搜索引擎,会发现淘宝/电商用户行为分析的文章非常多,但是都有几个特点:
- 理论部分逻辑混乱
- 没有给出使用的数据源,读者无法使用已有代码进行实践
本文区别于这些文章的点在于:
- 参考了我认为理论部分逻辑最清晰的文章
- 给出了所使用的数据源,下载了数据源之后,你可以直接使用数据源和本文的代码进行分析
本文的缺点在于:
- 仅仅给出了指标计算的方法,没有提供基于指标的分析结论
案例背景
背景
随着电商⾏业近⼏年的迅猛发展,电⼦商务从早些年的粗放式经营,逐步转化为精细化运营。随着平台数据量的不断积累,通过数据分析挖掘消费者的潜在需求,消费偏好成为平台运营过程中的重要环节。本项⽬基于某电商平台⽤户⾏为数据,在 MySQL 关系型数据库,探索⽤户⾏为规律,寻找⾼价值⽤户;分析商品特征,寻找⾼贡献商品;分析产品功能,优化产品路径。
分析流程
首先,我们看一下最下面的 基础数据 模块。这些基础数据一般是由业务系统工程师提供,他们可能会采集一些日志文件,并对这些日志文件进行分析,然后放入数据库中。此案例中的基础数据可能会涉及到用户行为数据,例如:当用户收藏某件商品时,会生成一条记录;当用户将商品加入购物车时,会生成一条记录;当用户进行支付购买时,会生成一条记录……每当有此类特殊事件触发时,就会生成一条记录并将其加入数据库中(或者,写入日志文件中)。这些基础数据就是我们后期进行一些分析工作的前提。
有了基础数据之后,在进行分析之前,我们还需要对其进行一些处理:
- 数据清洗:为什么要进行数据清洗呢?因为原始的基础数据中可能存在一些脏数据,即那些不符合规范要求的数据。那么,为什么会存在脏数据呢?因为业务系统工程师在采集这些数据的过程中,可能本身就存在一些不规范的地方;另外,业务工程师在采集数据时可能并不能提前知道后续的业务分析中对于数据有哪些要求,例如采集过程中可能某些字段为空,而实际业务要求该字段不能为空等等。具体脏数据的定义还需要结合实际项目情况来决定,例如:出现空字段、重复记录等。因此,在进行正式分析之前,需要先对基础数据进行数据清洗。
- 数据预处理:所谓预处理就是提前对数据进行一些处理,它是相对正式分析过程中的数据处理而言的。例如,后期的很多指标统计都依赖于某行为产生时的日期(YYYY/MM/DD),但是基础数据中的相关信息只有完整的时间戳形式(”日期 + 时间“),而考虑到后期很多地方都需要用到单独的日期信息,所以我们可以在原始时间戳的基础上,对其进行预处理,生成一个新的日期字段方便后期分析时使用。
在完成数据清洗和预处理后,我们就来到了 指标体系,即我们需要统计分析哪些指标,它其实是在整个项目经营设计阶段(即正式经营之前)就应该提前构建的。本例中,指标体系的构建基于 ”人 + 货 + 场“ 理论:”人“ 指的是电商平台的用户,”货“ 指的是电商平台的产品,”场“ 指的是电商平台本身。也就是说,我们需要分别对用户、商品、平台进行数据分析。
接下来,我们来看一下针对用户、商品、平台,都有哪些具体指标需要进行分析:
- 首先,针对用户,我们可以进行 PV 和 UV 的计算,这里 PV(Page View)指的是访问次数,即一定时间内某个页面的浏览次数;UV(Unique Visitor)指的是访问人数,即一定时间内访问某个页面的人数。 然后,我们还可以计算 留存,它指的是某一个统一时段内新增的用户数中,经过一段时间后仍在使用我们网站或者 APP 的用户占当初新增用户的比例。 再就是 购买行为 的计算,即用户购买了什么商品、属于什么品类、购买量是多少等等。然后,我们还可以使用 RFM 模型 进行统计,它是一种用于衡量当前用户价值和用户潜在价值的工具,即当前用户对平台产生了多少贡献,以及他们未来会对平台产生多少贡献,是否属于高价值用户等。通过 RFM 模型,我们可以对目标用户进行精准定位。
- 然后,针对商品,我们可以计算某商品的 点击量、收藏量、加入购物车的数量、购买量、购买转化率 等。
- 最后,针对平台,我们可以对商品被浏览、收藏、添加到购物车、最终购买 —— 这类在整个流程中比较重要的一些节点数据进行分析,计算 每个环节的转化率 是多少,看一下是否其中某个环节存在问题(系统功能、用户体验等),并针对该环节进行 功能优化。
因此,上图反映出了本案例中的业务流程。一般经典的电商类数据分析流程基本都与之类似,即:
基础数据 ⟶ 数据清洗、预处理 ⟶ “人货场” 指标体系 ⟶ 统计各种指标 ⟶ 根据这些指标来分析指导后期运营策略
使⽤ “⼈货场” 拆解⽅式建⽴指标体系
最终结果:评价 “⽤户、商品、平台“ 三者的质量
⼈货场
- “人”(⽤户) 是整个运营的核⼼。所有举动都围绕着,如何让更多的⼈有购买⾏为,让他们买的更多,买的更贵。所以对⼈的洞察是⼀切⾏为的基础。⽬前平台上的主⼒消费⼈群(用户)有哪些特征,他们对货(商品)有哪些需求,他们活跃在哪些场(平台),还有哪些有消费⼒的⼈⽬前不在平台上,对这些问题的回答指向了接下来的⾏动。
- “货” (商品) 就对应供给,涉及到了货品分层,哪些是红海(销量高、利润少、竞争多),哪些是蓝海(销量少、利润高、竞争少),如何进⾏动态调整,是要做⾃营还是平台,以满⾜消费者的需求(最终目的还是追求平台利益最大化)。
- “场”(平台) 就是消费者在什么场景下,以什么样的⽅式接触到了这个商品。平台可以理解为就是电商网站,但是现在这个 “场” 的概念已经前后有所延伸,往前延伸指的是推广引流等,往后延伸指的是物流、售后等。早期的导购(引流)做的⽐较简单,⽬前的场就⽐较丰富,但也暴露了淘宝和京东在导购⽅⾯的⼀些问题。⽐如内容营销,⽬前最好的可能是微信的 KOL(关键意见领袖,例如:网红、大 V 等) ⽣态和⼩红书,甚⾄微博,⽽不在电商⾃⼰的场。如何做⼀个全域的打通,和消费者进⾏多触点的接触,⽐如社交和电商联动,来完成销售转化,这就是腾讯和阿⾥⼀直都在讲的 “全域营销”。
确认问题
本次分析的⽬的是想通过对⽤户⾏为数据进⾏分析,为以下问题提供解释和改进建议:
- 基于漏⽃模型的⽤户购买流程各环节分析指标,确定各个环节的转换率,便于找到需要改进的环节;
- 商品分析:找出热销商品,研究热销商品特点;
- 基于 RFM 模型找出核⼼付费⽤户群,对这部分⽤户进⾏精准营销(进一步挖掘 / 挽留)。
准备⼯作
在这里我们使用的数据集是:User Behavior Data from Taobao for Recommendation
数据集一共有 1 亿条数据,我们需要事先使用 Python 进行简单的数据预处理:
import pandas as pd
# set field name
columns = ["user_id","item_id","category_id","behavior","timestamp"]
data = pd.read_csv("UserBehavior.csv",header=None,sep=",",iterator=True,names=columns,encoding='utf-8')
# 读取全部数据,需要等一会儿
user_behavior = data.get_chunk(100150807)
#user_behavior.info()
因为我们一共有 100 万名用户共 1 亿条数据,因此数据集的样本量最好不要太小,最好是 100 万的整数倍,这里我们抽取 300 万条交易记录作为分析例子。
userbehavior_3M = user_behavior.sample(frac = 0.03)
数据集的每一行表示一条用户行为,由用户ID、商品ID、商品类目ID、行为类型和时间戳组成,并以逗号分隔。关于数据集中每一列的详细描述如下:
列名称 | 说明 |
---|---|
用户ID | 整数类型,序列化后的用户ID |
商品ID | 整数类型,序列化后的商品ID |
商品类目ID | 整数类型,序列化后的商品所属类目ID |
行为类型 | 字符串,枚举类型,包括('pv', 'buy', 'cart', 'fav') |
时间戳 | 行为发生的时间戳 |
注意到,用户行为类型共有四种,它们分别是:
行为类型 | 说明 |
---|---|
pv | 商品详情页pv,等价于点击 |
buy | 商品购买 |
cart | 将商品加入购物车 |
fav | 收藏商品 |
接下来,我们创建数据表,并将数据导入到 MySQL 中:
- 由于原始数据集的时间为时间戳格式,因此我们在创建表的时候,将
timestamp
的数据类型设置为VARCHAR
。 - 已知行为类型是字符串的枚举类型,因此我们创建表的时候直接使用枚举类型,这个字段也可以使用
VARCHAR
作为数据类型。 - 创建表的时候需要注意,字段名称要和 pandas 列名称相同(大小写不敏感)
DROP DATABASE IF EXISTS UserBehavior;
CREATE DATABASE UserBehavior;
USE UserBehavior;
DROP TABLE IF EXISTS userbehavior;
CREATE TABLE IF NOT EXISTS userbehavior(
user_id INT,
item_id INT,
category_id INT,
behavior ENUM('pv', 'buy', 'cart', 'fav'),
timestamp VARCHAR(20)
);
最开始我尝试使用 MySQL Workbench 的 Table Data Import Wizard,结果发现数据导入的效率太低了,因此还是决定使用 Python 导入数据,这是本文最后一次使用 Python 的地方了,对 Python 不太熟悉的小伙伴可以放心。
# engine = create_engine('dialect+driver://username:password@host:port/database')
# dialect -- 数据库类型、driver -- 数据库驱动选择、username -- 数据库用户名、password -- 用户密码
# host 服务器地址、port 端口、database 数据库
from sqlalchemy import create_engine
#root是用户名,12345678是密码,localhost是指本地服务器,port 端口是3306,UserBehavior是在 MySQL 中创建的数据库,userbehavior 是在 MySQL 中创建的表。
engine = create_engine("mysql+pymysql://root:12345678@localhost:3306/UserBehavior?charset=utf8")
userbehavior_3M.to_sql(name='userbehavior', con=engine, if_exists='append',index=False) # index_label='id'
查找缺失值
SELECT count(user_id),
count(item_id),count(category_id),
count(behavior),count(time)
from user_behavior;
确认并没有缺失值
查询空值和重复值
select *, count(*) from userbehavior
GROUP BY user_id,item_id,category_id,behavior,timestamp
HAVING count(*)>1;
确认没有重复值
处理时间格式:
- 由于原始数据集的时间格式是
timestamp
,因此我们需要进行一下简单处理,将时间戳格式的时间转换为常见的时间类型:
ALTER TABLE userbehavior ADD COLUMN date_time TIMESTAMP(0) NULL;
UPDATE userbehavior SET date_time = FROM_UNIXTIME(timestamp);
ALTER TABLE userbehavior DROP COLUMN timestamp;
SELECT * FROM userbehavior;
- 数据集包含了 2017 年 11 月 25 日至 2017 年 12 月 3 日之间,有行为的约一百万随机用户的所有行为(行为包括点击、购买、加购、喜欢)。因此我们需要去除时间区间之外的异常记录。
SELECT * FROM userbehavior;
DELETE FROM userbehavior
WHERE date_time < '2017-11-25 00:00:00'
OR date_time > '2017-12-04 00:00:00'
OR date_time IS NULL;
SELECT MAX(date_time), MIN(date_time) FROM userbehavior;
指标体系建设
“⼈ 货 场” 指标体系
⽤户指标体系
基础指标体系(UV / PV / 留存率)+ RFM 模型分析
基础指标
UV、PV、浏览深度统计
- pv:统计 behavior = 'pv' 的记录数量,需要按日统计(分组)
- uv:统计 DISTINCT user_id 的数量,需要按⽇统计(分组)
- 浏览深度:pv/uv
SELECT DATE(date_time),
COUNT(DISTINCT user_id) AS uv,
COUNT(IF(behavior = 'pv', user_id, NULL)) AS pv,
COUNT(IF(behavior = 'pv', user_id, NULL)) / COUNT(DISTINCT user_id) AS 'pv/uv'
FROM userbehavior
GROUP BY 1;
留存率(按日)统计
注意:留存率可以分为很多种,例如:新注册用户留存率、活跃用户留存率等。由于本案例中的数据并没有包含用户注册相关信息,因此这里我们统计的是活跃用户留存率。
例如:
基准日期 | 当日活跃用户 | 1 天以后活跃用户 | 2 天以后活跃用户 | …… |
---|---|---|---|---|
2017-11-25 | 100 人 | 90 人(90%) | 80 人(80%) | ...... |
我们需要:
- 获取到一个类似上面的结果集
- 基础数据中所有的日期都应该进行如上的计算
例如,对于 ID 为 98047837
的用户,以 2017-11-25
为基准日期计算之后一段时间内的日活跃率,我们需要在数据集中找到该用户 2017-11-25
之后的数据,即:
user_id | date_time | dates_after |
---|---|---|
98047837 | 2017-11-25 | 2017-11-26(如果该用户 26 日这天记录存在,那么该用户在 29 日这天就是活跃的) |
98047837 | 2017-11-25 | 2017-11-27(同上) |
...... | ...... | ...... |
因此,对于该用户,我们通过计算 (dates_after
- date_time
)得到某后续日期与基准日期相差的天数,如果结果等于:
- 1 天,我们就应该在计算 “1 天以后活跃用户” 的时候 +1,或者进行
COUNT()
计数; - 2 天,我们就应该在计算 “2 天以后活跃用户” 的时候 +1,或者进行
COUNT()
计数; - ……
然后,我们该如何得到如前面展示的同时包含 “当日活跃用户”、“1 天以后活跃用户”、“2 天以后活跃用户” 等字段的表结构呢?
我们可以使用 自关联 的方式来完成:
首先,我们可以先从表中筛选出 user_id
和 date_time
字段,并按照这两个字段分组。这种情况下,某用户在某天即使存在多条记录也只返回一条,即表明该用户在这天是活跃的:
SELECT user_id, DATE(date_time) FROM userbehavior GROUP BY 1,2;
可以看到,得到的结果与我们之前所期望的数据集还差一个 dates_after
字段,下面我们尝试通过自关联的方式来得到该日期:
SELECT *
FROM
(SELECT user_id, DATE(date_time) FROM userbehavior GROUP BY 1, 2) AS a
LEFT JOIN
(SELECT user_id, DATE(date_time) FROM userbehavior GROUP BY 1, 2) AS b
ON a.user_id = b.user_id;
现在,我们得到了两个日期字段,但是我们发现某些记录中,第二个日期字段比第一个日期字段小,例如第 2 行中,date_time
为 2017-12-05
,而 date_time(1)
为 2019-11-20
。由于我们希望计算的是基准日期当天的活跃用户数,以及之后一段时间内的活跃用户留存率,因此第二个日期字段应该要大于或者等于第一个日期字段(然后再计算两个日期之间相差的天数)。所以,我们还需要通过 WHERE
条件对上面的结果集进行过滤:
SELECT
a.user_id, a.dates,
b.dates AS dates_after
FROM
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS a
LEFT JOIN
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS b
ON a.user_id = b.user_id
WHERE DATE(b.dates) >= DATE(a.dates);
假设我们希望最终得到的数据集中包含以下字段:
- 基准日期
- 活跃用户数
- 1 日留存率
- 2 日留存率
- ……
- 7 日留存率
- 15 日留存率
- 30 日留存率
首先,对于活跃用户数,我们可以在上表的基础上,对基准日期进行分组统计得到:
SELECT
a.dates,
COUNT(DISTINCT a.user_id) AS user_count
FROM
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS a
LEFT JOIN
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS b
ON a.user_id = b.user_id
WHERE DATE(b.dates) >= DATE(a.dates)
GROUP BY 1;
现在,我们已经得到了目标结果集中的前两个字段。显然,后续的 1 日留存率、2 日留存率……30 日留存率的计算模式是相同的。而我们知道,某日留存率 = 当日留存数 / 基准日期活跃数,所以我们先计算对应日期的留存数:
SELECT
a.dates,
COUNT(DISTINCT a.user_id) AS user_count,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 1, b.user_id, NULL)) AS remain_1,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 2, b.user_id, NULL)) AS remain_2,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 3, b.user_id, NULL)) AS remain_3,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 4, b.user_id, NULL)) AS remain_4,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 5, b.user_id, NULL)) AS remain_5,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 6, b.user_id, NULL)) AS remain_6,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 7, b.user_id, NULL)) AS remain_7,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 15, b.user_id, NULL)) AS remain_15,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 30, b.user_id, NULL)) AS remain_30
FROM
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS a
LEFT JOIN
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS b
ON a.user_id = b.user_id
WHERE b.dates >= a.dates
GROUP BY a.dates;
然后,基于某日留存数,我们就可以计算出当日的留存率。为了便于后续计算,我们可以将上面的 SELECT
语句保存为视图(或者通过 WITH AS
语句创建临时表,以便在同一条 SQL 语句内进行重复调用):
CREATE VIEW user_remain_view AS
SELECT
a.dates,
COUNT(DISTINCT a.user_id) AS user_count,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 1, b.user_id, NULL)) AS remain_1,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 2, b.user_id, NULL)) AS remain_2,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 3, b.user_id, NULL)) AS remain_3,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 4, b.user_id, NULL)) AS remain_4,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 5, b.user_id, NULL)) AS remain_5,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 6, b.user_id, NULL)) AS remain_6,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 7, b.user_id, NULL)) AS remain_7,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 15, b.user_id, NULL)) AS remain_15,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 30, b.user_id, NULL)) AS remain_30
FROM
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS a
LEFT JOIN
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS b
ON a.user_id = b.user_id
WHERE b.dates >= a.dates
GROUP BY a.dates;
SELECT * FROM user_remain_view;
计算留存率(视图方法):
SELECT
dates,
user_count,
CONCAT(ROUND(remain_1 / user_count * 100, 2), '%') AS day_1,
CONCAT(ROUND(remain_2 / user_count * 100, 2), '%') AS day_2,
CONCAT(ROUND(remain_3 / user_count * 100, 2), '%') AS day_3,
CONCAT(ROUND(remain_4 / user_count * 100, 2), '%') AS day_4,
CONCAT(ROUND(remain_5 / user_count * 100, 2), '%') AS day_5,
CONCAT(ROUND(remain_6 / user_count * 100, 2), '%') AS day_6,
CONCAT(ROUND(remain_7 / user_count * 100, 2), '%') AS day_7,
CONCAT(ROUND(remain_15 / user_count * 100, 2), '%') AS day_15,
CONCAT(ROUND(remain_30 / user_count * 100, 2), '%') AS day_30
FROM user_remain_view;
另外,我们也可以通过使用 WITH AS
语句创建临时表计算留存率:
WITH temp_table_trades AS
(SELECT
a.dates,
COUNT(DISTINCT a.user_id) AS user_count,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 1, b.user_id, NULL)) AS remain_1,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 2, b.user_id, NULL)) AS remain_2,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 3, b.user_id, NULL)) AS remain_3,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 4, b.user_id, NULL)) AS remain_4,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 5, b.user_id, NULL)) AS remain_5,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 6, b.user_id, NULL)) AS remain_6,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 7, b.user_id, NULL)) AS remain_7,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 15, b.user_id, NULL)) AS remain_15,
COUNT(DISTINCT IF(DATEDIFF(b.dates, a.dates) = 30, b.user_id, NULL)) AS remain_30
FROM
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS a
LEFT JOIN
(SELECT user_id, DATE(date_time) dates FROM userbehavior GROUP BY 1, 2) AS b
ON a.user_id = b.user_id
WHERE b.dates >= a.dates
GROUP BY a.dates)
SELECT
dates,
user_count,
CONCAT(ROUND(remain_1 / user_count * 100, 2), '%') AS day_1,
CONCAT(ROUND(remain_2 / user_count * 100, 2), '%') AS day_2,
CONCAT(ROUND(remain_3 / user_count * 100, 2), '%') AS day_3,
CONCAT(ROUND(remain_4 / user_count * 100, 2), '%') AS day_4,
CONCAT(ROUND(remain_5 / user_count * 100, 2), '%') AS day_5,
CONCAT(ROUND(remain_6 / user_count * 100, 2), '%') AS day_6,
CONCAT(ROUND(remain_7 / user_count * 100, 2), '%') AS day_7,
CONCAT(ROUND(remain_15 / user_count * 100, 2), '%') AS day_15,
CONCAT(ROUND(remain_30 / user_count * 100, 2), '%') AS day_30
FROM temp_table_trades;
RFM 模型分析
RFM 是 3 个指标的缩写:
- R(Recency) 表示最近一次消费时间间隔;
- F(Frequency) 表示消费频率;
- M(Monetary) 表示消费金额。
通过以上 3 个指标对用户进行分类的方法称为 RFM 模型分析。
不同业务对各指标的定义不同,具体需要根据业务场景和需求灵活定义。各指标与用户价值之间的关系如下:
- 对于最近一次消费时间间隔(R),上一次消费时间离得越近,也就是 R 的值越小,用户价值越高;
- 对于消费频率(F),消费频率越高,也就是 F 的值越大,用户价值越高;
- 对于消费金额(M),消费金额越高,也就是 M 的值越大,用户价值越高。
将这 3 个指标 按照价值 由低到高排序,并将其作为坐标轴,我们可以将用户空间分割为 8 个象限,对应下图中的 8 类用户:
用户分类 | R | F | M |
---|---|---|---|
1. 重要价值用户 | 高 | 高 | 高 |
2. 重要发展用户 | 高 | 低 | 高 |
3. 重要保持用户 | 低 | 高 | 高 |
4. 重要挽留用户 | 低 | 低 | 高 |
5. 一般价值用户 | 高 | 高 | 低 |
6. 一般发展用户 | 高 | 低 | 低 |
7. 一般保持用户 | 低 | 高 | 低 |
8. 一般挽留用户 | 低 | 低 | 低 |
注意:上表中各指标的高低代表的是其反映出的用户价值高低,而非指标本身的数值大小。
利用 RFM 模型,我们可以将用户按照价值进行分类,对不同价值的用户采取不同的运营策略,将公司的有限资源发挥出最大效用,从而实现 精细化运营。具体步骤:
- 计算 R、F、M 的值。一般涉及 3 个数据字段:用户 ID、消费时间、消费金额。
- 给 R、F、M 值按照用户价值打分(例如,最近一次消费时间越近,用户价值越高,R 项分值就越高),一般采用 5 分制。
- 计算用户价值平均值,即分别计算 R、F、M 得分的平均值。
- 用户分类。将各用户的 R、F、M 得分分别与各自的平均值进行比较,然后按照上面的分类规则表对用户进行分类。
RFM 模型:R 部分
R 指标分析:根据每个用户最近一次购买时间,给出相应的分数。
这里我们可以分两步:
- 获取每个用户的最近购买时间;
- 计算每个用户最近购买时间距离参照日期相差几天,根据相差的天数进行打分。
注意,参照日期需要在所有购买日期之后,这里我们可以选取2017-12-04
作为参照日期。
另外,我们这里采用 5 分制,通过计算最近一次购买日期距离参照日期的天数,如果: - <= 2,打 5 分;
- <= 4,打 4 分;
- <= 6,打 3 分;
- <= 8,打 2 分;
- 其他,打 1 分。
-- R 指标分析:根据每个用户最近一次购买时间,给出相应的分数
-- 1. 建立 R 视图:统计每个用户的最近购买时间
DROP VIEW IF EXISTS user_recency_view;
CREATE VIEW user_recency_view AS
SELECT
user_id,
MAX(DATE(date_time)) AS recent_buy_time
FROM userbehavior
WHERE behavior = 'buy'
GROUP BY user_id;
-- 2. 建立 R 评分视图:计算每个用户最近购买时间距离参照日期 '2017-12-04' 的天数,
-- 根据距离天数进行打分:<=2 5分;<=4 4分;<=6 3分;<=8 2分;其他 1分
DROP VIEW IF EXISTS r_score_view;
CREATE VIEW r_score_view AS
SELECT
user_id,
recent_buy_time,
DATEDIFF('2017-12-04', recent_buy_time) AS date_distance,
CASE
WHEN DATEDIFF('2017-12-04', recent_buy_time) <= 2 THEN 5
WHEN DATEDIFF('2017-12-04', recent_buy_time) <= 4 THEN 4
WHEN DATEDIFF('2017-12-04', recent_buy_time) <= 6 THEN 3
WHEN DATEDIFF('2017-12-04', recent_buy_time) <= 8 THEN 2
ELSE 1
END AS r_score
FROM user_recency_view;
SELECT * FROM r_score_view;
RFM 模型:F 部分
F 指标分析:计算每个用户在一定时间内的消费频率,给出相应分数。
这里,我们以整个数据集中的时间跨度为准,即不限制具体时间段,统计每个用户在数据集中的所有消费次数。
同样,我们这里采用 5 分制,通过对用户分组,计算每个用户有多少条消费记录,如果:
- <= 2,打 1 分;
- <= 4,打 2 分;
- <= 6,打 3 分;
- <= 8,打 4 分;
- 其他,打 5 分。
-- 1. 建立 F 视图:统计每个用户的消费次数
DROP VIEW IF EXISTS user_frequency_view;
CREATE VIEW user_frequency_view AS
SELECT
user_id,
COUNT(user_id) AS buy_frequency
FROM userbehavior
WHERE behavior = 'buy'
GROUP BY user_id;
-- 2. 建立 F 评分视图:基于购买次数对用户进行打分
-- 按照购买次数评分:<=2 1分;<=4 2分;<=6 3分;<=8 4分;其他 5分
DROP VIEW IF EXISTS f_score_view;
CREATE VIEW f_score_view AS
SELECT
user_id,
buy_frequency,
CASE
WHEN buy_frequency <= 2 THEN 1
WHEN buy_frequency <= 4 THEN 2
WHEN buy_frequency <= 6 THEN 3
WHEN buy_frequency <= 8 THEN 4
ELSE 5
END AS f_score
FROM user_frequency_view;
SELECT * FROM f_score_view;
整合结果
由于该数据集中没有包含消费金额相关的字段信息,这里我们将仅基于最近一次消费时间间隔(R)和消费频率(F)建⽴ RFM 模型,并且将用户分为以下 4 类:
- 重要⾼价值客户:指最近⼀次消费较近⽽且消费频率较⾼的客户;
- 重要唤回客户:指最近⼀次消费较远且消费频率较⾼的客户;
- 重要深耕客户:指最近⼀次消费较近且消费频率较低的客户;
- 重要挽留客户:指最近⼀次消费较远且消费频率较低的客户。
我们将按照最近⼀次消费时间间隔的均值和消费频率的均值来确定 R 和 F 得分的⾼、低分界线。
-- 计算 R 得分和 F 得分的均值
SELECT
(SELECT AVG(r_score) FROM r_score_view) AS r_avg,
(SELECT AVG(f_score) FROM f_score_view) AS f_avg;
-- 拿到每个用户的 R 得分和 F 得分(两表关联 r_score_view 和 f_score_view),然后与均值对比,得到各用户类型
DROP VIEW IF EXISTS rfm_inall_view;
CREATE VIEW rfm_inall_view AS
SELECT
r.user_id,
r.r_score,
f.f_score,
CASE
WHEN r.r_score > 2.7939 AND f.f_score > 2.2606 THEN '重要高价值客户'
WHEN r.r_score < 2.7939 AND f.f_score > 2.2606 THEN '重要唤回客户'
WHEN r.r_score > 2.7939 AND f.f_score < 2.2606 THEN '重要深耕客户'
WHEN r.r_score < 2.7939 AND f.f_score < 2.2606 THEN '重要挽留客户'
END AS user_class
FROM r_score_view AS r INNER JOIN f_score_view AS f ON r.user_id = f.user_id;
SELECT * FROM rfm_inall_view;
-- 统计各用户的用户类型、最近一次购买时间间隔、购买频率
DROP VIEW IF EXISTS user_class_view;
CREATE VIEW user_class_view AS
WITH temp_rf AS (
SELECT
r.user_id,
date_distance,
buy_frequency
FROM r_score_view r INNER JOIN f_score_view f ON r.user_id = f.user_id)
SELECT
rfm.user_id,
user_class,
date_distance,
buy_frequency
FROM rfm_inall_view AS rfm LEFT JOIN temp_rf AS trf ON rfm.user_id = trf.user_id;
SELECT * FROM user_class_view;
-- 统计各个用户类型下的用户数量、最近一次购买的平均时间间隔、平均消费频率
SELECT
user_class,
COUNT(user_id) AS user_count,
AVG(date_distance) AS avg_date_distance,
AVG(buy_frequency) AS avg_buy_frequency
FROM user_class_view
GROUP BY user_class;
现在,我们已经完成了对用户群体的分类,这实际上相当于在一定程度上对用户进行画像,接下来就可以针对不同用户群体采取不同的运营策略。当然,后续具体运营策略的设计和实施就属于运营人员的工作范围了。
商品指标体系
前面我们分析了 “人货场” 中与 “人(用户)” 相关的指标,现在我们来分析与 “货(商品)” 相关的指标。
我们将从下面两个维度来进行分析:
- 按照商品进行分组统计: 商品的点击量(浏览 / 曝光)、收藏量、加购量(加入购物车的次数)、购买次数、购买转化率(该商品的所有⽤户中有购买转化的⽤户占⽐)。
- 按照商品品类进行分组统计: 对应品类的点击量、收藏量、加购量、购买次数、购买转化率。
商品指标统计
-- 统计各商品的点击量、收藏量、加购量、购买次数、购买转化率
SELECT
item_id,
SUM(IF(behavior = 'pv', 1, 0)) AS pv,
SUM(IF(behavior = 'fav', 1, 0)) AS favorite,
SUM(IF(behavior = 'cart', 1, 0)) AS cart,
SUM(IF(behavior = 'buy', 1, 0)) AS purchase,
CONCAT(ROUND(COUNT(DISTINCT IF(behavior = 'buy', user_id, NULL)) /
COUNT(DISTINCT user_id) * 100, 2), '%') AS purchase_ratio
FROM userbehavior
GROUP BY item_id
ORDER BY purchase DESC, purchase_ratio DESC;
商品品类指标统计
-- 统计各品类商品的点击量、收藏量、加购量、购买次数、购买转化率
SELECT
category_id,
SUM(IF(behavior = 'pv', 1, 0)) AS pv,
SUM(IF(behavior = 'fav', 1, 0)) AS favorite,
SUM(IF(behavior = 'cart', 1, 0)) AS cart,
SUM(IF(behavior = 'buy', 1, 0)) AS purchase,
CONCAT(ROUND(COUNT(DISTINCT IF(behavior = 'buy', user_id, NULL)) /
COUNT(DISTINCT user_id) * 100, 2), '%') AS purchase_ratio
FROM userbehavior
GROUP BY category_id
ORDER BY purchase DESC, purchase_ratio DESC;
平台指标体系
前面我们分析了 “人货场” 中的 “人(用户)” 和 “货(商品)” 的相关指标,接下来我们来分析与 “场(平台)” 相关的指标。
同样,我们从以下两个维度进行分析:
- 从平台角度来看用户的行为: 按日统计整个平台的点击量、收藏量、加购量、购买次数、购买转化率。
用户行为路径分析: 用户在购买商品的时候,可能会经历一系列的操作,例如:
- “浏览 → 收藏 → 加入购物车 → 支付购买”;
- “浏览 → 支付购买”
- “浏览 → 加入购物车 → 支付购买”
我们将这些统称为行为路径。这里,我们可以以最近一次的 “支付购买” 行为为基准,并往前推 4 个操作行为,由此得到一条由 5 个操作组成的行为路径,然后统计每条行为路径下的用户数量。
平台用户指标统计
-- 统计平台每日的点击量、收藏量、加购量、购买次数、购买转化率
SELECT
DATE(date_time),
SUM(IF(behavior = 'pv', 1, 0)) AS pv,
SUM(IF(behavior = 'fav', 1, 0)) AS favorite,
SUM(IF(behavior = 'cart', 1, 0)) AS cart,
SUM(IF(behavior = 'buy', 1, 0)) AS purchase,
CONCAT(ROUND(COUNT(DISTINCT IF(behavior = 'buy', user_id, NULL)) /
COUNT(DISTINCT user_id) * 100, 2), '%') AS purchase_ratio
FROM userbehavior
GROUP BY 1
ORDER BY 1 DESC;
用户行为路径分析
如前所述,这里我们以最后的 “支付购买” 为基准,并往前推 4 个操作行为,由此得到一条由 5 个操作组成的行为路径,然后统计每条行为路径下的用户数量。
这里各行为代号可以从用户行为字段 behavior
获取:
- 1:曝光:pv
- 2:购买:buy
- 3:加⼊购物⻋:cart
- 4:加⼊收藏夹:fav
这里的核心步骤是:拼接行为路径。
例如,对于用户张三,其对于 a 商品可能存在以下行为:1、3、2、4、3、1、2。我们从最后的 2(购买)开始,往前推 4 个行为,然后拼接得到一条 “2 → 4 → 3 → 1 → 2” 的行为路径。
因此,我们需要:
- 将多个行为并列摆放,即使用窗口函数中的偏移分析函数
LAG()
或者LEAD()
为各行为分别创建一个字段; - 使用
CONCAT()
函数对各行为字段进行拼接,得到行为路径; - 统计各行为路径下的用户数量。
/*
用户行为拼接准备
使用偏移分析函数 LAG() 或者 LEAD() 为各行为分别创建一个字段,这里涉及到分组和排序:
1. 将 PARTITION BY 后的字段指定为 user_id 和 item_id
2. 将 ORDER BY 后的字段指定为 date_time
注意,这里我们排序字段指定的是 date_time 而不是 dates,因为同一天内可能存在多个行为,需要考虑它们之间的顺序。
另外,由于要统计的是最近一次购买行为对应的路径,可以使用排序函数 RANK() 按照 date_time 字段进行倒排序,然后取第一条记录。
*/
DROP VIEW IF EXISTS path_base_view;
CREATE VIEW path_base_view AS
SELECT a.* FROM
(SELECT
user_id,
item_id,
LAG(behavior, 4) OVER (PARTITION BY user_id, item_id ORDER BY date_time) AS lag_4,
LAG(behavior, 3) OVER (PARTITION BY user_id, item_id ORDER BY date_time) AS lag_3,
LAG(behavior, 2) OVER (PARTITION BY user_id, item_id ORDER BY date_time) AS lag_2,
LAG(behavior, 1) OVER (PARTITION BY user_id, item_id ORDER BY date_time) AS lag_1,
behavior,
RANK() OVER (PARTITION BY user_id, item_id ORDER BY date_time DESC) AS rank_number
FROM userbehavior) AS a
WHERE a.rank_number = 1 AND a.behavior = 'buy';
SELECT * FROM path_base_view;
-- 拼接行为路径,并统计各行为路径下的用户数量
-- 注意:lag_4 到 lag_1 可能存在 NULL,因此我们用字符串 '空' 来对其进行标记,防止 CONCAT 函数输出 NULL 值。
SELECT
CONCAT(IFNULL(lag_4, '空'), '-',
IFNULL(lag_3, '空'), '-',
IFNULL(lag_2, '空'), '-',
IFNULL(lag_1, '空'), '-',
behavior) AS behavior_path,
COUNT(DISTINCT user_id) AS user_count
FROM path_base_view
GROUP BY
CONCAT(IFNULL(lag_4, '空'), '-',
IFNULL(lag_3, '空'), '-',
IFNULL(lag_2, '空'), '-',
IFNULL(lag_1, '空'), '-',
behavior)
ORDER BY user_count DESC;