寫在前面: 博主是一名軟件工程系大數據應用開發專業大二的學生,暱稱來源於《愛麗絲夢遊仙境》中的Alice和自己的暱稱。作爲一名互聯網小白,
寫博客一方面是爲了記錄自己的學習歷程,一方面是希望能夠幫助到很多和自己一樣處於起步階段的萌新
。由於水平有限,博客中難免會有一些錯誤,有紕漏之處懇請各位大佬不吝賜教!個人小站:http://alices.ibilibili.xyz/ , 博客主頁:https://alice.blog.csdn.net/
儘管當前水平可能不及各位大佬,但我還是希望自己能夠做得更好,因爲一天的生活就是一生的縮影
。我希望在最美的年華,做最好的自己
!
在初次介紹用戶畫像項目的時候我們談到過,按照實現方式,標籤可以分爲匹配型,統計型和挖掘型。之前已經爲大家介紹了關於用戶畫像項目中匹配型標籤的開發流程。
具體請見👇
大數據【企業級360°全方位用戶畫像】匹配型標籤累計開發
本篇博客,我們來談談統計型標籤的開發~
統計型標籤是需要使用聚合函數計算後得到的標籤,比如最近3個月的退單率,用戶最常用的支付方式等等。
本篇博客,我將通過完整開發一個標籤的流程,爲大家做詳細介紹。
例如我們現在需要開發一個統計型標籤,計算用戶的客單價。
客單價就是一個客戶所有的訂單金額/訂單數量,簡單說就是我們需要統計每個用戶每筆訂單所花的錢。
現在目標清楚了,我們需要先到web頁面,創建對應的四級和五級標籤。
我們可以看到類似的效果
創建完畢之後,我們可以在數據庫中看到對應的數據。
接着我們就要開始寫代碼了。
首先創建一個object,根據需要開發的標籤名字,我們將其命名爲:AvgTransactionTag
1、創建SparkSession
因爲我們彙總計算需要使用到SparkSQL,所以我們需要先創建SparkSQL的運行環境。
爲了方便我們後期運行時查看控制檯,我們可以設置一下日誌級別。
val spark: SparkSession = SparkSession.builder().appName("AgeTag").master("local[*]").getOrCreate()
// 設置日誌級別
spark.sparkContext.setLogLevel("WARN")
2、連接MySQL
我們這裏採用Spark通過jdbc的方式連接MySQL。
// 設置Spark連接MySQL所需要的字段
var url: String ="jdbc:mysql://bd001:3306/tags_new2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&user=root&password=123456"
var table: String ="tbl_basic_tag" //mysql數據表的表名
var properties:Properties = new Properties
// 連接MySQL
val mysqlConn: DataFrame = spark.read.jdbc(url,table,properties)
3、讀取MySQL數據庫四級標籤的數據
因爲後續可能需要對讀取的數據做一些形式上的轉換,所以我們這裏先引入了隱式轉換和SparkSQL的內置函數,然後根據MySQL的連接對象,讀取了四級標籤的數據,並對其做了一定的處理。
// 引入隱式轉換
import spark.implicits._
//引入sparkSQL的內置函數
import org.apache.spark.sql.functions._
//讀取MySQL數據庫的四級標籤
val fourTagsDS: Dataset[Row] = mysqlConn.select("rule").where("id=137")
// 對四級標籤數據做處理
val KVMap: Map[String, String] = fourTagsDS.map(row => {
// 獲取到rule值
val RuleValue: String = row.getAs("rule").toString
// 使用"##"對數據進行切分
val KVMaps: Array[(String, String)] = RuleValue.split("##").map(kv => {
val arr: Array[String] = kv.split("=")
(arr(0), arr(1))
})
KVMaps
}).collectAsList().get(0).toMap
// 將Map 轉換成HBaseMeta的樣例類
val hbaseMeta: HBaseMeta = toHBaseMeta(KVMap)
因爲涉及到了樣例類的調用,所以我們也提前寫好了樣例類。
//將mysql中的四級標籤的rule 封裝成HBaseMeta
//方便後續使用的時候方便調用
def toHBaseMeta(KVMap: Map[String, String]): HBaseMeta = {
//開始封裝
HBaseMeta(KVMap.getOrElse("inType",""),
KVMap.getOrElse(HBaseMeta.ZKHOSTS,""),
KVMap.getOrElse(HBaseMeta.ZKPORT,""),
KVMap.getOrElse(HBaseMeta.HBASETABLE,""),
KVMap.getOrElse(HBaseMeta.FAMILY,""),
KVMap.getOrElse(HBaseMeta.SELECTFIELDS,""),
KVMap.getOrElse(HBaseMeta.ROWKEY,"")
)
}
4、讀取MySQL數據庫五級標籤的數據
上一步我們已經讀取完了四級標籤,這一步我們需要讀取MySQL中五級標籤的數據,也就是標籤值的數據。同樣,再讀取完之後,需要對數據進行處理。因爲我們的標籤值是一個範圍的數據,例如1-999
,我們需要將這個範圍的開始和結束的數字獲取到,然後將其添加爲DataFrame的Schema,方便我們後期對其與Hbase數據進行關聯查詢的時候獲取到區間起始數據。
//4. 讀取mysql數據庫的五級標籤
val fiveTagsDS: Dataset[Row] = mysqlConn.select("id","rule").where("pid=137")
val fiveTagDF: DataFrame = fiveTagsDS.map(row => {
// row 是一條數據
// 獲取出id 和 rule
val id: Int = row.getAs("id").toString.toInt
val rule: String = row.getAs("rule").toString
//133 1-999
//134 1000-2999
var start: String = ""
var end: String = ""
val arr: Array[String] = rule.split("-")
if (arr != null && arr.length == 2) {
start = arr(0)
end = arr(1)
}
// 封裝
(id, start, end)
}).toDF("id", "start", "end")
fiveTagDF.show()
//+---+-----+----+
//| id|start| end|
//+---+-----+----+
//|138| 1| 999|
//|139| 1000|2999|
//|140| 3000|4999|
//|141| 5000|9999|
//+---+-----+----+
5、讀取Hbase中的標籤值數據
到了這一步,開始逐漸顯得與匹配型標籤的操作不一樣了。我們在讀取完了Hbase中的數據之後,需要展開分析。
因爲一個用戶可能會有多條數據 ,也就會有多個支付金額。我們需要將數據按照用戶id進行分組,然後獲取到金額總數和訂單總數,求餘就是客單價。
// 5. 讀取hbase中的數據,這裏將hbase作爲數據源進行讀取
val hbaseDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource")
// hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是兩種不同的寫法
.option("zkHosts",hbaseMeta.zkHosts)
.option(HBaseMeta.ZKPORT, hbaseMeta.zkPort)
.option(HBaseMeta.HBASETABLE, hbaseMeta.hbaseTable)
.option(HBaseMeta.FAMILY, hbaseMeta.family)
.option(HBaseMeta.SELECTFIELDS, hbaseMeta.selectFields)
.load()
hbaseDatas.show(5)
//+--------+-----------+
//|memberId|orderAmount|
//+--------+-----------+
//|13823431| 2479.45|
//| 4035167| 2449.00|
//| 4035291| 1099.42|
//| 4035041| 1999.00|
//|13823285| 2488.00|
//+--------+-----------+
// 因爲一個用戶可能會有多條數據 ,也就會有多個支付金額
// 我們需要將數據按照用戶id進行分組,然後獲取到金額總數和訂單總數,求餘就是客單價
val userFirst: DataFrame = hbaseDatas.groupBy("memberId").agg(sum("orderAmount").cast("Int").as("sumAmount"),count("orderAmount").as("countAmount"))
userFirst.show(5)
//+---------+---------+-----------+
//| memberId|sumAmount|countAmount|
//+---------+---------+-----------+
//| 4033473| 251930| 142|
//| 13822725| 179298| 116|
//| 13823681| 169746| 108|
//|138230919| 240061| 125|
//| 13823083| 233524| 132|
//+---------+---------+-----------+
// val frame: DataFrame = userFirst.select($"sumAmount" / $"countAmount")
val userAvgAmount: DataFrame = userFirst.select('memberId,('sumAmount / 'countAmount).cast("Int").as("AvgAmount"))
userAvgAmount.show(5)
//+---------+-------------------------+
//| memberId|(sumAmount / countAmount)|
//+---------+-------------------------+
//| 4033473| 1774.1549295774648|
//| 13822725| 1545.6724137931035|
//| 13823681| 1571.7222222222222|
//|138230919| 1920.488|
//| 13823083| 1769.121212121212|
//+---------+-------------------------+
6、數據關聯
我們在第四步和第五步中分別對MySQL中的五級標籤數據和Hbase中的標籤值數據進行了處理。在第六步中,我們理應對其進行關聯。因爲客單價的標籤值時一個範圍的數據,所以我們這裏使用到了Between,想要獲取到區間範圍的起始值只需要用五級標籤返回的DataFrame對象fiveTagDF.col
的形式即可獲取到,是不是很方便呢?
// 將 Hbase的數據與 五級標籤的數據進行 關聯
val dataJoin: DataFrame = userAvgAmount.join(fiveTagDF, userAvgAmount.col("AvgAmount")
.between(fiveTagDF.col("start"), fiveTagDF.col("end")))
dataJoin.show()
// 選出我們最終需要的字段,返回需要和Hbase中舊數據合併的新數據
val AvgTransactionNewTags: DataFrame = dataJoin.select('memberId.as("userId"),'id.as("tagsId"))
AvgTransactionNewTags.show(5)
7、解決數據覆蓋的問題
在獲取到了新數據之後,我們需要將Hbase結果表中的“舊數據”讀取出來,然後,與之進行合併。所以我們需要定義一個udf,用於解決標籤值重複或者數據合併的問題。
/* 定義一個udf,用於處理舊數據和新數據中的數據合併的問題 */
val getAllTages: UserDefinedFunction = udf((genderOldDatas: String, jobNewTags: String) => {
if (genderOldDatas == "") {
jobNewTags
} else if (jobNewTags == "") {
genderOldDatas
} else if (genderOldDatas == "" && jobNewTags == "") {
""
} else {
val alltages: String = genderOldDatas + "," + jobNewTags //可能會出現 83,94,94
// 對重複數據去重
alltages.split(",").distinct // 83 94
// 使用逗號分隔,返回字符串類型
.mkString(",") // 83,84
}
})
// 讀取hbase中的歷史數據
val genderOldDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource")
// hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是兩種不同的寫法
.option("zkHosts","192.168.10.20")
.option(HBaseMeta.ZKPORT, "2181")
.option(HBaseMeta.HBASETABLE, "test")
.option(HBaseMeta.FAMILY, "detail")
.option(HBaseMeta.SELECTFIELDS, "userId,tagsId")
.load()
// 新表和舊錶進行join
val joinTags: DataFrame = genderOldDatas.join(AvgTransactionNewTags, genderOldDatas("userId") === AvgTransactionNewTags("userId"))
joinTags.show()
val allTags: DataFrame = joinTags.select(
// 處理第一個字段
when((genderOldDatas.col("userId").isNotNull), (genderOldDatas.col("userId")))
.when((AvgTransactionNewTags.col("userId").isNotNull), (AvgTransactionNewTags.col("userId")))
.as("userId"),
getAllTages(genderOldDatas.col("tagsId"), AvgTransactionNewTags.col("tagsId")).as("tagsId")
)
// 新數據與舊數據彙總之後的數據
allTags.show(10)
8、數據寫入
我們在合併完了數據之後,最後將其寫入到Hbase中即可。
// 將最終結果進行覆蓋
allTags.write.format("com.czxy.tools.HBaseDataSource")
.option("zkHosts", hbaseMeta.zkHosts)
.option(HBaseMeta.ZKPORT, hbaseMeta.zkPort)
.option(HBaseMeta.HBASETABLE,"test")
.option(HBaseMeta.FAMILY, "detail")
.option(HBaseMeta.SELECTFIELDS, "userId,tagsId")
.option("repartition",1)
.save()
完整代碼
import java.util.Properties
import com.czxy.bean.HBaseMeta
import org.apache.spark.sql.expressions.UserDefinedFunction
import org.apache.spark.sql.{DataFrame, Dataset, Row, SparkSession}
/*
* @Author: Alice菌
* @Date: 2020/6/12 21:10
* @Description:
*
* 基於用戶的客單價統計標籤分析
*/
object AvgTransactionTag {
def main(args: Array[String]): Unit = {
val spark: SparkSession = SparkSession.builder().appName("AgeTag").master("local[*]").getOrCreate()
// 設置日誌級別
spark.sparkContext.setLogLevel("WARN")
// 設置Spark連接MySQL所需要的字段
var url: String ="jdbc:mysql://bd001:3306/tags_new2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&user=root&password=123456"
var table: String ="tbl_basic_tag" //mysql數據表的表名
var properties:Properties = new Properties
// 連接MySQL
val mysqlConn: DataFrame = spark.read.jdbc(url,table,properties)
// 引入隱式轉換
import spark.implicits._
//引入sparkSQL的內置函數
import org.apache.spark.sql.functions._
// 讀取MySQL數據庫的四級標籤
val fourTagsDS: Dataset[Row] = mysqlConn.select("rule").where("id=137")
val KVMap: Map[String, String] = fourTagsDS.map(row => {
// 獲取到rule值
val RuleValue: String = row.getAs("rule").toString
// 使用"##"對數據進行切分
val KVMaps: Array[(String, String)] = RuleValue.split("##").map(kv => {
val arr: Array[String] = kv.split("=")
(arr(0), arr(1))
})
KVMaps
}).collectAsList().get(0).toMap
println(KVMap)
// 將Map 轉換成HBaseMeta的樣例類
val hbaseMeta: HBaseMeta = toHBaseMeta(KVMap)
//4. 讀取mysql數據庫的五級標籤
val fiveTagsDS: Dataset[Row] = mysqlConn.select("id","rule").where("pid=137")
val fiveTagDF: DataFrame = fiveTagsDS.map(row => {
// row 是一條數據
// 獲取出id 和 rule
val id: Int = row.getAs("id").toString.toInt
val rule: String = row.getAs("rule").toString
//133 1-999
//134 1000-2999
var start: String = ""
var end: String = ""
val arr: Array[String] = rule.split("-")
if (arr != null && arr.length == 2) {
start = arr(0)
end = arr(1)
}
// 封裝
(id, start, end)
}).toDF("id", "start", "end")
fiveTagDF.show()
//+---+-----+----+
//| id|start| end|
//+---+-----+----+
//|138| 1| 999|
//|139| 1000|2999|
//|140| 3000|4999|
//|141| 5000|9999|
//+---+-----+----+
// 5. 讀取hbase中的數據,這裏將hbase作爲數據源進行讀取
val hbaseDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource")
// hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是兩種不同的寫法
.option("zkHosts",hbaseMeta.zkHosts)
.option(HBaseMeta.ZKPORT, hbaseMeta.zkPort)
.option(HBaseMeta.HBASETABLE, hbaseMeta.hbaseTable)
.option(HBaseMeta.FAMILY, hbaseMeta.family)
.option(HBaseMeta.SELECTFIELDS, hbaseMeta.selectFields)
.load()
hbaseDatas.show(5)
//+--------+-----------+
//|memberId|orderAmount|
//+--------+-----------+
//|13823431| 2479.45|
//| 4035167| 2449.00|
//| 4035291| 1099.42|
//| 4035041| 1999.00|
//|13823285| 2488.00|
//+--------+-----------+
// 因爲一個用戶可能會有多條數據 ,也就會有多個支付金額
// 我們需要將數據按照用戶id進行分組,然後獲取到金額總數和訂單總數,求餘就是客單價
val userFirst: DataFrame = hbaseDatas.groupBy("memberId").agg(sum("orderAmount").cast("Int").as("sumAmount"),count("orderAmount").as("countAmount"))
userFirst.show(5)
//+---------+---------+-----------+
//| memberId|sumAmount|countAmount|
//+---------+---------+-----------+
//| 4033473| 251930| 142|
//| 13822725| 179298| 116|
//| 13823681| 169746| 108|
//|138230919| 240061| 125|
//| 13823083| 233524| 132|
//+---------+---------+-----------+
// val frame: DataFrame = userFirst.select($"sumAmount" / $"countAmount")
val userAvgAmount: DataFrame = userFirst.select('memberId,('sumAmount / 'countAmount).cast("Int").as("AvgAmount"))
userAvgAmount.show(5)
//+---------+-------------------------+
//| memberId|(sumAmount / countAmount)|
//+---------+-------------------------+
//| 4033473| 1774.1549295774648|
//| 13822725| 1545.6724137931035|
//| 13823681| 1571.7222222222222|
//|138230919| 1920.488|
//| 13823083| 1769.121212121212|
//+---------+-------------------------+
// 將 Hbase的數據與 五級標籤的數據進行 關聯
val dataJoin: DataFrame = userAvgAmount.join(fiveTagDF, userAvgAmount.col("AvgAmount")
.between(fiveTagDF.col("start"), fiveTagDF.col("end")))
dataJoin.show()
println("---------------------------------------------")
// 選出我們最終需要的字段,返回需要和Hbase中舊數據合併的新數據
val AvgTransactionNewTags: DataFrame = dataJoin.select('memberId.as("userId"),'id.as("tagsId"))
AvgTransactionNewTags.show(5)
// 7、解決數據覆蓋的問題
// 讀取test,追加標籤後覆蓋寫入
// 標籤去重
/* 定義一個udf,用於處理舊數據和新數據中的數據合併的問題 */
val getAllTages: UserDefinedFunction = udf((genderOldDatas: String, jobNewTags: String) => {
if (genderOldDatas == "") {
jobNewTags
} else if (jobNewTags == "") {
genderOldDatas
} else if (genderOldDatas == "" && jobNewTags == "") {
""
} else {
val alltages: String = genderOldDatas + "," + jobNewTags //可能會出現 83,94,94
// 對重複數據去重
alltages.split(",").distinct // 83 94
// 使用逗號分隔,返回字符串類型
.mkString(",") // 83,84
}
})
// 讀取hbase中的歷史數據
val genderOldDatas: DataFrame = spark.read.format("com.czxy.tools.HBaseDataSource")
// hbaseMeta.zkHosts 就是 192.168.10.20 和 下面是兩種不同的寫法
.option("zkHosts","192.168.10.20")
.option(HBaseMeta.ZKPORT, "2181")
.option(HBaseMeta.HBASETABLE, "test")
.option(HBaseMeta.FAMILY, "detail")
.option(HBaseMeta.SELECTFIELDS, "userId,tagsId")
.load()
// 新表和舊錶進行join
val joinTags: DataFrame = genderOldDatas.join(AvgTransactionNewTags, genderOldDatas("userId") === AvgTransactionNewTags("userId"))
joinTags.show()
val allTags: DataFrame = joinTags.select(
// 處理第一個字段
when((genderOldDatas.col("userId").isNotNull), (genderOldDatas.col("userId")))
.when((AvgTransactionNewTags.col("userId").isNotNull), (AvgTransactionNewTags.col("userId")))
.as("userId"),
getAllTages(genderOldDatas.col("tagsId"), AvgTransactionNewTags.col("tagsId")).as("tagsId")
)
// 新數據與舊數據彙總之後的數據
allTags.show(10)
// 將最終結果進行覆蓋
allTags.write.format("com.czxy.tools.HBaseDataSource")
.option("zkHosts", hbaseMeta.zkHosts)
.option(HBaseMeta.ZKPORT, hbaseMeta.zkPort)
.option(HBaseMeta.HBASETABLE,"test")
.option(HBaseMeta.FAMILY, "detail")
.option(HBaseMeta.SELECTFIELDS, "userId,tagsId")
.option("repartition",1)
.save()
}
//將mysql中的四級標籤的rule 封裝成HBaseMeta
//方便後續使用的時候方便調用
def toHBaseMeta(KVMap: Map[String, String]): HBaseMeta = {
//開始封裝
HBaseMeta(KVMap.getOrElse("inType",""),
KVMap.getOrElse(HBaseMeta.ZKHOSTS,""),
KVMap.getOrElse(HBaseMeta.ZKPORT,""),
KVMap.getOrElse(HBaseMeta.HBASETABLE,""),
KVMap.getOrElse(HBaseMeta.FAMILY,""),
KVMap.getOrElse(HBaseMeta.SELECTFIELDS,""),
KVMap.getOrElse(HBaseMeta.ROWKEY,"")
)
}
}
小結
本篇博客,博主主要爲大家帶來了如何對統計型標籤進行開發的一個小Demo。其實關於統計型標籤的開發還有很多,它們會隨着不同的業務,有着不同的開發流程,例如求取用戶的常用支付方式,最近登錄時間等等…這裏就不一一敘述了。
大家可能發現了,最近幾篇講解標籤開發的博客代碼都有大量的相似之處,那麼我們能不能將其抽取一下,優化一下代碼的開發呢?下一篇博客,讓菌哥來告訴你答案!
如果以上過程中出現了任何的紕漏錯誤,煩請大佬們指正😅
受益的朋友或對大數據技術感興趣的夥伴記得點贊關注支持一波🙏
希望我們都能在學習的道路上越走越遠😉