寫在前面: 博主是一名軟件工程系大數據應用開發專業大二的學生,暱稱來源於《愛麗絲夢遊仙境》中的Alice和自己的暱稱。作爲一名互聯網小白,
寫博客一方面是爲了記錄自己的學習歷程,一方面是希望能夠幫助到很多和自己一樣處於起步階段的萌新
。由於水平有限,博客中難免會有一些錯誤,有紕漏之處懇請各位大佬不吝賜教!個人小站:http://alices.ibilibili.xyz/ , 博客主頁:https://alice.blog.csdn.net/
儘管當前水平可能不及各位大佬,但我還是希望自己能夠做得更好,因爲一天的生活就是一生的縮影
。我希望在最美的年華,做最好的自己
!
上一篇博客,已經爲大家介紹了基於RFM(用戶價值模型)的挖掘型標籤開發過程(👉大數據【企業級360°全方位用戶畫像】基於RFM模型的挖掘型標籤開發),本篇博客,我們來學習基於RFE(用戶活躍度模型)的挖掘型標籤開發。
RFE模型引入
在正式開始實現需求之前,肯定要給各位朋友們解釋下。
RFE模型可以說是RFM模型的變體 。RFE模型基於用戶的普通行爲(非轉化或交易行爲)產生,它跟RFM類似都是使用三個維度做價值評估。
RFE詳解
RFE 模型是根據會員最近一次訪問時間R( Recency)、訪問頻率 F(Frequency)和頁面互動度 E(Engagements)計算得出的RFE得分。 其中:
- 最近一次訪問時間 R( Recency): 會員最近一次訪問或到達網站的時間。
- 訪問頻率 F( Frequency):用戶在特定時間週期內訪問或到達的頻率。
- 頁面互動度 E( Engagements):互動度的定義可以根據不同企業或行業的交互情況而定,例如可以定義爲頁面 瀏覽時間、瀏覽商品數量、視頻播放數量、點贊數量、轉發數量等 。
在RFE模型中,由於不要求用戶發生交易,因此可以做未發生登錄、 註冊等匿名用戶的行爲價值分析, 也可以做實名用戶分析。該模型常用來做用戶活躍分羣或價值區分, 也可用於內容型(例如論壇、新聞、資訊等)企業的會員分析。
RFM和 RFE模型的實現思路相同, 僅僅是計算指標發生變化。 對於RFE的數據來源, 可以從企業自己監控的用戶行爲日誌獲取,也可以從第三方網站分析工具獲得。
基於RFE模型的實踐應用
在得到用戶的RFE得分之後, 跟 RFM 類似也可以有兩種應用思路:
1:基於三個維度值做用戶羣體劃分和解讀,對用戶的活躍度做分析。 RFE得分爲 313 的會員說明其訪問頻率低, 但是每次訪問時的交互都非常不錯, 此時重點要做用戶回訪頻率的提升,例如通過活動邀請、 精準廣告投放、會員活動推薦等提升回訪頻率。
2:基於RFE的彙總得分評估所有會員的活躍度價值,並可以做活躍度排名; 同時,該得分還可以作爲輸入維 度跟其他維度一起作爲其他數據分析和挖掘模型的輸入變量,爲分析建模提供基礎。
比如:
- 6忠誠 (1天內訪問2次及以上,每次訪問頁面不重複)
- 5活躍 (2天內訪問至少1次)
- 4迴流 (3天內訪問至少1次)
- 3新增 (註冊並訪問)
- 2不活躍 (7天內未訪問)
- 1流失 (7天以上無訪問)
具體代碼實現
大家看到這裏,應該都發現了,RFE模型和之前我們介紹過的RFM模型非常類似,只不過一個是用戶價值模型,一個是用戶活躍度模型。
因爲同爲挖掘型標籤的開發,所以流程上大部分的內容都是相似的,下面博主就不詳細分步驟介紹了😅,關於代碼中有任何的疑惑,可以私信聯繫我喲~
import com.czxy.base.BaseModel
import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.{Column, DataFrame, SparkSession, functions}
import scala.collection.immutable
/*
* @Author: Alice菌
* @Date: 2020/6/24 08:25
* @Description:
用戶的活躍度標籤開發
*/
object RFEModel extends BaseModel{
override def setAppName: String = "RFEModel"
override def setFourTagId: String = "176"
override def getNewTag(spark: SparkSession, fiveTagDF: DataFrame, hbaseDF: DataFrame): DataFrame = {
// 展示MySQL的五級標籤數據
fiveTagDF.show()
//+---+----+
//| id|rule|
//+---+----+
//|177| 1|
//|178| 2|
//|179| 3|
//|180| 4|
//+---+----+
// 展示HBase中的數據
hbaseDF.show(false)
//+--------------+--------------------+-------------------+
//|global_user_id| loc_url| log_time|
//+--------------+--------------------+-------------------+
//| 424|http://m.eshop.co...|2019-08-13 03:03:55|
//| 619|http://m.eshop.co...|2019-07-29 15:07:41|
//| 898|http://m.eshop.co...|2019-08-14 09:23:44|
//| 642|http://www.eshop....|2019-08-11 03:20:17|
//RFE三個單詞
//最近一次訪問時間R
val recencyStr: String = "recency"
//訪問頻率 F
val frequencyStr: String = "frequency"
//頁面互動度 E
val engagementsStr: String = "engagements"
// 特徵單詞
val featureStr: String = "feature" // 向量
val predictStr: String = "predict" // 分類
// 計算業務數據
// R(會員最後一次訪問或到達網站的時間)
// F(用戶在特定時間週期內訪問或到達的頻率)
// E(頁面的互動度,注意:一個頁面訪問10次,算1次)
// 引入隱式轉換
import spark.implicits._
//引入java 和scala相互轉換
import scala.collection.JavaConverters._
//引入sparkSQL的內置函數
import org.apache.spark.sql.functions._
/* 分別計算 R F E 的值 */
// R 計算 最後一次瀏覽距今的時間
val getRecency: Column = datediff(current_timestamp(),max("log_time")) as recencyStr
// F 計算 頁面訪問次數(一個頁面訪問多次,就算多次)
val getFrequency: Column = count("loc_url") as frequencyStr
// E 計算 頁面互動度(一個頁面訪問多次,只計算一次)
val getEngagements: Column = countDistinct("loc_url") as engagementsStr
val getRFEDF: DataFrame = hbaseDF.groupBy("global_user_id")
.agg(getRecency, getFrequency, getEngagements)
getRFEDF.show(false)
//+--------------+-------+---------+-----------+
//|global_user_id|recency|frequency|engagements|
//+--------------+-------+---------+-----------+
//|296 |312 |380 |227 |
//|467 |312 |405 |267 |
//|675 |312 |370 |240 |
//|691 |312 |387 |244 |
//現有的RFM 量綱不統一,需要執行歸一化 爲RFM打分
//計算R的分數
val getRecencyScore: Column =
when(col(recencyStr).between(0,15), 5)
.when(col(recencyStr).between(16,30), 4)
.when(col(recencyStr).between(31,45), 3)
.when(col(recencyStr).between(46,60), 2)
.when(col(recencyStr).gt(60), 1)
.as(recencyStr)
//計算F的分數
val getFrequencyScore: Column =
when(col(frequencyStr).geq(400), 5)
.when(col(frequencyStr).between(300,399), 4)
.when(col(frequencyStr).between(200,299), 3)
.when(col(frequencyStr).between(100,199), 2)
.when(col(frequencyStr).leq(99), 1)
.as(frequencyStr)
//計算E的分數
val getEngagementScore: Column =
when(col(engagementsStr).geq(250), 5)
.when(col(engagementsStr).between(200,249), 4)
.when(col(engagementsStr).between(150,199), 3)
.when(col(engagementsStr).between(50,149), 2)
.when(col(engagementsStr).leq(49), 1)
.as(engagementsStr)
// 計算 RFE 的分數
val getRFEScoreDF: DataFrame = getRFEDF.select('global_user_id ,getRecencyScore,getFrequencyScore,getEngagementScore)
getRFEScoreDF.show(false)
//+--------------+-------+---------+-----------+
//|global_user_id|recency|frequency|engagements|
//+--------------+-------+---------+-----------+
//|296 |1 |4 |4 |
//|467 |1 |5 |5 |
//|675 |1 |4 |4 |
//|691 |1 |4 |4 |
//|829 |1 |5 |5 |
// 爲了方便計算,我們將數據轉換成向量
val RFEFeature: DataFrame = new VectorAssembler()
.setInputCols(Array(recencyStr, frequencyStr, engagementsStr))
.setOutputCol(featureStr)
.transform(getRFEScoreDF)
RFEFeature.show()
//+--------------+-------+---------+-----------+-------------+
//|global_user_id|recency|frequency|engagements| feature|
//+--------------+-------+---------+-----------+-------------+
//| 296| 1| 4| 4|[1.0,4.0,4.0]|
//| 467| 1| 5| 5|[1.0,5.0,5.0]|
//| 675| 1| 4| 4|[1.0,4.0,4.0]|
//| 691| 1| 4| 4|[1.0,4.0,4.0]|
// 利用KMeans算法,進行數據的分類
val KMeansModel: KMeansModel = new KMeans()
.setK(4) // 設置4類
.setMaxIter(5) // 迭代計算5次
.setFeaturesCol(featureStr) // 設置特徵數據
.setPredictionCol(predictStr) // 計算完畢後的標籤結果
.fit(RFEFeature)
// 將其轉換成DF
val KMeansModelDF: DataFrame = KMeansModel.transform(RFEFeature)
KMeansModelDF.show()
//+--------------+-------+---------+-----------+-------------+-------+
//|global_user_id|recency|frequency|engagements| feature|predict|
//+--------------+-------+---------+-----------+-------------+-------+
//| 296| 1| 4| 4|[1.0,4.0,4.0]| 1|
//| 467| 1| 5| 5|[1.0,5.0,5.0]| 0|
//| 675| 1| 4| 4|[1.0,4.0,4.0]| 1|
//| 691| 1| 4| 4|[1.0,4.0,4.0]| 1|
// 計算用戶的價值
val clusterCentersSum: immutable.IndexedSeq[(Int, Double)] = for(i <- KMeansModel.clusterCenters.indices) yield (i,KMeansModel.clusterCenters(i).toArray.sum)
val clusterCentersSumSort: immutable.IndexedSeq[(Int, Double)] = clusterCentersSum.sortBy(_._2).reverse
clusterCentersSumSort.foreach(println)
//(0,11.0)
//(3,10.0)
//(2,10.0)
//(1,9.0)
// 獲取到每種分類以及其對應的索引
val clusterCenterIndex: immutable.IndexedSeq[(Int, Int)] = for(a <- clusterCentersSumSort.indices) yield (clusterCentersSumSort(a)._1,a)
clusterCenterIndex.foreach(println)
//(0,0)
//(3,1)
//(2,2)
//(1,3)
// 類別的價值從高到低,角標依次展示
// 將其轉換成DF
val clusterCenterIndexDF: DataFrame = clusterCenterIndex.toDF(predictStr,"index")
clusterCenterIndexDF.show()
//+-------+-----+
//|predict|index|
//+-------+-----+
//| 0| 0|
//| 3| 1|
//| 2| 2|
//| 1| 3|
//+-------+-----+
// 開始join
val JoinDF: DataFrame = fiveTagDF.join(clusterCenterIndexDF,fiveTagDF.col("rule") === clusterCenterIndexDF.col("index"))
JoinDF.show()
//+---+----+-------+-----+
//| id|rule|predict|index|
//+---+----+-------+-----+
//|177| 0| 0| 0|
//|178| 1| 3| 1|
//|179| 2| 2| 2|
//|180| 3| 1| 3|
//+---+----+-------+-----+
val JoinDFS: DataFrame = JoinDF.select(predictStr,"id")
//fiveTageList
val fiveTageMap: Map[String, String] = JoinDFS.as[(String,String)].collect().toMap
//7、獲得數據標籤(udf)
// 需要自定義UDF函數
val getRFMTags: UserDefinedFunction = udf((featureOut: String) => {
fiveTageMap.get(featureOut)
})
val CustomerValueTag: DataFrame = KMeansModelDF.select('global_user_id .as("userId"),getRFMTags('predict).as("tagsId"))
//CustomerValueTag.show(false)
//|userId|tagsId|
//+------+------+
//|296 |180 |
//|467 |177 |
//|675 |180 |
//|691 |180 |
//|829 |177 |
//|125 |180 |
//|451 |180 |
//|800 |180 |
//|853 |179 |
CustomerValueTag
}
def main(args: Array[String]): Unit = {
exec()
}
}
知識拓展
這裏問大家一個問題,我們在調用K-Means算法進行聚類計算的時候,需要先設定一個K值,那麼這個K值具體是多少是如何得到的呢?
就拿本題來說,如果你覺得我們在標籤系統中人爲地劃分成了四類,所以在進行聚類計算的時候,就把K設置成了4的話,那就理解錯了。
大多數情況下,我們是無法預先確定K值的大小,所以有認真看過之前介紹機器學習常見面試題的朋友(👉關於機器學習的面試題,你又瞭解多少呢?),肯定對於肘部法則有一定的印象。
手肘法的核心指標是
集合內誤差平方和:Within Set Sum of Squared Error, WSSSE
或者叫SSE(sum of the squared errors,誤差平方和),公式爲
本次所開發的標籤,爲什麼K = 4 呢,接下倆,讓我們用代碼來講道理!
我們在原有代碼的基礎上,添加上這幾行代碼,然後運行程序,等待結果。
var SSE: String =""
//4 數據分類(不知道哪個類的活躍度高和低)
for(k<-2 to 9){
val model: KMeansModel = new KMeans()
.setK(k)
.setMaxIter(5)
.setSeed(10)
.setFeaturesCol(featureStr)
.setPredictionCol(predictStr)
.fit(RFEFeature)
SSE=k+"-"+model.computeCost(RFEFeature)
println(SSE)
}
友情提示:運行的過程可能非常漫長。爲啥勒,畢竟一次KMeans 計算就夠久的了,而這個循環要計算8次…
不出所料,經過了N秒之後的等待,終於有了下面的結果。
2-185.03108672935983
3-23.668965517242555
4-0.0
5-0.0
6-0.0
7-0.0
8-0.0
9-0.0
其實看到這裏,已經很清楚拐點就是在K = 4 的時候了。但是如果你跟我說莫得辦法,看不出來,菌哥還是有妙招!!!
爲了讓數據更有畫面感,菌哥打開了很久沒上號的Echars😂,爲大家帶來了下面的簡單小圖。
相信看到這裏,大家一定沒有疑問了吧~
小結
如果以上過程中出現了任何的紕漏錯誤,煩請大佬們指正😅
受益的朋友或對大數據技術感興趣的夥伴記得點贊關注支持一波🙏
希望我們都能在學習的道路上越走越遠😉