SparkCore项目实战 需求一Top10热门品类 需求二Top10热门品类下每个品类的Top10活跃用户统计 需求三计算页面单跳转换率

数据格式简介

本项目的数据是采集电商网站的用户行为数据,主要包含用户的4种行为:搜索、点击、下单和支付。另:

(1)数据采用下划线分割字段

(2)每一行表示用户的一个行为,所以每一行只能是四种行为中的一种

(3)如果搜索关键字是null,表示这次不是搜索

(4)如果点击的品类id和产品id是-1表示这次不是点击

(5)下单行为来说一次可以下单多个产品,所以品类id和产品id都是多个,id之间使用逗号分割。如果本次不是下单行为,则他们相关数据用null来表示

(6)支付行为和下单行为类似

在这里插入图片描述

数据详细字段说明

在这里插入图片描述

数据下载链接

需求一:Top10热门品类(普通算子实现)

思路分析

品类是指产品的分类。分别统计每个品类点击的次数,下单的次数和支付的次数。先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数

#抽象概括为
(id)		    点击品类数 下单品类数 支付品类数
衣服(id)	    点击品类数 下单品类数 支付品类数
生活用品(id)    点击品类数 下单品类数 支付品类数


#最终的输出效果为
CategoryCountInfo(15,6120,1672,1259)
CategoryCountInfo(2,6119,1767,1196)
#而不是
(,6120,1672,1259)
(保健品,6119,1767,1196)

代码实现

package com.xcu.bigdata.spark.core.pg03_topn

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable.ListBuffer


object Top1 {
  def main(args: Array[String]): Unit = {
    // 创建SparkConf
    val conf: SparkConf = new SparkConf().setAppName("Top1").setMaster("local[*]")
    // 创建SparkContext,该对象是提交的入口
    val sc = new SparkContext(conf)
    // 1 读取数据
    val dataRDD: RDD[String] = sc.textFile("./input/user_visit_action1.txt")
    // 2 将读到的数据进行切分,并且将切分的内容封装为UserVisitAction对象
    val actionRDD: RDD[UserVisitAction] = dataRDD.map {
      line => {
        val fields: Array[String] = line.split("_")
        UserVisitAction(
          fields(0),
          fields(1).toLong,
          fields(2),
          fields(3).toLong,
          fields(4),
          fields(5),
          fields(6).toLong,
          fields(7).toLong,
          fields(8),
          fields(9),
          fields(10),
          fields(11),
          fields(12).toLong
        )
      }
    }
    // 3 判读当前这条日志记录的是什么行为,并且封装为结果对象
    /*
      (鞋,1,0,0)
      (保健品,1,0,0)
      (鞋,0,1,0)
      (保健品,0,1,0)
      (鞋,0,0,1)=====>(鞋,1,1,1)
     */
    val infoRDD: RDD[CategoryCountInfo] = actionRDD.flatMap {
      userAction => {
        // 判读是否是点击行为
        if (userAction.click_category_id != -1) {
          // 封装输出结果对象
          List(CategoryCountInfo(userAction.click_category_id + "", 1, 0, 0))
          // 判读是否是下单行为,如果是下单行为,需要对当前订单中涉及的所有品类id进行切分
        } else if (userAction.order_category_ids != "null") {
          // 坑:读取的文件应该是null字符串,而不是null对象
          val ids: Array[String] = userAction.order_category_ids.split(",")
          // 定义一个集合,用于存放多个品类id封装的输出结果对象
          val list: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          // 对所有品类id进行遍历
          for (id <- ids) {
            list.append(CategoryCountInfo(id, 0, 1, 0))
          }
          list
          // 支付
        } else if (userAction.pay_category_ids != "null") {
          val ids: Array[String] = userAction.pay_category_ids.split(",")
          // 定义一个集合,用于存放多个品类id封装的输出结果对象
          val list: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          // 对所有品类的id进行遍历
          for (id <- ids) {
            list.append(CategoryCountInfo(id, 0, 0, 1))
          }
          list
        } else {
          Nil
        }
      }
    }
    // 4 将相同的品类的放到一组
    val groupRDD: RDD[(String, Iterable[CategoryCountInfo])] = infoRDD.groupBy(_.categoryId)
    // 5 将分组之后的数据进行聚合处理
    val reduceRDD: RDD[(String, CategoryCountInfo)] = groupRDD.mapValues {
      datas => {
        datas.reduce {
          (info1, info2) => {
            info1.clickCount = info1.clickCount + info2.clickCount
            info1.orderCount = info1.orderCount + info2.orderCount
            info1.payCount = info1.payCount + info2.payCount
            info1 //((鞋子), 100, 20, 11)
          }
        }
      }
    }
    // 6 对上述RDD的结构进行转换,只保留value部分,得到聚合之后的RDD[CategoryCountInfo]
    val mapRDD: RDD[CategoryCountInfo] = reduceRDD.map(_._2)
    // 7 对RDD中的数据排序,取前10
    // clickCount降序排序,clickCount相同的是按照orderCount排序,依次类推
    // 可能会内存溢出
    val resRDD: Array[CategoryCountInfo] = mapRDD.sortBy(info => (info.clickCount, info.orderCount, info.payCount), false).take(10)
    // 8 打印输出
    resRDD.foreach(println)
    // 关闭连接
    sc.stop()
  }
}


// 用户访问动作表
case class UserVisitAction(date: String, // 用户点击行为日期
                           user_id: Long, // 用户的id
                           session_id: String, // Session的id
                           page_id: Long, // 某个页面的id
                           action_time: String, // 动作的时间点
                           search_keyword: String, // 用户搜索的关键词
                           click_category_id: Long, // 某一个商品品类的id
                           click_product_id: Long, // 某一个商品的id
                           order_category_ids: String, // 一次订单中所有品类的id集合
                           order_product_ids: String, // 一次订单中所有商品的id集合
                           pay_category_ids: String, // 一次支付中所有品类的id集合
                           pay_product_ids: String, // 一次支付中所有商品id集合
                           city_id: Long // 城市id
                          )


// 输出结果表
case class CategoryCountInfo(
                              categoryId: String, // 品类id
                              var clickCount: Long, // 点击次数
                              var orderCount: Long, // 订单次数
                              var payCount: Long // 支付次数
                            )

结果输出:

CategoryCountInfo(15,6120,1672,1259)
CategoryCountInfo(2,6119,1767,1196)
CategoryCountInfo(20,6098,1776,1244)
CategoryCountInfo(12,6095,1740,1218)
CategoryCountInfo(11,6093,1781,1202)
CategoryCountInfo(17,6079,1752,1231)
CategoryCountInfo(7,6074,1796,1252)
CategoryCountInfo(9,6045,1736,1230)
CategoryCountInfo(19,6044,1722,1158)
CategoryCountInfo(13,6036,1781,1161)

优化:需求一(使用ReduceByKey进行预聚合)

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable.ListBuffer

object TOPN1 {
  def main(args: Array[String]): Unit = {
    val conf: SparkConf = new SparkConf().setAppName("").setMaster("local[*]")
    val sc = new SparkContext(conf)
    val textRDD: RDD[String] = sc.textFile("E:\\input\\user_visit_action.txt")
    val actionRDD: RDD[UserVisitAction] = textRDD.map(line => {
      val fields: Array[String] = line.split("_")
      UserVisitAction(fields(0),
        fields(1).toLong,
        fields(2),
        fields(3).toLong,
        fields(4),
        fields(5),
        fields(6).toLong,
        fields(7).toLong,
        fields(8),
        fields(9),
        fields(10),
        fields(11),
        fields(12).toLong
      )
    })
    val infoRDD: RDD[(String, CategoryCountInfo)] = actionRDD.flatMap(
      action => {
        if (action.click_category_id != -1) { // 为下边的reduceByKey算子制造key
          List((action.click_category_id.toString, CategoryCountInfo(action.click_category_id.toString, 1, 0, 0)))
        } else if (action.order_category_ids != "null") {
          val ids: Array[String] = action.order_category_ids.split(",")
          val list = ListBuffer[(String, CategoryCountInfo)]()
          for (elem <- ids) {
            list.append((elem, CategoryCountInfo(elem, 0, 1, 0)))
          }
          list
        } else if (action.pay_category_ids != "null") {
          val ids: Array[String] = action.pay_category_ids.split(",")
          val list = ListBuffer[(String, CategoryCountInfo)]()
          for (elem <- ids) {
            list.append((elem, CategoryCountInfo(elem, 0, 0, 1)))
          }
          list
        } else {
          Nil
        }
      }
    )
    // 使用reduceByKey进行预聚合(如果使用reduce,所有数据交由一个task处理,会造成数据倾斜)
    val reduceByKeyRDD: RDD[(String, CategoryCountInfo)] = infoRDD.reduceByKey(
      (info1, info2) => {
        info1.clickCount = info1.clickCount + info2.clickCount
        info1.orderCount = info1.orderCount + info2.orderCount
        info1.payCount = info1.payCount + info2.payCount
        info1
      }
    )
    val mapRDD: RDD[CategoryCountInfo] = reduceByKeyRDD.map(_._2)
    // 排序(依旧可能内存溢出)
    val resRDD: Array[CategoryCountInfo] = mapRDD.sortBy(info => {
      (info.clickCount, info.orderCount, info.payCount)
    }, ascending = false).take(10)
    // 结果输出
    resRDD.foreach(println)

  }
}

case class UserVisitAction(date: String, // 用户点击行为日期
                           user_id: Long, // 用户的id
                           session_id: String, // Session的id
                           page_id: Long, // 某个页面的id
                           action_time: String, // 动作的时间点
                           search_keyword: String, // 用户搜索的关键词
                           click_category_id: Long, // 某一个商品品类的id
                           click_product_id: Long, // 某一个商品的id
                           order_category_ids: String, // 一次订单中所有品类的id集合
                           order_product_ids: String, // 一次订单中所有商品的id集合
                           pay_category_ids: String, // 一次支付中所有品类的id集合
                           pay_product_ids: String, // 一次支付中所有商品id集合
                           city_id: Long // 城市id
                          )

case class CategoryCountInfo(categoryId: String, // 品类id
                             var clickCount: Long, // 点击次数
                             var orderCount: Long, // 订单次数
                             var payCount: Long // 支付次数
                            )

优化:需求一(采用累加器,避免shuffle过程)

最好的办法应该是遍历一次能够计算出来上述的3个指标。使用累加器可以达成我们的需求

  1. 遍历全部日志数据,根据品类id和操作类型分别累加,需要用到累加器
    定义累加器,当碰到订单和支付业务的时候注意拆分字段才能得到品类 id
  2. 遍历完成之后就得到每个品类 id 和操作类型的数量.
  3. 按照点击下单支付的顺序来排序
  4. 取出 Top10
import org.apache.spark.rdd.RDD
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.{immutable, mutable}
import scala.collection.mutable.ListBuffer

object w2 {
  def main(args: Array[String]): Unit = {
    // 创建SparkConf
    val conf: SparkConf = new SparkConf().setAppName("Top1").setMaster("local[*]")
    // 创建SparkContext,该对象是提交的入口
    val sc = new SparkContext(conf)
    // 读取数据
    val dataRDD: RDD[String] = sc.textFile("E:\\input\\user_visit_action.txt")
    // 将读到的数据进行切分,并且将切分的内容封装为UserVisitAction对象
    val actionRDD: RDD[UserVisitAction] = dataRDD.map {
      line => {
        val fields: Array[String] = line.split("_")
        UserVisitAction(
          fields(0),
          fields(1).toLong,
          fields(2),
          fields(3).toLong,
          fields(4),
          fields(5),
          fields(6).toLong,
          fields(7).toLong,
          fields(8),
          fields(9),
          fields(10),
          fields(11),
          fields(12).toLong
        )
      }
    }

    // 创建累加器
    val acc = new CategoryCountAccumulator
    // 注册累加器
    sc.register(acc, "myAcc")
    actionRDD.foreach(action => {
      acc.add(action)
    })
    //获取累加器的值
    /*((鞋,click),10)
     ((鞋,order),20)
     ((鞋,pay),30)*/
    val accMap: mutable.Map[(String, String), Long] = acc.value
    // 对累加出来的数据按照类别进行分组。注意:这个时候每个类别之后有三条记录,数据量不会很大
    val groupMap: Map[String, mutable.Map[(String, String), Long]] = accMap.groupBy(_._1._1)
    // 对分组后的数据进行结构的转换:CategoryCountInfo
    val mapRDD: immutable.Iterable[CategoryCountInfo] = groupMap.map {
      case (id, map) => {
        CategoryCountInfo(
          id,
          map.getOrElse((id, "click"), 0L),
          map.getOrElse((id, "order"), 0L),
          map.getOrElse((id, "pay"), 0L)
        )
      }
    }
    // 将转换后的数据进行排序(降序)取前10名
    mapRDD.toList.sortWith(
      (left, right) => {
        if (left.clickCount > right.clickCount) {
          true
        } else if (left.clickCount == right.clickCount) {
          if (left.orderCount > right.orderCount) {
            true
          } else if (left.orderCount == right.orderCount) {
            left.payCount > right.payCount
          } else {
            false
          }
        } else {
          false
        }
      }
    ).take(10).foreach(println)
    // 关闭连接
    sc.stop()
  }
}


// 用户访问动作表
case class UserVisitAction(date: String, // 用户点击行为日期
                           user_id: Long, // 用户的id
                           session_id: String, // Session的id
                           page_id: Long, // 某个页面的id
                           action_time: String, // 动作的时间点
                           search_keyword: String, // 用户搜索的关键词
                           click_category_id: Long, // 某一个商品品类的id
                           click_product_id: Long, // 某一个商品的id
                           order_category_ids: String, // 一次订单中所有品类的id集合
                           order_product_ids: String, // 一次订单中所有商品的id集合
                           pay_category_ids: String, // 一次支付中所有品类的id集合
                           pay_product_ids: String, // 一次支付中所有商品id集合
                           city_id: Long // 城市id
                          )


// 输出结果表
case class CategoryCountInfo(
                              categoryId: String, // 品类id
                              var clickCount: Long, // 点击次数
                              var orderCount: Long, // 订单次数
                              var payCount: Long // 支付次数
                            )
// AccumulatorV2[输入类型, 输出类型]
class CategoryCountAccumulator extends AccumulatorV2[UserVisitAction, mutable.Map[(String, String), Long]] {

  var map1 = mutable.Map[(String, String), Long]()

  // 初始化
  override def isZero: Boolean = map1.isEmpty

  // 拷贝
  override def copy(): AccumulatorV2[UserVisitAction, mutable.Map[(String, String), Long]] = {
    val newACC = new CategoryCountAccumulator
    newACC.map1 = this.map1
    newACC
  }

  // 重置
  override def reset(): Unit = map1.clear()

  // 分区内业务处理逻辑
  override def add(action: UserVisitAction): Unit = {
    // 点击
    if (action.click_category_id != -1) {
      val key: (String, String) = (action.click_category_id.toString, "click")
      map1(key) = map1.getOrElse(key, 0L) + 1L
      // 下单
    } else if (action.order_category_ids != "null") {
      val ids: Array[String] = action.order_category_ids.split(",")
      for (id <- ids) {
        val key: (String, String) = (id, "order")
        map1(key) = map1.getOrElse(key, 0L) + 1L
      }
      // 支付
    } else if (action.pay_category_ids != "null") {
      val ids: Array[String] = action.pay_category_ids.split(",")
      for (id <- ids) {
        val key: (String, String) = (id, "pay")
        map1(key) = map1.getOrElse(key, 0L) + 1L
      }
    }

  }

  // 分区间合并
  override def merge(other: AccumulatorV2[UserVisitAction, mutable.Map[(String, String), Long]]): Unit = {
    var map2 = other.value
    var map3 = this.map1
    map2.foldLeft(map3)((map3, map2) => {
      val k: (String, String) = map2._1
      val v: Long = map2._2
      map3(k) = map3.getOrElse(k, 0L) + v
      map3
    })
  }

  override def value: mutable.Map[(String, String), Long] = map1
}

需求二:Top10热门品类下每个品类的Top10活跃用户统计

不同的Session代表不同的用户

思路分析

  1. 通过需求一,获取到了Top10热门品类的id

  2. 将原始数据进行过滤,然后只保留热门品类和点击操作

  3. 对session的点击数进行转换 (category-session,1)

  4. 对session的点击数进行统计 (category-session,sum)

  5. 将统计聚合的结果进行转换 (category,(session,sum))

  6. 将转换后的结构按照品类进行分组 (category,Iterator[(session,sum)])

  7. 对分组后的数据降序 取前10

#最终输出效果
(7,List((a41bc6ea-b3e3-47ce-98af-48169da7c91b,9), (9fa653ec-5a22-4938-83c5-21521d083cd0,7), (f34878b8-1784-4d81-a4d1-0c93ce53e942,7), (2d4b9c3e-2a9e-41b6-9573-9fde3533ed89,7), (95cb71b8-7033-448f-a4db-ae9861dd996b,7), (4dbd319c-3f44-48c9-9a71-a917f1d922c1,7), (aef6615d-4c71-4d39-8062-9d5d778e55f1,7), (a96c0b6b-f0e3-4bc5-9780-6c1046c335af,6), (22421bea-8f9b-4d76-b10d-9ca91f1d9403,6), (f666d6ba-b3e8-45b1-a269-c5d6c08413c3,6)))

(9,List((199f8e1d-db1a-4174-b0c2-ef095aaef3ee,9), (329b966c-d61b-46ad-949a-7e37142d384a,8), (5e3545a0-1521-4ad6-91fe-e792c20c46da,8), (bed60a57-3f81-4616-9e8b-067445695a77,7), (8f9723a3-833d-4103-a7ff-352cd17de067,7), (cbdbd1a4-7760-4195-bfba-fa44492bf906,7), (f205fd4f-f312-46d2-a850-26a16ac2734c,7), (8a0f8fe1-d0f4-4687-aff3-7ce37c52ab71,7), (e306c00b-a6c5-44c2-9c77-15e919340324,7), (4f0261cc-2cb1-40e0-9ffb-5587920c1084,7)))
......

代码实现

package com.xcu.bigdata.spark.core.pg03_topn

import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD

import scala.collection.mutable.ListBuffer


object Top2 {
  def main(args: Array[String]): Unit = {
    // 创建SparkConf
    val conf: SparkConf = new SparkConf().setAppName("Top2").setMaster("local[*]")
    // 创建SparkContext,该对象是提交的入口
    val sc = new SparkContext(conf)
    // 1 读取数据
    val dataRDD: RDD[String] = sc.textFile("./input/user_visit_action1.txt")
    // 2 将读到的数据进行切分,并且将切分的内容封装为UserVisitAction对象
    val actionRDD: RDD[UserVisitAction] = dataRDD.map {
      line => {
        val fields: Array[String] = line.split("_")
        UserVisitAction(
          fields(0),
          fields(1).toLong,
          fields(2),
          fields(3).toLong,
          fields(4),
          fields(5),
          fields(6).toLong,
          fields(7).toLong,
          fields(8),
          fields(9),
          fields(10),
          fields(11),
          fields(12).toLong
        )
      }
    }
    // 3 判读当前这条日志记录的是什么行为,并且封装为结果对象
    val infoRDD: RDD[CategoryCountInfo] = actionRDD.flatMap {
      userAction => {
        // 判读是否是点击行为
        if (userAction.click_category_id != -1) {
          // 封装输出结果对象
          List(CategoryCountInfo(userAction.click_category_id + "", 1, 0, 0))
          // 判读是否是下单行为,如果是下单行为,需要对当前订单中涉及的所有品类id进行切分
        } else if (userAction.order_category_ids != "null") {
          // 坑:读取的文件应该是null字符串,而不是null对象
          val ids: Array[String] = userAction.order_category_ids.split(",")
          // 定义一个集合,用于存放多个品类id封装的输出结果对象
          val list: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          // 对所有品类id进行遍历
          for (id <- ids) {
            list.append(CategoryCountInfo(id, 0, 1, 0))
          }
          list
          // 支付品类
        } else if (userAction.pay_category_ids != "null") {
          val ids: Array[String] = userAction.pay_category_ids.split(",")
          // 定义一个集合,用于存放多个品类id封装的输出结果对象
          val list: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          // 对所有品类的id进行遍历
          for (id <- ids) {
            list.append(CategoryCountInfo(id, 0, 0, 1))
          }
          list
        } else {
          Nil
        }
      }
    }
    // 4 将相同的品类的放到一组
    val groupRDD: RDD[(String, Iterable[CategoryCountInfo])] = infoRDD.groupBy(_.categoryId)
    // 5 将分组之后的数据进行聚合处理
    val reduceRDD: RDD[(String, CategoryCountInfo)] = groupRDD.mapValues {
      datas => {
        datas.reduce {
          (info1, info2) => {
            info1.clickCount = info1.clickCount + info2.clickCount
            info1.orderCount = info1.orderCount + info2.orderCount
            info1.payCount = info1.payCount + info2.payCount
            info1 //(鞋子, 100, 20, 11)
          }
        }
      }
    }
    // 6 对上述RDD的结构进行转换,只保留value部分,得到聚合之后的RDD
    val mapRDD: RDD[CategoryCountInfo] = reduceRDD.map(_._2)
    // 7 对RDD中的数据排序,取前10
    // clickCount降序排序,clickCount相同时候再按照orderCount排序,依次类推
    val resRDD: Array[CategoryCountInfo] = mapRDD.sortBy(info => (info.clickCount, info.orderCount, info.payCount), false).take(10)
    //******************************需求2******************************
    // 1 获取热门品类top10的品类id
    val ids: Array[String] = resRDD.map(_.categoryId)
    // 2 因为发送给Excutor中的task使用,每一个task都会创建一个副本,所以可以使用广播变量
    val broadcastIds: Broadcast[Array[String]] = sc.broadcast(ids)
    // 3 将原始数据进行过滤(保留热门品类中的点击操作)
    val filterRDD: RDD[UserVisitAction] = actionRDD.filter(
      action => {
        // 只保留点击行为
        if (action.click_category_id != -1) {
          // 同时确定是热门品类的点击
          // id是long类型,需要进行转换
          broadcastIds.value.contains(action.click_category_id.toString)
        } else {
          false
        }
      }
    )
    // 对session的点击数进行转换(category_session, 1)
    val mapRDD1: RDD[(String, Int)] = filterRDD.map(
      action => {
        (action.click_category_id + "_" + action.session_id, 1)
      }
    )
    // 对session的点击数进行统计(category_session,sum)
    val reduceBykeyRDD: RDD[(String, Int)] = mapRDD1.reduceByKey(_ + _)
    // 将统计聚合的结果进行转换(category(session,sum))
    val mapRDD2: RDD[(String, (String, Int))] = reduceBykeyRDD.map {
      case (categoryAndSession, sum) => {
        (categoryAndSession.split("_")(0), (categoryAndSession.split("_")(1), sum))
      }
    }
    // 将转换后的结构按照品类进行分组(category,Iterator[(session,sum)])
    val groupRDD2: RDD[(String, Iterable[(String, Int)])] = mapRDD2.groupByKey()
    // 对分组后的数据进行排序, 取前10
    val resRDD1: RDD[(String, List[(String, Int)])] = groupRDD2.mapValues(
      datas => {
        datas.toList.sortWith {
          case (left, right) => {
            left._2 > right._2
          }
        }.take(10)
      }
    )
    resRDD1.foreach(println)
    // 关闭连接
    sc.stop()
  }
}

case class UserVisitAction(date: String, // 用户点击行为日期
                           user_id: Long, // 用户的id
                           session_id: String, // Session的id
                           page_id: Long, // 某个页面的id
                           action_time: String, // 动作的时间点
                           search_keyword: String, // 用户搜索的关键词
                           click_category_id: Long, // 某一个商品品类的id
                           click_product_id: Long, // 某一个商品的id
                           order_category_ids: String, // 一次订单中所有品类的id集合
                           order_product_ids: String, // 一次订单中所有商品的id集合
                           pay_category_ids: String, // 一次支付中所有品类的id集合
                           pay_product_ids: String, // 一次支付中所有商品id集合
                           city_id: Long // 城市id
                          )

case class CategoryCountInfo(categoryId: String, // 品类id
                             var clickCount: Long, // 点击次数
                             var orderCount: Long, // 订单次数
                             var payCount: Long // 支付次数
                            )

部分结果集:

(7,List((a41bc6ea-b3e3-47ce-98af-48169da7c91b,9), (9fa653ec-5a22-4938-83c5-21521d083cd0,7), (f34878b8-1784-4d81-a4d1-0c93ce53e942,7), (2d4b9c3e-2a9e-41b6-9573-9fde3533ed89,7), (95cb71b8-7033-448f-a4db-ae9861dd996b,7), (4dbd319c-3f44-48c9-9a71-a917f1d922c1,7), (aef6615d-4c71-4d39-8062-9d5d778e55f1,7), (a96c0b6b-f0e3-4bc5-9780-6c1046c335af,6), (22421bea-8f9b-4d76-b10d-9ca91f1d9403,6), (f666d6ba-b3e8-45b1-a269-c5d6c08413c3,6)))
(9,List((199f8e1d-db1a-4174-b0c2-ef095aaef3ee,9), (329b966c-d61b-46ad-949a-7e37142d384a,8), (5e3545a0-1521-4ad6-91fe-e792c20c46da,8), (bed60a57-3f81-4616-9e8b-067445695a77,7), (8f9723a3-833d-4103-a7ff-352cd17de067,7), (cbdbd1a4-7760-4195-bfba-fa44492bf906,7), (f205fd4f-f312-46d2-a850-26a16ac2734c,7), (8a0f8fe1-d0f4-4687-aff3-7ce37c52ab71,7), (e306c00b-a6c5-44c2-9c77-15e919340324,7), (4f0261cc-2cb1-40e0-9ffb-5587920c1084,7)))

Process finished with exit code 0

需求三:计算页面单跳转换率

什么是页面单跳转换率?

比如一个用户在一次 Session 过程中访问的页面路径 3,5,7,9,10,21,那么页面 3 跳到页面 5 叫一次单跳,7-9 也叫一次单跳,那么单跳转化率就是要统计页面点击的概率

具体怎么计算:

比如:计算页面 3 跳到页面 5(3-5)的单跳转化率,先获取符合条件的 Session 对于页面 3 的访问次数(PV)为 A,然后获取符合条件的 Session 中访问了页面 3 又紧接着访问了页面 5 的次数为 B,那么 B/A 就是 3-5 的页面单跳转化率

思路分析

  1. 读取原始数据

  2. 将原始数据映射为样例类

  3. 将原始数据根据session进行分组

  4. 将分组后的数据根据时间进行升序排序

  5. 将排序后的数据进行结构转换(pageId,1)

  6. 计算分母:将相同的页面id进行聚合统计(pageId,sum)

  7. 计算分子:将页面id进行拉链,形成连续的拉链效果,转换结构(pageId-pageId2,1)

  8. 将转换结构后的数据进行聚合统计(pageId-pageId2,sum)

  9. 计算页面单跳转换率

代码实现

package com.xcu.bigdata.spark.core.pg03_topn

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}


object Top3 {
  def main(args: Array[String]): Unit = {
    // 创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("Top3").setMaster("local[*]")
    // 创建SparkContext
    val sc = new SparkContext(conf)
    // 1 读取数据
    val dataRDD: RDD[String] = sc.textFile("./input/user_visit_action1.txt")
    // 2 将读取到数据进行切分,并且将切分的内容封装为UserVisitAction对象
    val actionRDD: RDD[UserVisitAction] = dataRDD.map {
      line => {
        val fields: Array[String] = line.split("_")
        UserVisitAction(
          fields(0),
          fields(1).toLong,
          fields(2),
          fields(3).toLong,
          fields(4),
          fields(5),
          fields(6).toLong,
          fields(7).toLong,
          fields(8),
          fields(9),
          fields(10),
          fields(11),
          fields(12).toLong
        )
      }

    }
    //******************************需求3******************************
    // 计算分母
    // 3 对当前日志中记录的访问页面进行计数
    val pageIdRDD: RDD[(Long, Long)] = actionRDD.map {
      action => {
        (action.page_id, 1L)
      }
    }
    // 4 通过页面的计数,计算每一个页面出现的总次数 作为求单跳转换率的分母
    val fmIdsMap: Map[Long, Long] = pageIdRDD.reduceByKey(_ + _).collect().toMap
    /*
    zs  11:35:00  首页
    ls  11:35:00  首页
    zs  11:36:00  详情
    zs  11:37:00  下单
     */
    // 计算分子
    // 5.1 将原始数据根据sessionId进行分组,得到同一个用户的访问行为
    val sessionRDD: RDD[(String, Iterable[UserVisitAction])] = actionRDD.groupBy(_.session_id)
    // 5.2 将分组后的同一个用户的访问行为,按照时间进行升序排序
    val pageFlowRDD: RDD[(String, List[(String, Int)])] = sessionRDD.mapValues {
      action => {
        // 得到排序后的同一个session用户访问行为
        val userActions: List[UserVisitAction] = action.toList.sortWith {
          (left, right) => {
            left.action_time < right.action_time
          }
        }
        // 5.3 对排序后的同一个用户访问行为进行结构转换,只保留页面就可以
        val pageIdsList: List[Long] = userActions.map(_.page_id)
        /*
        A->B->C->D->E->F
        B->C->D->E->F
         */
        // 5.4 对当前会话用户访问页面进行拉链,得到页面的流转情况(页面A, 页面B)
        val pageFlows: List[(Long, Long)] = pageIdsList.zip(pageIdsList.tail)
        // 5.5 对拉链后的数据,进行结构转换(页面A-页面B, 1)
        pageFlows.map {
          case (pageId1, pageId2) => {
            (pageId1 + "-" + pageId2, 1)
          }
        }
      }
    }
    // 6 将每一个会话的页面跳转统计完毕之后,没有必要保留会话信息了,即session,所以对上述RDD的结构进行转换
    // 只保留页面跳转以及计数
    val pageFlowMapRdd: RDD[(String, Int)] = pageFlowRDD.map(_._2).flatMap(list => list)
    // 7 对页面跳转情况进行聚合操作
    val pageAToPageBSumRDD: RDD[(String, Int)] = pageFlowMapRdd.reduceByKey(_ + _)
    pageAToPageBSumRDD.foreach {
      //(pageA-pageB, sum)
      case (pageFlow, fz) => {
        val pageIds: Array[String] = pageFlow.split("-")
        // 获取分母页面id
        val fmPageId: Long = pageIds(0).toLong
        // 根据分母页面id,获取分母页面总访问数
        val fmSum: Long = fmIdsMap.getOrElse(fmPageId, 1L)
        // 转换率
        println(pageFlow + "--->" + fz.toDouble / fmSum)
      }
    }
    // 关闭连接
    sc.stop()
  }
}

case class UserVisitAction(date: String, // 用户点击行为日期
                           user_id: Long, // 用户的id
                           session_id: String, // Session的id
                           page_id: Long, // 某个页面的id
                           action_time: String, // 动作的时间点
                           search_keyword: String, // 用户搜索的关键词
                           click_category_id: Long, // 某一个商品品类的id
                           click_product_id: Long, // 某一个商品的id
                           order_category_ids: String, // 一次订单中所有品类的id集合
                           order_product_ids: String, // 一次订单中所有商品的id集合
                           pay_category_ids: String, // 一次支付中所有品类的id集合
                           pay_product_ids: String, // 一次支付中所有商品id集合
                           city_id: Long // 城市id
                          )

case class CategoryCountInfo(categoryId: String, // 品类id
                             var clickCount: Long, // 点击次数
                             var orderCount: Long, // 订单次数
                             var payCount: Long // 支付次数
                            )
  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值