15_spark_core_实战

一.数据准备

1)数据格式
在这里插入图片描述

2)数据详细字段说明

编号字段名称字段类型字段含义
1dateString用户点击行为的日期
2user_idLong用户的ID
3session_idStringSession的ID
4page_idLong某个页面的ID
5action_timeString动作的时间点
6search_keywordString用户搜索的关键词
7click_category_idLong点击某一个商品品类的ID
8click_product_idLong某一个商品的ID
9order_category_idsString一次订单中所有品类的ID集合
10order_product_idsString一次订单中所有商品的ID集合
11pay_category_idsString一次支付中所有品类的ID集合
12pay_product_idsString一次支付中所有商品的ID集合
13city_idLong城市 id

二.需求1:Top10热门品类

在这里插入图片描述需求说明:品类是指产品的分类,大型电商网站品类分多级,咱们的项目中品类只有一级,不同的公司可能对热门的定义不一样。我们按照每个品类的点击、下单、支付的量(次数)来统计热门品类。
鞋 点击数 下单数 支付数
衣服 点击数 下单数 支付数
电脑 点击数 下单数 支付数
例如,综合排名 = 点击数20% + 下单数30% + 支付数*50%
本项目需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。

2.1需求分析(方案一)常规算子

思路:分别统计每个品类点击的次数,下单的次数和支付的次数。然后想办法将三个RDD联合到一起。
(品类,点击总数)(品类,下单总数)(品类,支付总数)
(品类,(点击总数,下单总数,支付总数))
然后就可以按照各品类的元组(点击总数,下单总数,支付总数)进行倒序排序了,因为元组排序刚好是先排第一个元素,然后排第二个元素,最后第三个元素。

2.2 需求实现(方案一)

0) 创建包名:com.atguigu.project01
1) 方案一代码实现方式1(cogroup算子实现满外连接)

```scala
package com.atguigu.project01

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

object require01_top10Category_method1 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)
    //1.读取原始日志数据
    val actionRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")
    //2.统计品类的点击数量 (品类ID,点击数量)
    val clickActionRDD: RDD[String] = actionRDD.filter {
      action => {
        val datas: Array[String] = action.split("_")
        datas(6) != "-1"
      }
    }
    val clickCountRDD: RDD[(String, Int)] = clickActionRDD.map {
      action => {
        val datas: Array[String] = action.split("_")
        (datas(6), 1)
      }
    }.reduceByKey(_ + _)

    //3.统计品类的下单数量 (品类ID,下单数量)
    val orderActionRDD: RDD[String] = actionRDD.filter {
      action => {
        val datas: Array[String] = action.split("_")
        datas(8) != "null"
      }
    }
    val orderCountRDD: RDD[(String, Int)] = orderActionRDD.flatMap {
      action => {
        val datas: Array[String] = action.split("_")
        val cids: Array[String] = datas(8).split(",")
        cids.map((_, 1))
      }
    }.reduceByKey(_ + _)

    //4.统计品类的支付数量 (品类ID,支付数量)
    val payActionRDD: RDD[String] = actionRDD.filter {
      action => {
        val datas: Array[String] = action.split("_")
        datas(10) != "null"
      }
    }
    val payCountRDD: RDD[(String, Int)] = payActionRDD.flatMap {
      action => {
        val datas: Array[String] = action.split("_")
        val cids: Array[String] = datas(10).split(",")
        cids.map((_, 1))
      }
    }.reduceByKey(_ + _)

    //5.按照品类进行排序,取热门品类前10名
    //热门排名:先按照点击排,然后按照下单数量排,最后按照支付数量排
    //元组排序:先比较第一个,再比较第二个,最后比较第三个
    //(品类ID,(点击数量,下单数量,支付数量))
    //三个RDD需要进行满外连接,因此需要用到cogroup()算子
    val cogroupRDD: RDD[(String, (Iterable[Int], Iterable[Int], Iterable[Int]))] = clickCountRDD.cogroup(orderCountRDD, payCountRDD)

    val cogroupRDD2: RDD[(String, (Int, Int, Int))] = cogroupRDD.mapValues {
      case (iter1, iter2, iter3) => {
        /*var clickCnt = 0
        val clickIter: Iterator[Int] = iter1.iterator
        if (clickIter.hasNext) {
          clickCnt = clickIter.next()
        }

        var orderCnt = 0
        val orderIter: Iterator[Int] = iter2.iterator
        if (orderIter.hasNext) {
          orderCnt = orderIter.next()
        }

        var payCnt = 0
        val payIter: Iterator[Int] = iter3.iterator
        if (payIter.hasNext) {
          payCnt = payIter.next()
        }

        (clickCnt, orderCnt, payCnt)*/

        //更简洁方法,使用sum更简单不会报空指针异常,但是如果你使用max话可能报空指针异常
        //假如你的品类ID某个ID只有点击其他什么都没有,会发生空针异常
        (iter1.sum,iter2.sum,iter3.sum)
      }
    }
    //倒序排序取前10
    val result: Array[(String, (Int, Int, Int))] = cogroupRDD2.sortBy(_._2, false).take(10)

    //6.将结果采集到控制到打印输出
    result.foreach(println)

    //TODO 3 关闭资源
    sc.stop()
  }
}



结果:

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

上述方案有两个小问题需要解决优化
问题1:actionRDD算子用到了多次,最好提前缓存下。
问题2:cogroup算子底层会走shuffle,效率比较低。


2)	方案一代码实现方式2(**union补0的方式实现满外连接**)


```scala
package com.atguigu.project01

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

object require01_top10Category_method1_2 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)
    //1.读取原始日志数据
    val actionRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")

    //问题1:actionRDD在下面用到了多次,最好缓存一下
    actionRDD.cache()

    //2.统计品类的点击数量 (品类ID,点击数量)
    val clickActionRDD: RDD[String] = actionRDD.filter {
      action => {
        val datas: Array[String] = action.split("_")
        datas(6) != "-1"
      }
    }
    val clickCountRDD: RDD[(String, Int)] = clickActionRDD.map {
      action => {
        val datas: Array[String] = action.split("_")
        (datas(6), 1)
      }
    }.reduceByKey(_ + _)

    //3.统计品类的下单数量 (品类ID,下单数量)
    val orderActionRDD: RDD[String] = actionRDD.filter {
      action => {
        val datas: Array[String] = action.split("_")
        datas(8) != "null"
      }
    }
    val orderCountRDD: RDD[(String, Int)] = orderActionRDD.flatMap {
      action => {
        val datas: Array[String] = action.split("_")
        val cids: Array[String] = datas(8).split(",")
        cids.map((_, 1))
      }
    }.reduceByKey(_ + _)

    //4.统计品类的支付数量 (品类ID,支付数量)
    val payActionRDD: RDD[String] = actionRDD.filter {
      action => {
        val datas: Array[String] = action.split("_")
        datas(10) != "null"
      }
    }
    val payCountRDD: RDD[(String, Int)] = payActionRDD.flatMap {
      action => {
        val datas: Array[String] = action.split("_")
        val cids: Array[String] = datas(10).split(",")
        cids.map((_, 1))
      }
    }.reduceByKey(_ + _)
    //问题2:cogroup()算子底层会走shuffle,最好换掉..
    //5.按照品类进行排序,取热门品类前10名
    //union方式实现满外连接,需要先补0
    // (品类ID,点击数量)  => (品类ID,(点击数量,0,0))
    // (品类ID,下单数量)  => (品类ID,(0,下单数量,0))
    // (品类ID,支付数量)  => (品类ID,(0,0,支付数量))
    // (品类ID,(点击数量,下单数量,支付数量))
    val clickRDD: RDD[(String, (Int, Int, Int))] = clickCountRDD.map {
      case (cid, cnt) => {
        (cid, (cnt, 0, 0))
      }
    }
    val ordrRDD: RDD[(String, (Int, Int, Int))] = orderCountRDD.map {
      case (cid, cnt) => {
        (cid, (0, cnt, 0))
      }
    }
    val payRDD: RDD[(String, (Int, Int, Int))] = payCountRDD.map {
      case (cid, cnt) => {
        (cid, (0, 0, cnt))
      }
    }
    //union的方式实现满外连接
    val unionRDD: RDD[(String, (Int, Int, Int))] = clickRDD.union(ordrRDD).union(payRDD)

    val unionRDD2: RDD[(String, (Int, Int, Int))] = unionRDD.reduceByKey {
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    }

    //倒序排序取前10
    val result: Array[(String, (Int, Int, Int))] = unionRDD2.sortBy(_._2, false).take(10)

    //6.将结果采集到控制到打印输出
    result.foreach(println)

    //TODO 3 关闭资源
    sc.stop()
  }
}

上述方案还可以继续优化…生命不止,优化不息…
3) 方案一代码实现方式3(转换数据结构,提前补0,减少reduceByKey的次数)

package com.atguigu.project01

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

object require01_top10Category_method1_3 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)
    //1.读取原始日志数据
    val actionRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")

    //2.将数据转换结构 (提前补0)
    // 如果数据是点击 : (品类ID,(1,0,0))
    // 如果数据是下单 : (品类ID,(0,1,0))
    // 如果数据是支付 : (品类ID,(0,0,1))
    val flatRDD: RDD[(String, (Int, Int, Int))] = actionRDD.flatMap {
      action => {
        val datas: Array[String] = action.split("_")
        if (datas(6) != "-1") {
          //点击的数据
          List((datas(6), (1, 0, 0)))
        } else if (datas(8) != "null") {
          //下单的数据
          val ids: Array[String] = datas(8).split(",")
          ids.map((_, (0, 1, 0)))
        } else if (datas(10) != "null") {
          //支付的数据
          val ids: Array[String] = datas(10).split(",")
          ids.map((_, (0, 0, 1)))
        } else {
          Nil
        }
      }
    }

    //3.将相同的品类ID的数据进行分组聚合 reduceByKey
    val reduceRDD: RDD[(String, (Int, Int, Int))] = flatRDD.reduceByKey {
      (t1, t2) => {
        (t1._1 + t2._1, t1._2 + t2._2, t1._3 + t2._3)
      }
    }

    //倒序排序取前10
    val result: Array[(String, (Int, Int, Int))] = reduceRDD.sortBy(_._2, false).take(10)

    //4.将结果采集到控制台打印输出
    result.foreach(println)

    //TODO 3 关闭资源
    sc.stop()
  }
}

2.3 需求分析(方案二)样例类

采用样例类的方式实现。

在这里插入图片描述

2.4 需求实现(方案二)

1)用来封装用户行为的样例类

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

注意:样例类的属性默认是val修饰,不能修改;需要修改属性,需要采用var修饰。

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

2)核心业务代码实现

package com.atguigu.project01

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

object require01_top10Category_method2 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)

    //1 读取数据
    val lineRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")
    //2 封装样例类 将lineRDD变为actionRDD
    val actionRDD: RDD[UserVisitAction] = lineRDD.map(
      line => {
        val datas: Array[String] = line.split("_")
        //将解析出来的数据封装到样例类里面
        UserVisitAction(
          datas(0),
          datas(1),
          datas(2),
          datas(3),
          datas(4),
          datas(5),
          datas(6),
          datas(7),
          datas(8),
          datas(9),
          datas(10),
          datas(11),
          datas(12)
        )
      }
    )
    //3 转换数据结构 将actionRDD的数据变为 CategoryCountInfo(品类ID,1,0,0)
    val infoRDD: RDD[CategoryCountInfo] = actionRDD.flatMap(
      action => {
        if (action.click_category_id != "-1") {
          //点击的数据
          List(CategoryCountInfo(action.click_category_id, 1, 0, 0))
        } else if (action.order_category_ids != "null") {
          //下单的数据
          val arr: Array[String] = action.order_category_ids.split(",")
          arr.map(id => CategoryCountInfo(id, 0, 1, 0))
        } else if (action.pay_category_ids != "null") {
          //支付的数据
          val arr: Array[String] = action.pay_category_ids.split(",")
          arr.map(id => CategoryCountInfo(id, 0, 0, 1))
        } else {
          Nil
        }
      }
    )
    //4 按照品类id分组,将同一个品类的数据分到同一个组内
    val groupRDD: RDD[(String, Iterable[CategoryCountInfo])] = infoRDD.groupBy(_.categoryId)

    //5 将分组后的数据进行聚合处理 (品类id, (品类id, clickCount, OrderCount, PayCount))
    val reduceRDD: RDD[CategoryCountInfo] = groupRDD.mapValues(
      datas => {
        datas.reduce(
          (info1, info2) => {
            info1.orderCount += info2.orderCount
            info1.clickCount += info2.clickCount
            info1.payCount += info2.payCount
            info1
          }
        )
      }
    ).map(_._2)

    //6 将聚合后的数据 倒序排序 取前10
    val result: Array[CategoryCountInfo] = reduceRDD.sortBy(info => (info.clickCount, info.orderCount, info.payCount), false).take(10)
    result.foreach(println)

    //TODO 3 关闭资源
    sc.stop()
  }
}

2.5 需求分析(方案三)样例类+算子优化

针对方案二中的groupBy算子,没有提前聚合的功能,替换成reduceByKey
在这里插入图片描述

2.6 需求实现(方案三)

1)样例类代码和方案二一样。(详见方案二)
2)核心代码实现

object require01_top10Category_method3 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)

    //1 读取数据
    val lineRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")
    //2 封装样例类 将lineRDD变为actionRDD
    val actionRDD: RDD[UserVisitAction] = lineRDD.map(
      line => {
        val datas: Array[String] = line.split("_")
        //将解析出来的数据封装到样例类里面
        UserVisitAction(
          datas(0),
          datas(1),
          datas(2),
          datas(3),
          datas(4),
          datas(5),
          datas(6),
          datas(7),
          datas(8),
          datas(9),
          datas(10),
          datas(11),
          datas(12)
        )
      }
    )
    //3 转换数据结构 将actionRDD的数据变为 CategoryCountInfo(品类ID,1,0,0)
    val infoRDD: RDD[(String, CategoryCountInfo)] = actionRDD.flatMap(
      action => {
        if (action.click_category_id != "-1") {
          //点击的数据
          List((action.click_category_id, CategoryCountInfo(action.click_category_id, 1, 0, 0)))
        } else if (action.order_category_ids != "null") {
          //下单的数据
          val arr: Array[String] = action.order_category_ids.split(",")
          arr.map(id => (id, CategoryCountInfo(id, 0, 1, 0)))
        } else if (action.pay_category_ids != "null") {
          //支付的数据
          val arr: Array[String] = action.pay_category_ids.split(",")
          arr.map(id => (id, CategoryCountInfo(id, 0, 0, 1)))
        } else {
          Nil
        }
      }
    )
    // 4 按照品类id分组,两两聚合
    val reduceRDD: RDD[CategoryCountInfo] = infoRDD.reduceByKey(
      (info1, info2) => {
        info1.orderCount += info2.orderCount
        info1.clickCount += info2.clickCount
        info1.payCount += info2.payCount
        info1
      }
    ).map(_._2)
    
    //5 将聚合后的数据 倒序排序 取前10
    val result: Array[CategoryCountInfo] = reduceRDD.sortBy(info => (info.clickCount, info.orderCount, info.payCount), false).take(10)
    result.foreach(println)

    //TODO 3 关闭资源
    sc.stop()
  }
}

2.7 需求分析(方案四)累加器

在这里插入图片描述

2.8 需求实现(方案四)

1)累加器实现

//品类行为统计累加器
/**
 * 输入  UserVisitAction
 * 输出  ((鞋,click),1) ((鞋,order),1) ((鞋,pay),1)  mutable.Map[(String, String), Long]
 */
class CategoryCountAccumulator extends AccumulatorV2[UserVisitAction,mutable.Map[(String, String), Long]]{
  var map = mutable.Map[(String, String), Long]()

  override def isZero: Boolean = map.isEmpty

  override def copy(): AccumulatorV2[UserVisitAction, mutable.Map[(String, String), Long]] = new CategoryCountAccumulator

  override def reset(): Unit = map.clear()

  override def add(action: UserVisitAction): Unit = {
    if(action.click_category_id != "-1"){
      //点击  key=(cid,"click")
      val key = (action.click_category_id,"click")
      map(key) = map.getOrElse(key,0L) + 1L
    }else if(action.order_category_ids != "null"){
      //下单  key=(cid,"order")
      val cids: Array[String] = action.order_category_ids.split(",")
      for (cid <- cids) {
        val key = (cid,"order")
        map(key) = map.getOrElse(key,0L) + 1L
      }

    }else if(action.pay_category_ids != "null"){
      //支付  key=(cid,"pay")
      val cids: Array[String] = action.pay_category_ids.split(",")
      for (cid <- cids) {
        val key = (cid,"pay")
        map(key) = map.getOrElse(key,0L) + 1L
      }
    }
  }

  override def merge(other: AccumulatorV2[UserVisitAction, mutable.Map[(String, String), Long]]): Unit = {
    other.value.foreach {
      case (key, count) => {
        map(key) = map.getOrElse(key, 0L) + count
      }
    }
  }

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

2)核心逻辑实现

package com.atguigu.project01

import org.apache.spark.rdd.RDD
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.{immutable, mutable}

object require01_top10Category_method4 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)

    //1 读取数据
    val lineRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")
    //2 封装样例类 将lineRDD变为actionRDD
    val actionRDD: RDD[UserVisitAction] = lineRDD.map(
      line => {
        val datas: Array[String] = line.split("_")
        //将解析出来的数据封装到样例类里面
        UserVisitAction(
          datas(0),
          datas(1),
          datas(2),
          datas(3),
          datas(4),
          datas(5),
          datas(6),
          datas(7),
          datas(8),
          datas(9),
          datas(10),
          datas(11),
          datas(12)
        )
      }
    )
    //3 使用累加器统计相同品类id的点击数量,下单数量,支付数量
    //3.1 创建累加器
    val cateAcc = new CategoryCountAccumulator
    //3.2 注册累加器
    sc.register(cateAcc,"cateacc")
    //3.3 使用累加器
    actionRDD.foreach(action => cateAcc.add(action))
    //3.4 获得累加器的值 ((4,click),5961) ((4,order),1760) ((4,pay),1271)
    val accMap: mutable.Map[(String, String), Long] = cateAcc.value
    
    //4 将accMap按照品类id进行分组(4,Map((4,click) -> 5961, (4,order) -> 1760, (4,pay) -> 1271))
    val groupMap: Map[String, mutable.Map[(String, String), Long]] = accMap.groupBy(_._1._1)
    
    //5 将groupMap转换成样例类集合
    val infoIter: immutable.Iterable[CategoryCountInfo] = groupMap.map {
      case (id, map) => {
        val click = map.getOrElse((id, "click"), 0L)
        val order = map.getOrElse((id, "order"), 0L)
        val pay = map.getOrElse((id, "pay"), 0L)
        CategoryCountInfo(id, click, order, pay)
      }
    }
    //6 对样例类集合倒序排序取前10
    val result: List[CategoryCountInfo] = infoIter.toList.sortBy(info => (info.clickCount, info.orderCount, info.payCount))(Ordering[(Long, Long, Long)].reverse).take(10)
    result.foreach(println)

    //TODO 3 关闭资源
    sc.stop()
  }
}

3 需求2:Top10热门品类中每个品类的Top10活跃Session统计

3.1 需求分析

在这里插入图片描述

3.2 需求实现

1)累加器实现 参考2.7章节
2)核心逻辑实现

package com.atguigu.project01

import org.apache.spark.broadcast.Broadcast
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
import scala.collection.{immutable, mutable}

object require02_top10Category_sessionTop10 {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)

    //1 读取数据
    val lineRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")
    //2 封装样例类 将lineRDD变为actionRDD
    val actionRDD: RDD[UserVisitAction] = lineRDD.map(
      line => {
        val datas: Array[String] = line.split("_")
        //将解析出来的数据封装到样例类里面
        UserVisitAction(
          datas(0),
          datas(1),
          datas(2),
          datas(3),
          datas(4),
          datas(5),
          datas(6),
          datas(7),
          datas(8),
          datas(9),
          datas(10),
          datas(11),
          datas(12)
        )
      }
    )
    //3 使用累加器统计相同品类id的点击数量,下单数量,支付数量
    //3.1 创建累加器
    val cateAcc = new CategoryCountAccumulator

    //3.2 注册累加器
    sc.register(cateAcc, "cateacc")

    //3.3 使用累加器
    actionRDD.foreach(action => cateAcc.add(action))

    //3.4 获得累加器的值 ((4,click),5961) ((4,order),1760) ((4,pay),1271)
    val accMap: mutable.Map[(String, String), Long] = cateAcc.value

    //4 将accMap按照品类id进行分组   (4,Map((4,click) -> 5961, (4,order) -> 1760, (4,pay) -> 1271))
    val groupMap: Map[String, mutable.Map[(String, String), Long]] = accMap.groupBy(_._1._1)

    //5 将groupMap转换成样例类集合
    val infoIter: immutable.Iterable[CategoryCountInfo] = groupMap.map {
      case (id, map) => {
        val click = map.getOrElse((id, "click"), 0L)
        val order = map.getOrElse((id, "order"), 0L)
        val pay = map.getOrElse((id, "pay"), 0L)
        CategoryCountInfo(id, click, order, pay)
      }
    }

    //6 对样例类集合倒序排序取前10
    val result: List[CategoryCountInfo] = infoIter.toList.sortBy(info => (info.clickCount, info.orderCount, info.payCount))(Ordering[(Long, Long, Long)].reverse).take(10)

    //********************需求二**********************
    //1 获取Top10热门品类
    val ids: List[String] = result.map(_.categoryId)
    //2 ids创建广播变量
    val bdIds: Broadcast[List[String]] = sc.broadcast(ids)
    //3 过滤原始数据,保留热门Top10的点击数据
    val filterActionRDD: RDD[UserVisitAction] = actionRDD.filter(
      action => {
        //一个会话一定会从点击开始,因此我们在这只要点击的数据
        if (action.click_category_id != "-1") {
          bdIds.value.contains(action.click_category_id)
        } else {
          false
        }
      }
    )
    //4 转换数据结构 UserVisitAction => (品类id-会话id,1)
    val idAndSessionToOneRDD: RDD[(String, Int)] = filterActionRDD.map(
      action => (action.click_category_id + "=" + action.session_id, 1)
    )

    //5 按照品类id-会话id分组聚合 (品类id-会话id,1)=>(品类id-会话id,sum)
    val idAndSessionToSumRDD: RDD[(String, Int)] = idAndSessionToOneRDD.reduceByKey(_ + _)

    //6 再次变化结构,分开品类和会话 (品类id-会话id,sum) => (品类id,(会话id,sum))
    val idToSessionAndSumRDD: RDD[(String, (String, Int))] = idAndSessionToSumRDD.map {
      case (key, sum) => {
        val keys: Array[String] = key.split("=")
        (keys(0), (keys(1), sum))
      }
    }

    //7 按照品类id分组 (品类id,(会话id,sum)) => (品类id,[(会话id,sum) ,(会话id,sum),....])
    val groupRDD: RDD[(String, Iterable[(String, Int)])] = idToSessionAndSumRDD.groupByKey()

    //8 对groupRDD的每个品类的集合倒序排序,求前10
    val resultRDD: RDD[(String, List[(String, Int)])] = groupRDD.mapValues(
      datas => {
        datas.toList.sortBy(_._2)(Ordering[Int].reverse)
        }.take(10)
    )

    resultRDD.collect().foreach(println)

    //TODO 3 关闭资源
    sc.stop()
  }
}

4 需求3:页面单跳转化率统计

4.1 需求分析

1)页面单跳转化率
计算页面单跳转化率,什么是页面单跳转换率,比如一个用户在一次 Session 过程中访问的页面路径 3,5,7,9,10,21,那么页面 3 跳到页面 5 叫一次单跳,7-9 也叫一次单跳,那么单跳转化率就是要统计页面点击的概率。
比如:计算 3-5 的单跳转化率,先获取符合条件的 Session 对于页面 3 的访问次数(PV)为 A,然后获取符合条件的 Session 中访问了页面 3 又紧接着访问了页面 5 的次数为 B,那么 B/A 就是 3-5 的页面单跳转化率。
在这里插入图片描述
2)统计页面单跳转化率意义
产品经理和运营总监,可以根据这个指标,去尝试分析,整个网站,产品,各个页面的表现怎么样,是不是需要去优化产品的布局;吸引用户最终可以进入最后的支付页面。
数据分析师,可以此数据做更深一步的计算和分析。
企业管理层,可以看到整个公司的网站,各个页面的之间的跳转的表现如何,可以适当调整公司的经营战略或策略。
3)需求详细描述
在该模块中,需要根据查询对象中设置的Session过滤条件,先将对应得Session过滤出来,然后根据查询对象中设置的页面路径,计算页面单跳转化率,比如查询的页面路径为:3、5、7、8,那么就要计算3-5、5-7、7-8的页面单跳转化率。
需要注意的一点是,页面的访问是有先后的,要做好排序。
1、2、3、4、5、6、7
1-2/ 1 2-3/2 3-4/3 4-5/4 5-6/5 6-7/6
4)需求分析
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.2 需求实现

1)代码实现

package com.atguigu.project01

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

object require03_PageFlow {
  def main(args: Array[String]): Unit = {
    //TODO 1 创建SparkConf配置文件,并设置App名称
    val conf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
    //TODO 2 利用SparkConf创建sc对象
    val sc = new SparkContext(conf)
    //1 读取数据
    val lineRDD: RDD[String] = sc.textFile("input\\user_visit_action.txt")
    //2 封装样例类 将lineRDD变为actionRDD
    val actionRDD: RDD[UserVisitAction] = lineRDD.map(
      line => {
        val datas: Array[String] = line.split("_")
        //将解析出来的数据封装到样例类里面
        UserVisitAction(
          datas(0),
          datas(1),
          datas(2),
          datas(3),
          datas(4),
          datas(5),
          datas(6),
          datas(7),
          datas(8),
          datas(9),
          datas(10),
          datas(11),
          datas(12)
        )
      }
    )
    //3 准备过滤数据
    //定义需要统计的页面 (只统计集合中规定的页面跳转率) 分母过滤
    val ids = List("1", "2", "3", "4", "5", "6", "7")
    //定义对分子过滤的集合
    val zipIds: List[String] = ids.zip(ids.tail).map {
      case (p1, p2) => p1 + "-" + p2
    }

    //对ids创建广播变量
    val bdIds = sc.broadcast(ids)

    //4 计算分母 (页面,访问次数)
    val idsArr = actionRDD
      //过滤出要统计的page_id(由于最后一个页面总次数,不参与运算,所以也过滤了)
      .filter(action => bdIds.value.init.contains(action.page_id))
      .map(action => (action.page_id, 1))
      .reduceByKey(_ + _).collect()

    //array转成Map方便后续使用
    val idsMap: Map[String, Int] = idsArr.toMap

    //5 计算分子
    //5.1 按照session进行分组
    val sessionGroupRDD: RDD[(String, Iterable[UserVisitAction])] = actionRDD.groupBy(_.session_id)
    //5.2 将分组后的数据根据时间进行排序(升序)
    val page2pageRDD: RDD[(String, List[String])] = sessionGroupRDD.mapValues(
      datas => {
        //1 将迭代器转成list,然后按照行动时间升序排序
        val actions: List[UserVisitAction] = datas.toList.sortBy(_.action_time)
        //2 转变数据结构,获取到pageId
        val pageidList: List[String] = actions.map(_.page_id)
        //3 根据排好序的页面List拉链获得单跳元组 (1,2,3,4) zip (2,3,4)
        val pageToPageList: List[(String, String)] = pageidList.zip(pageidList.tail)
        //4 再次转变结构 元组变字符串 (1,2) => 1-2
        val pageJumpCounts: List[String] = pageToPageList.map {
          case (p1, p2) => p1 + "-" + p2
        }
        //5 对分子也进行过滤下,提升效率 只保留1-2,2-3,3-4,4-5,5-6,6-7的数据
        pageJumpCounts.filter(
          str => zipIds.contains(str)
        )
      }
    )

    val pageFlowRDD: RDD[List[String]] = page2pageRDD.map(_._2)

    //5.3 聚合统计结果
    val reduceFlowRDD: RDD[(String, Int)] = pageFlowRDD.flatMap(list => list).map((_, 1)).reduceByKey(_ + _)

    //6 分母和分组全部搞定,计算页面单跳转换率
    reduceFlowRDD.foreach{
      case (pageflow,sum) => {
        val pageIds: Array[String] = pageflow.split("-")

        //根据pageIds(0) 取分母的值
        val pageIdSum: Int = idsMap.getOrElse(pageIds(0), 1)
        //页面单跳转换率 = 分子/分母
        println(pageflow + " = " + sum.toDouble / pageIdSum)
      }
    }
    
    //TODO 3 关闭资源
    sc.stop()
  }
}

总结

.对于spark实战难度为公司级别需求。对于需求要明确一下几点:
	(1).少走shuffle,尽量优化代码,提升性能。
    (2).公司开发一般实现需求是样例类
    (3)灵活运用缓存和广播变量
    (4)累加器一定要会写自定义累加器
    (5)处理问题尽量要细致,逻辑思维很重要
    
1.Top10热门品类
对于热门需要问清楚项目经理,公司热门具体指名什么,按照什顺序来排序的。
对于需求尽量做到:

2.Top10热门品类中每个品类的Top10活跃Session统计
在需求一的基础下实现Top10活跃Session统计

3.页面单跳转化率统计
需要问清楚项目经理,公司是按照PV还是UV来进行求页面跳转转化率
UV:店铺各bai页面的访问人数,一个IP在24小时内多次访问店铺被只算一次
PV:24小时内店铺内所有页面的浏览总量,可累加.(我们这个**案例是求的PV**)
IPV:指买家找到您店铺的宝贝后,点击进入宝贝详情页的次数,可累加。
IPV_UV是浏览过商品详情的独立访问者,注意:IPV_UV也是不能累加的。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
上百节课详细讲解,需要的小伙伴自行百度网盘下载,链接见附件,永久有效。 课程介绍: 讲解一个真实的、复杂的大型企业级大数据项目,是Spark的大型项目实战课程。 通过本套课程的学习,可以积累大量Spark项目经验,迈入Spark高级开发行列。 课程特色: 1、项目中全面覆盖了Spark CoreSpark SQL和Spark Streaming这三个技术框架几乎全部的初级和高级的技术点和知识点, 让学员学以致用,通过一套课程,即掌握如何将Spark所有的技术点和知识点应用在真实的项目中,来实现业务需求! 2、项目中的4个功能横块,全郃是实际企业项目中提取出来的,并进行技术整合和改良过的功能模块.全都是企业级的复杂和真实的需求,业务模块非常之复杂,绝对不是市面上的Dem级别的大数据项目能够想比拟的,学习过后,真正帮助学员增加实际 企业级项目的实战经验。 3、项目中通过实际的功能模块和业务场景,以及讲师曾经开发过的处理十亿、甚至百亿以上数据级别的SparK作业的经验积累,贯穿讲解了大量的高级复杂的性能调优技术和知识、troubleshooting解决线上报错和故障的经验、高端的全方位数据倾斜处理和解决方案.真正帮助学员掌握高精尖的Spark技术! 4、项目中采用完全还原企业大数据项目开发场景的方式来讲解,每一个业务模块的讲解都包括了需求分析、方案设计、数据设计、编码实现、功能测试、性能调优等环节,真实还原企业级大数据项目开发场景。 模块简介: 1、用户访问session分析,该模块主要是对用户访问session进行统计分析.包括session的聚合指标计算、 按时间比例随机抽取session、获取每天点击、下单和购买排名前10的品类、并获取top10品类的点击量排名前10的session.该模块可以让产品经理、数据分析师以及企业管理层形象地看到各种条件下的具体用户行为以及统计指标.从而对公司的产品设计以及业务发展战略做出调整.主要使用Spark Core实现. 2、页面单跳转化率统计,该模块主要是计算关键页面之间的单步跳转转化率,涉及到页面切片算法以及页面流匹配算法.该模块可以让产品经理、数据分析师以及企业管理层看到各个关键页面之间的转化率.从而对网页布局,进行更好的优化设计。主要使用Spark Core实现. 3、热门商品离线统计,该模块主要实现每天统计出各个区域的top3热门商品.然后使用Oozie进行离线统计任务的定时调度,使用Zeppeline进行数据可视化的报表展示.该模块可以让企业管理层看到公司售卖的 商品的整体情况,从而对公司的商品相关的战略进行调螫.主要使用Spark SQL实现。 4、广告流量实时统计.该模块负责实时统计公司的广告流量.包括广告展现流量和广告点击流量,实现动态黑名单机制以及黑名单过滤,实现滑动窗口内的各城市的广告展现流立和广告点击流直的统计,实现 每个区域诲个广告的点击流置实时统计,实现每个区域top3点击量的广告的统计,主要使用Spark Streaming实现.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值