大數據【企業級360°全方位用戶畫像】統計型標籤開發

寫在前面: 博主是一名軟件工程系大數據應用開發專業大二的學生,暱稱來源於《愛麗絲夢遊仙境》中的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。其實關於統計型標籤的開發還有很多,它們會隨着不同的業務,有着不同的開發流程,例如求取用戶的常用支付方式,最近登錄時間等等…這裏就不一一敘述了。

        大家可能發現了,最近幾篇講解標籤開發的博客代碼都有大量的相似之處,那麼我們能不能將其抽取一下,優化一下代碼的開發呢?下一篇博客,讓菌哥來告訴你答案!

在這裏插入圖片描述
        如果以上過程中出現了任何的紕漏錯誤,煩請大佬們指正😅

        受益的朋友或對大數據技術感興趣的夥伴記得點贊關注支持一波🙏

        希望我們都能在學習的道路上越走越遠😉
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章