RFE基本概念
是一种基于用户普通行为(非转化或交易行为)的用户活跃度模型,主要用于评估和分析用户的活跃度和价值。它类似于RFM模型,但更侧重于用户的页面互动度。
(用户活跃度、用户价值度的分析在数据分析师的日常工作中会经常碰到,如何根据公司的业务情况对本公司的用户做活跃度和价值度的划分是是一种常规化的分析工作。)
RFE模型与RFM模型相似:
RFM模型:基于用户的交易订单数据
RFE模型:基于用户的浏览行为数据
具体指标
用户包含各种类型,反映了不同群体的特征和想法,在使用整个产品的周期中,应定义更为全面的指标:
- 流失用户:有一段时间没有打开产品,那么就会被视为流失用户,可以依据产品的属性,按照30天、60天、90天等划分.
- 不活跃用户:经常有一段时间没有打开产品,为了和流失用户区分开来,需要选择无交集的时间范围。比如流失用户是60天以上没有打开产品,那么不活跃用户就可以定为1~60天没有打开软件的用户。
- 回流用户:有一段时间没有使用产品,之后突然回来再次使用,则称之为回流用户。回流用户是活跃用户,且是有六十用户或不活跃用户唤回而来。
- 活跃用户:一段时间内打开过产品。
- 忠诚用户:也可以叫做超级活跃用户,长期持续使用产品,比如连续四周或者一个月的十五天内等。
用户生命周期
在用户生命周期中,对每个用户进行群体划分,有针对性的做分群分层运营,可以更高效的提高营收转换。(用户生命周期是指:用户从注册账号建立起业务关系->完全终止关系的全过程,它动态的描述了用户在不同阶段的大致特征)。
几乎所有软件/网站的用户都符合如下这个趋势图:
RFE模型的主要用途
- 用户活跃分群或价值区分:通过RFE模型,可以将用户划分为不同的群体,了解每个群体的活跃度和价值特点。这有助于企业更精准地定位目标用户群体,制定更有针对性的营销策略。
- 未登录用户的价值数据分析:由于RFE模型不要求用户发生交易或登录行为,因此可以对未登录用户的行为进行分析和评估。这有助于企业了解未登录用户的兴趣和需求,为他们提供更具吸引力的内容和产品。
- 数据挖掘和机器学习建模:RFE模型中的三个维度可以作为数据挖掘或机器学习的特征,用于构建预测模型。通过模型进行预测输出,可以为企业提供更深入的用户行为分析和预测能力。
桑葚图:详细的监控活跃数据的变化 (RFE标签的主要作用之一就是画桑葚图)
RFE模型含义
RFE模型是根据会员最近一次访问时间R、访问频率F和页面互动度E计算得出的RFE得分。
R(Recency)最近一次访问时间:
会员最近一次访问或到达网站的时间,max(log_time),距今天的天数
这个维度衡量的是用户最近一次访问或到达网站的时间。通过这个时间点,可以了解用户的活跃度和留存情况。
F(Frequency)访问频率:
用户在特定时间周期内访问或到达的频率,count(global_user_id),pv
这个维度衡量的是用户在特定时间周期内访问或到达网站的频率。通过访问频率,可以了解用户对网站的依赖程度和忠诚度。
E(Engagements)页面互动度:
互动度的定义可以根据不同企业或行业的交互情况而定,countDistinct(log_url) uv
这个维度衡量的是用户在网站上的互动程度,包括页面浏览时间、浏览商品数量、视频播放数量、点赞数量、转发数量等。页面互动度的高低反映了用户对网站内容的兴趣度和参与度。
例如可以定义为:页面浏览时间、浏览商品数量、视频播放量、点赞、转发数量等
在RFE模型中,由于不要求用户发生交易,因此即可以做未发生登录、注册等匿名用户的行为价值分析,也可以做实名用户分析,该模型常用来做用户活跃分群或价值区分,也可以用于内容型(例如论坛、新闻、资讯等)企业的会员分析。
RFM和RFE模型和实现思路相同,仅仅是讲计算指标发生变化,对于RFE的数据来源,可以从企业自己监控的用户行为日志获取,也可以从第三方网站分析工具获得。
RFM:业务数据,交易订单数据
RFE:访问日志,流量数据
RFE实践应用
在得到用户的RFE得分之后,跟RFM类似也可以有两种应用思路:
- 基于三个维度值做用户群体划分和解读,对用户的活跃度进行分析。
- RFE的氛围R:3, F:1, E:3的会员说明其访问频率低,但是每次访问时的交互都非常不错,此时重点要做用户回访频率的提升,例如通过活动邀请、精准广告投放、会员活动推荐等提升回访频率。
- 基于RFE的汇总得分评估所有会员的活跃度价值,并可以做活跃度排名;同时,该得分还可以作为输入维度跟其他维度一起作为其他数据分析和挖掘模型的输入变量,为分析建模提供基础。
比如:
- 6忠诚(1天内访问2次及以上,每次访问页面不重复)
- 5活跃(2天内访问至少1次)
- 4回流(3天内访问至少1次)
- 3新增(注册并访问)
- 2不活跃(7天内未访问)
- 1流失(7天以上未访问)
RFM/RFE Model
代码实现
由于这是第二个挖掘类型标签了,其中很多代码存在重复,因此我们将重复代码抽象为方法,便于使用;
如下先给出
算法模型工具类:专门以数据训练算法模型、保存及加载
import cn.itcast.tags.config.ModelConfig
import cn.itcast.tags.utils.HdfsUtils
import org.apache.hadoop.conf.Configuration
import org.apache.spark.internal.Logging
import org.apache.spark.ml.{Model, Pipeline, PipelineModel}
import org.apache.spark.ml.classification.{DecisionTreeClassificationModel, DecisionTreeClassifier}
import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
import org.apache.spark.ml.evaluation.{BinaryClassificationEvaluator, MulticlassClassificationEvaluator}
import org.apache.spark.ml.feature.{VectorAssembler, VectorIndexer}
import org.apache.spark.ml.param.ParamMap
import org.apache.spark.ml.tuning.{CrossValidator, CrossValidatorModel, ParamGridBuilder, TrainValidationSplit, TrainValidationSplitModel}
import org.apache.spark.sql.DataFrame
import org.apache.spark.storage.StorageLevel
/**
* 算法模型工具类:专门依据数据集训练算法模型,保存及加载
*/
object MLModelTools extends Logging {
/**
* 加载模型,如果模型不存在,使用算法训练模型
* @param dataframe 训练数据集
* @param mlType 表示标签模型名称
* @return Model 模型
*/
def loadModel(dataframe: DataFrame, mlType: String, clazz: Class[_]): Model[_] = {
// 获取算法模型保存路径
val modelPath = s"${ModelConfig.MODEL_BASE_PATH}/${clazz.getSimpleName.stripSuffix("$")}"
// 1. 判断模型是否存在,存在直接加载
val conf: Configuration = dataframe.sparkSession.sparkContext.hadoopConfiguration
if(HdfsUtils.exists(conf, modelPath)){
logWarning(s"正在从【$modelPath】加载模型.................")
mlType.toLowerCase match {
case "rfm" => KMeansModel.load(modelPath) // 加载返回
case "rfe" => KMeansModel.load(modelPath) // 加载返回
case "psm" => KMeansModel.load(modelPath) // 加载返回
case "usg" => PipelineModel.load(modelPath)
}
}else{
// 2. 如果模型不存在训练模型,获取最佳模型及保存模型
logWarning(s"正在训练模型.................")
val bestModel = mlType.toLowerCase match {
case "rfm" => trainBestKMeansModel(dataframe, kClusters = 5)
case "rfe" => trainBestKMeansModel(dataframe, kClusters = 4)
case "psm" => trainBestKMeansModel(dataframe, kClusters = 5)
case "usg" => trainBestPipelineModel(dataframe)
}
// 保存模型
logWarning(s"保存最佳模型.................")
bestModel.save(modelPath)
// 返回模型
bestModel
}
}
/**
* 调整算法超参数,获取最佳模型
* @param dataframe 数据集
* @return
*/
def trainBestKMeansModel(dataframe: DataFrame, kClusters: Int): KMeansModel = {
// TODO:模型调优方式二:调整算法超参数 -> MaxIter 最大迭代次数, 使用训练验证模式完成
// 1.设置超参数的值
val maxIters: Array[Int] = Array(5, 10, 20)
// 2.不同超参数的值,训练模型
dataframe.persist(StorageLevel.MEMORY_AND_DISK)
val models: Array[(Double, KMeansModel, Int)] = maxIters.map{ maxIter =>
// a. 使用KMeans算法应用数据训练模式
val kMeans: KMeans = new KMeans()
.setFeaturesCol("features")
.setPredictionCol("prediction")
.setK(kClusters) // 设置聚类的类簇个数
.setMaxIter(maxIter)
.setSeed(31) // 实际项目中,需要设置值
// b. 训练模式
val model: KMeansModel = kMeans.fit(dataframe)
// c. 模型评估指标WSSSE
val ssse = model.computeCost(dataframe)
// d. 返回三元组(评估指标, 模型, 超参数的值)
(ssse, model, maxIter)
}
dataframe.unpersist()
models.foreach(println)
// 3.获取最佳模型
val (_, bestModel, _) = models.minBy(tuple => tuple._1)
// 4.返回最佳模型
bestModel
}
/**
* 采用K-Fold交叉验证方式,调整超参数获取最佳PipelineModel模型
* @param dataframe 数据集
* @return
*/
def trainBestPipelineModel(dataframe: DataFrame): PipelineModel = {
// a. 特征向量化
val assembler: VectorAssembler = new VectorAssembler()
.setInputCols(Array("color", "product"))
.setOutputCol("raw_features")
// b. 类别特征进行索引
val vectorIndexer: VectorIndexer = new VectorIndexer()
.setInputCol("raw_features")
.setOutputCol("features")
.setMaxCategories(30)
// c. 构建决策树分类器
val dtc: DecisionTreeClassifier = new DecisionTreeClassifier()
.setFeaturesCol("features")
.setLabelCol("label")
.setPredictionCol("prediction")
// 构建Pipeline管道对象,组合模型学习器(算法)和转换器(模型)
val pipeline: Pipeline = new Pipeline()
.setStages(Array(assembler, vectorIndexer, dtc))
// TODO: 创建网格参数对象实例,设置算法中超参数的值
val paramGrid: Array[ParamMap] = new ParamGridBuilder()
.addGrid(dtc.impurity, Array("gini", "entropy"))
.addGrid(dtc.maxDepth, Array(5, 10))
.addGrid(dtc.maxBins, Array(32, 64))
.build()
// f. 多分类评估器
val evaluator = new MulticlassClassificationEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction")
// 指标名称,支持:f1、weightedPrecision、weightedRecall、accuracy
.setMetricName("accuracy")
// TODO: 创建交叉验证实例对象,设置算法、评估器和数据集占比
val cv: CrossValidator = new CrossValidator()
.setEstimator(pipeline) // 设置算法,此处为管道
.setEvaluator(evaluator) // 设置模型评估器
.setEstimatorParamMaps(paramGrid) // 设置算法超参数
// TODO: 将数据集划分为K份,其中1份为验证数据集,其余K-1份为训练收集,通常K>=3
.setNumFolds(3)
// 传递数据集,训练模型
val cvModel: CrossValidatorModel = cv.fit(dataframe)
// TODO: 获取最佳模型
val pipelineModel: PipelineModel = cvModel.bestModel.asInstanceOf[PipelineModel]
// 返回获取最佳模型
pipelineModel
}
}
RFE用户活跃度模型
import cn.itcast.tags.config.ModelConfig
import cn.itcast.tags.models.{AbstractModel, ModelType}
import cn.itcast.tags.tools.TagTools
import cn.itcast.tags.utils.HdfsUtils
import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
import org.apache.spark.ml.feature.{MinMaxScaler, MinMaxScalerModel, VectorAssembler}
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types.DataTypes
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.storage.StorageLevel
/**
* 挖掘类型标签模型开发:客户价值模型RFM
*/
class RfmTagModel extends AbstractModel("客户价值RFM", ModelType.ML){
/*
361 客户价值
362 高价值 0
363 中上价值 1
364 中价值 2
365 中下价值 3
366 超低价值 4
*/
override def doTag(businessDF: DataFrame, tagDF: DataFrame): DataFrame = {
val session: SparkSession = businessDF.sparkSession
import session.implicits._
/*
root
|-- memberid: string (nullable = true)
|-- ordersn: string (nullable = true)
|-- orderamount: string (nullable = true)
|-- finishtime: string (nullable = true)
*/
//businessDF.printSchema()
//businessDF.show(10, truncate = false)
/*
root
|-- id: long (nullable = false)
|-- name: string (nullable = true)
|-- rule: string (nullable = true)
|-- level: integer (nullable = true)
*/
//tagDF.printSchema()
/*
|id |name|rule|level|
+---+----+----+-----+
|362|高价值 |0 |5 |
|363|中上价值|1 |5 |
|364|中价值 |2 |5 |
|365|中下价值|3 |5 |
|366|超低价值|4 |5 |
+---+----+----+-----+
*/
//tagDF.filter($"level" === 5).show(10, truncate = false)
/*
TODO: 1、计算每个用户RFM值
按照用户memberid分组,然后进行聚合函数聚合统计
R:消费周期,finishtime
日期时间函数:current_timestamp、from_unixtimestamp、datediff
F: 消费次数 ordersn
count
M:消费金额 orderamount
sum
*/
val rfmDF: DataFrame = businessDF
// a. 按照memberid分组,对每个用户的订单数据句话操作
.groupBy($"memberid")
.agg(
max($"finishtime").as("max_finishtime"), //
count($"ordersn").as("frequency"), //
sum(
$"orderamount".cast(DataTypes.createDecimalType(10, 2))
).as("monetary") //
)
// 计算R值
.select(
$"memberid".as("userId"), //
// 计算R值:消费周期
datediff(
current_timestamp(), from_unixtime($"max_finishtime")
).as("recency"), //
$"frequency", //
$"monetary"
)
//rfmDF.printSchema()
//rfmDF.show(10, truncate = false)
/*
TODO: 2、按照规则给RFM进行打分(RFM_SCORE)
R: 1-3天=5分,4-6天=4分,7-9天=3分,10-15天=2分,大于16天=1分
F: ≥200=5分,150-199=4分,100-149=3分,50-99=2分,1-49=1分
M: ≥20w=5分,10-19w=4分,5-9w=3分,1-4w=2分,<1w=1分
使用CASE WHEN .. WHEN... ELSE .... END
*/
// R 打分条件表达式
val rWhen = when(col("recency").between(1, 3), 5.0) //
.when(col("recency").between(4, 6), 4.0) //
.when(col("recency").between(7, 9), 3.0) //
.when(col("recency").between(10, 15), 2.0) //
.when(col("recency").geq(16), 1.0) //
// F 打分条件表达式
val fWhen = when(col("frequency").between(1, 49), 1.0) //
.when(col("frequency").between(50, 99), 2.0) //
.when(col("frequency").between(100, 149), 3.0) //
.when(col("frequency").between(150, 199), 4.0) //
.when(col("frequency").geq(200), 5.0) //
// M 打分条件表达式
val mWhen = when(col("monetary").lt(10000), 1.0) //
.when(col("monetary").between(10000, 49999), 2.0) //
.when(col("monetary").between(50000, 99999), 3.0) //
.when(col("monetary").between(100000, 199999), 4.0) //
.when(col("monetary").geq(200000), 5.0) //
val rfmScoreDF: DataFrame = rfmDF.select(
$"userId", //
rWhen.as("r_score"), //
fWhen.as("f_score"), //
mWhen.as("m_score") //
)
//rfmScoreDF.printSchema()
//rfmScoreDF.show(50, truncate = false)
/*
TODO: 3、使用RFM_SCORE进行聚类,对用户进行分组
KMeans算法,其中K=5
*/
// 3.1 组合R\F\M列为特征值features
val assembler: VectorAssembler = new VectorAssembler()
.setInputCols(Array("r_score", "f_score", "m_score"))
.setOutputCol("raw_features")
val rawFeaturesDF: DataFrame = assembler.transform(rfmScoreDF)
// 将训练数据缓存
rawFeaturesDF.persist(StorageLevel.MEMORY_AND_DISK)
// TODO: =============== 对特征数据进行处理:最大最小归一化 ================
val scalerModel: MinMaxScalerModel = new MinMaxScaler()
.setInputCol("raw_features")
.setOutputCol("features")
.fit(rawFeaturesDF)
val featuresDF: DataFrame = scalerModel.transform(rawFeaturesDF)
//featuresDF.printSchema()
//featuresDF.show(10, truncate = false)
// 3.2 使用KMeans算法聚类,训练模型
/*
val kMeansModel: KMeansModel = new KMeans()
.setFeaturesCol("features")
.setPredictionCol("prediction") // 由于K=5,所以预测值prediction范围:0,1,2,3,4
// K值设置,类簇个数
.setK(5)
.setMaxIter(20)
.setInitMode("k-means||")
// 训练模型
.fit(featuresDF)
// WSSSE = 0.9977375565642177
println(s"WSSSE = ${kMeansModel.computeCost(featuresDF)}")
*/
//val kMeansModel: KMeansModel = trainModel(featuresDF)
// 调整超参数,获取最佳模型
//val kMeansModel: KMeansModel = trainBestModel(featuresDF)
// 加载模型
val kMeansModel: KMeansModel = loadModel(featuresDF)
// 3.3. 使用模型预测
val predictionDF: DataFrame = kMeansModel.transform(featuresDF)
/*
root
|-- userId: string (nullable = true)
|-- r_score: double (nullable = true)
|-- f_score: double (nullable = true)
|-- m_score: double (nullable = true)
|-- features: vector (nullable = true)
|-- prediction: integer (nullable = true)
*/
//predictionDF.printSchema()
//predictionDF.show(50, truncate = false)
// 3.4 获取类簇中心点
val centerIndexArray: Array[((Int, Double), Int)] = kMeansModel
.clusterCenters
// 返回值类型:: Array[(linalg.Vector, Int)]
.zipWithIndex // (vector1, 0), (vector2, 1), ....
// TODO: 对每个类簇向量进行累加和:R + F + M
.map{case(clusterVector, clusterIndex) =>
// rfm表示将R + F + M之和,越大表示客户价值越高
val rfm: Double = clusterVector.toArray.sum
clusterIndex -> rfm
}
// 按照rfm值进行降序排序
.sortBy(tuple => - tuple._2)
// 再次进行拉链操作
.zipWithIndex
//centerIndexArray.foreach(println)
// TODO: 4. 打标签
// 4.1 获取属性标签规则rule和名称tagName,放在Map集合中
val rulesMap: Map[String, String] = TagTools.convertMap(tagDF)
//rulesMap.foreach(println)
// 4.2 聚类类簇关联属性标签数据rule,对应聚类类簇与标签tagName
val indexTagMap: Map[Int, String] = centerIndexArray
.map{case((centerIndex, _), index) =>
val tagName = rulesMap(index.toString)
(centerIndex, tagName)
}
.toMap
//indexTagMap.foreach(println)
// 4.3 使用KMeansModel预测值prediction打标签
// a. 将索引标签Map集合 广播变量广播出去
val indexTagMapBroadcast = session.sparkContext.broadcast(indexTagMap)
// b. 自定义UDF函数,传递预测值prediction,返回标签名称tagName
val index_to_tag: UserDefinedFunction = udf(
(clusterIndex: Int) => indexTagMapBroadcast.value(clusterIndex)
)
// c. 打标签
val modelDF: DataFrame = predictionDF.select(
$"userId", // 用户ID
index_to_tag($"prediction").as("rfm")
)
//modelDF.printSchema()
//modelDF.show(100, truncate = false)
// 返回画像标签数据
modelDF
}
/**
* 使用KMeans算法训练模型
* @param dataframe 数据集
* @return KMeansModel模型
*/
def trainModel(dataframe: DataFrame): KMeansModel = {
// 使用KMeans聚类算法模型训练
val kMeansModel: KMeansModel = new KMeans()
.setFeaturesCol("features")
.setPredictionCol("prediction")
.setK(5) // 设置列簇个数:5
.setMaxIter(20) // 设置最大迭代次数
.fit(dataframe)
println(s"WSSSE = ${kMeansModel.computeCost(dataframe)}")
// 返回
kMeansModel
}
/**
* TODO:调整KMeans算法超参数,获取最佳模型
* @param dataframe 数据集
* @return 最佳模型
*/
def trainBestModel(dataframe: DataFrame): KMeansModel = {
/*
针对KMeans聚类算法来说,超参数有哪些呢??
1. K值,采用肘部法则确定
但是对于RFM模型来说,K值确定,等于5
2. 最大迭代次数MaxIters
迭代训练模型最大次数,可以调整
*/
// TODO:模型调优方式二:调整算法超参数 -> MaxIter 最大迭代次数, 使用训练验证模式完成
// 1.设置超参数的值
val maxIters: Array[Int] = Array(10, 20, 50)
// 2.不同超参数的值,训练模型
val models: Array[(Double, KMeansModel, Int)] = maxIters.map{ maxIter =>
// a. 使用KMeans算法应用数据训练模式
val kMeans: KMeans = new KMeans()
.setFeaturesCol("features")
.setPredictionCol("prediction")
.setK(5) // 设置聚类的类簇个数
.setMaxIter(maxIter)
// b. 训练模式
val model: KMeansModel = kMeans.fit(dataframe)
// c. 模型评估指标WSSSE
val ssse = model.computeCost(dataframe)
// d. 返回三元组(评估指标, 模型, 超参数的值)
(ssse, model, maxIter)
}
models.foreach(println)
// 3.获取最佳模型
val (_, bestModel, _) = models.minBy(tuple => tuple._1)
// 4.返回最佳模型
bestModel
}
/**
* 从HDFS文件系统加载模型,当模型存在时,直接从路径加载;如果不存在,训练模型,并保存
* @param dataframe 数据集,包含字段features,类型为向量vector
* @return KMeansModel模型实例对象
*/
def loadModel(dataframe: DataFrame): KMeansModel = {
val modelPath: String = s"${ModelConfig.MODEL_BASE_PATH}/${this.getClass.getSimpleName.stripSuffix("$")}"
// 1. 判断模型是否存在:路径是否存在,如果存在,直接加载
val modelExists: Boolean = HdfsUtils.exists(
dataframe.sparkSession.sparkContext.hadoopConfiguration, //
modelPath
)
if(modelExists){
logWarning(s"================== 正在从<${modelPath}>加载模型 ==================")
// 直接加载,返回结款
KMeansModel.load(modelPath)
} else{
// 2. 如果模型不存在,首先训练模型,获取最佳模型,并保存,最后返回模型
// 2.1 训练获取最佳模型
logWarning(s"================== 正在从训练获取最佳模型 ==================")
val model: KMeansModel = trainBestModel(dataframe)
// 2.2 模型保存
logWarning(s"================== 正在保存模型至<${modelPath}> ==================")
model.save(modelPath)
// 2.3 返回最佳模型
model
}
}
}
object RfmTagModel{
def main(args: Array[String]): Unit = {
val tagModel = new RfmTagModel()
tagModel.executeModel(361L)
}
}
(叠甲:大部分资料来源于黑马程序员,这里只是做一些自己的认识、思路和理解,主要是为了分享经验,如果大家有不理解的部分可以私信我,也可以移步【黑马程序员_大数据实战之用户画像企业级项目】https://www.bilibili.com/video/BV1Mp4y1x7y7?p=201&vd_source=07930632bf702f026b5f12259522cb42,以上,大佬勿喷)