Spark实战案例-需求1-Top10热门品类

上一篇:Spark教程-1.SparkCore

第5章 Spark案例实操

在之前的学习中,我们已经学习了Spark的基础编程方式,接下来,我们看看在实际的工作中如何使用这些API实现具体的需求。这些需求是电商网站的真实需求,所以在实现功能前,咱们必须先将数据准备好。

image-20210309181327882

上面的数据图是从数据文件中截取的一部分内容,表示为电商网站的用户行为数据,主要包含用户的4种行为:搜索,点击,下单,支付。数据规则如下:

  • 数据文件中每行数据采用下划线隔数据

  • 每一行数据表示用户的一次行为,这个行为只能是4种行为的一种

  • 如果搜索关键字为null,表示数据不是搜索数据

  • 如果点击的品类ID和产品ID为-1,表示数据不是点击数据

  • 针对于下单行为,一次可以下单多个商品,所以品类ID和产品ID可以是多个,id之间采用逗号分隔,如果本次不是下单行为,则数据采用null表示

  • 支付行为和下单行为类似

详细字段说明:

编号字段名称字段类型字段含义
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

样例类:

//用户访问动作表
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
)

6.1 需求1:Top10热门品类

image-20210309103118937

6.1.1 需求说明

品类是指产品的分类,大型电商网站品类分多级,咱们的项目中品类只有一级,不同的公司可能对热门的定义不一样。我们按照每个品类的点击、下单、支付的量来统计热门品类。

鞋 点击数 下单数 支付数

衣服 点击数 下单数 支付数

电脑 点击数 下单数 支付数

例如,综合排名 = 点击数20%+下单数30%+支付数*50%

本项目需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。

6.1.2 实现方案一

6.1.2.1 需求分析

分别统计每个品类点击的次数,下单的次数和支付的次数:

(品类,点击总数)(品类,下单总数)(品类,支付总数)

6.1.2.2 需求实现
package com.atguigu.bigdata.spark.core.rdd.req

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

object Req1_HotCategoryTop10Analysis {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
    val sc = new SparkContext(sparkConf)
    // TODO 1.获取原始数据
    val actionRDD = sc.textFile("datas/user_visit_action.txt")
    // TODO 2.分别统计一个品类的点击数量,下单数量,支付数量
    // 2.1 品类的点击数量的统计
    // 保留点击数据,其他数据不要
    val clickActionRDD = actionRDD.filter(
      action => {
        val datas = action.split("_")
        datas(6) != "-1"
      }
    )
    //(品类ID,1 click)=> (品类ID,sum click)
    val clickRDD = clickActionRDD.map(
      action => {
        val datas = action.split("_")
        (datas(6), 1)
      }
    )
    // (品类ID,sum click)
    val totalClickRDD = clickRDD.reduceByKey(_ + _)

    // 2.2 品类的下单数量的统计
    // 保留下单数据,其他数据不要
    val orderActionRDD = actionRDD.filter(
      action => {
        val datas = action.split("_")
        datas(8) != "null"
      }
    )
    // (品类ID,1 order)=> (品类ID,sum order)
    val orderRDD = orderActionRDD.flatMap(
      action => {
        val datas = action.split("_")
        val categoryids = datas(8)
        val ids = categoryids.split(",")
        ids.map((_, 1))
      }
    )
    // (品类ID,sum order)
    val totalOrderRDD = orderRDD.reduceByKey(_ + _)

    // 2.3 品类的支付数量的统计
    val payActionRDD = actionRDD.filter(
      action => {
        val datas = action.split("_")
        datas(10) != "null"

      }
    )
    // (品类ID,1 pay)=> (品类ID,sum pay)
    val payRDD = payActionRDD.flatMap(
      action => {
        val datas = action.split("_")
        val categoryids = datas(10)
        val ids = categoryids.split(",")
        ids.map((_, 1))
      }
    )
    // (品类ID,sum pay)
    val totalPayRDD = payRDD.reduceByKey(_ + _)

    // TODO 3. 将统计结果进行排行(点击,下单,支付)
    // Scala可以将无关的数据封装为一个整体来使用,可以使用元组
    // 元组在scala中的排序方式,首先按照第一个元素排序,相同者,按照第二个元素排序,依此类推

    // (品类ID,(点击数量,下单数量,支付数量))
    val clickLeftJoinOrderRDD: RDD[(String, (Int, Option[Int]))] = totalClickRDD.leftOuterJoin(totalOrderRDD)
    val clickOrderPayRDD: RDD[(String, ((Int, Option[Int]), Option[Int]))] = clickLeftJoinOrderRDD.leftOuterJoin(totalPayRDD)

    // ((Int, Option[Int]), Option[Int]))
    // ((点击,下单),支付)
    val wantRDD = clickOrderPayRDD.map {
      case (cid, ((click, optOrder), optPay)) => {
        val order = optOrder.getOrElse(0)
        val pay = optPay.getOrElse(0)
        (cid, ((click, order), pay))
      }
    }

    // TODO 4.  取前10名
    //根据(click, order), pay)排序,false是降序,take取前10
    val result: Array[(String, ((Int, Int), Int))] = wantRDD.sortBy(_._2, false).take(10)
    result.foreach(println)
    sc.stop()

  }
}


6.1.3 实现方案二

6.1.3.1 需求分析

一次性统计每个品类点击的次数,下单的次数和支付的次数:

(品类,(点击总数,下单总数,支付总数))

6.1.3.2 需求实现
package com.atguigu.bigdata.spark.core.rdd.req

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


object Req1_HotCategoryTop10Analysis2 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
    val sc = new SparkContext(sparkConf)

    // TODO 1. 获取原始数据
    val actionRDD = sc.textFile("datas/user_visit_action.txt")
    actionRDD.cache()

    // TODO 2. 分别统计一个品类的点击数量,下单数量,支付数量

    // Q : actionRDD,读取文件中,重复使用?
    // A : 即使对象重复使用,但是数据无法重复使用,因为RDD不保存数据,从头执行数据操作
    //     可以采用持久化操作
    // Q : reduceByKey算子被调用的次数过多,性能下降。?
    // A : 极限情况下,使用一个reduceByKey实现功能
    // Q : 连接3个数据源时,会导致性能下降?
    // A : 改变数据结构,提升计算性能

    //点击
    val clickActionRDD = actionRDD.filter(
      action => {
        val datas: Array[String] = action.split("_")
        datas(6) != "-1"
      }
    )

    val clickRDD = clickActionRDD.map(
      action => {
        val datas: Array[String] = action.split("_")
        ( datas(6), 1 )
      }
    )
    // (品类ID,1)=> ( 品类,(1,0,0) )
    // reduceByKey丢弃???
    val totalClickRDD = clickRDD.reduceByKey(_+_)

    // 订单
    val orderActionRDD = actionRDD.filter(
      action => {
        val datas: Array[String] = action.split("_")
        datas(8) != "null"
      }
    )

    val orderRDD = orderActionRDD.flatMap(
      action => {
        val datas: Array[String] = action.split("_")
        val categoryids = datas(8)
        val ids = categoryids.split(",")
        ids.map( (_, 1) )
      }
    )

    // (品类ID,1) => (品类,(0,1,0))
    // reduceByKey是否丢弃???
    val totalOrderRDD: RDD[(String, Int)] = orderRDD.reduceByKey(_+_)

    //支付
    val payActionRDD = actionRDD.filter(
      action => {
        val datas: Array[String] = action.split("_")
        datas(10) != "null"
      }
    )
    val payRDD = payActionRDD.flatMap(
      action => {
        val datas: Array[String] = action.split("_")
        val categoryids = datas(10)
        val ids = categoryids.split(",")
        ids.map( (_, 1) )
      }
    )

    // (品类ID, 1)=> (品类,(0,0,1))
    // reduceByKey丢弃???
    val totalPayRDD: RDD[(String, Int)] = payRDD.reduceByKey(_+_)

    // TODO 3. 将统计结果进行排行(降序)( 点击, 下单, 支付 )
    // ( 品类ID, (点击数量, 0, 0) )
    val newClickRDD = totalClickRDD.map{
      case ( id, clickCnt ) => {
        ( id, (clickCnt, 0, 0) )
      }
    }
    // ( 品类ID, (0, 下单数量, 0) )
    val newOrderRDD = totalOrderRDD.map {
      case ( id, orderCnt ) => {
        ( id, (0, orderCnt, 0) )
      }
    }
    // ( 品类ID, (0, 0, 支付数量) )
    val newPayRDD = totalPayRDD.map {
      case ( id, payCnt ) => {
        ( id, (0, 0, payCnt) )
      }
    }
    // (1, (1,0,0))
    // (1, (0,2,0))
    // (1, (0,0,3))
    // => (1, (1,2,3))
    // union
    val rdd: RDD[(String, (Int, Int, Int))] = newClickRDD.union(newOrderRDD).union(newPayRDD)
    // ( 品类ID, ( 点击数量,下单数量,支付数量 ) )
    val wantRDD = rdd.reduceByKey{
      case ( (c1, o1, p1), (c2, o2, p2) ) => {
        ( c1 + c2, o1 + o2, p1 + p2 )
      }
    }
    // TODO 4. 取前10名
    val result: Array[(String, (Int, Int, Int))] = wantRDD.sortBy(_._2, false).take(10)

    result.foreach(println)
    sc.stop

  }
}

方案二优化:

package com.atguigu.bigdata.spark.core.rdd.req

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

object Req1_HotCategoryTop10Analysis3 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
    val sc = new SparkContext(sparkConf)

    // TODO 1. 获取原始数据
    val actionRDD = sc.textFile("datas/user_visit_action.txt")
    // TODO 2. 将原始数据进行结构的转换
    //点击 => ( 品类, (1,0,0) )
    //下单 => ( 品类1,(0,1,0)),  ( 品类2,(0,1,0))
    //支付 => (品类1,(0,0,1)), (品类2,(0,0,1))

    // map : 数据不会增多,只是 A => B
    // flatMap : 数据可能会增多
    val flatRDD: RDD[(String, (Int, Int, Int))] = actionRDD.flatMap(
      action => {
        val datas = action.split("_")
        if (datas(6) != "-1") {
          // 点击
          List((datas(6), (1, 0, 0)))
        } else if ((datas(8) != "null")) {
          // 下单
          List((datas(8), (0, 1, 0)))
        } else if ((datas(10) != "null")) {
          // 支付
          List((datas(10), (0, 0, 1)))
        } else {
          Nil
        }
      }
    )
    val resultRDD: RDD[(String, (Int, Int, Int))] = flatRDD.reduceByKey {
      case ((c1, o1, p1), (c2, o2, p2)) => {
        (c1 + c2, o1 + o2, p1 + p2)
      }
    }
    resultRDD.sortBy(_._2, false).take(10).foreach(println)
    
    sc.stop()

  }
}

6.1.4 实现方案三

6.1.4.1 需求分析

使用累加器的方式聚合数据

6.1.4.2 需求实现
package com.atguigu.bigdata.spark.core.rdd.req


import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable

object Req1_HotCategoryTop10Analysis4 {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setMaster("local").setAppName("RDDCreate")
    val sc = new SparkContext(sparkConf)

    // TODO 1. 获取原始数据
    val actionRDD = sc.textFile("datas/user_visit_action.txt")

    // 不使用shuffle操作完成数据统计
    // 创建累加器
    val acc = new HotCategoryAccumulator()
    sc.register(acc, "hotCategory")

    actionRDD.foreach(
      action => {
        val datas = action.split("_")
        if (datas(6) != "-1") {
          //点击
          acc.add(datas(6), "click") //click 加了一次
        } else if (datas(8) != "null") {
          //下单
          val ids = datas(8).split(",") //下单有多个
          ids.foreach( //循环
            id => {
              acc.add((id, "order")) //增加每个下单
            }
          )
        } else if (datas(10) != "null") {
          //支付
          val ids = datas(10).split(",") //支付有多个
          ids.foreach( //循环
            id => {
              acc.add((id, "pay")) //增加每个支付
            }
          )
        }
      }
    )

    val hotCategoryMap: mutable.Map[String, HotCategory] = acc.value //拿到map的结果

    // TODO 将累加的结果进行排序(降序)和取前10名
    // Map[String, HotCategory] 只需要 HotCategory
    // map没有排序功能,需要toList
    // sortWith,这里不是tuple,是一个一个的对象,对象需要用自定义的规则,sortWith有左右
    hotCategoryMap.map(_._2).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) //取前10


    sc.stop()

  }

  // 输入IN: (品类,行为类型)
  // 输出OUT: Map[品类,HotCategory] , HotCategory中封装(点击数量,下单数量,支付数量)

  class HotCategoryAccumulator extends AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] {
    private val hotCategoryMap = mutable.Map[String, HotCategory]()


    override def isZero: Boolean = {
      hotCategoryMap.isEmpty //初始化,就是没有值
    }

    override def copy(): AccumulatorV2[(String, String), mutable.Map[String, HotCategory]] = {
      new HotCategoryAccumulator()
    }

    override def reset(): Unit = {
      hotCategoryMap.clear() //清除
    }


    override def add(v: (String, String)): Unit = {
      val id = v._1 //品类id
      val actionType = v._2 //行为类型

      // 判断map里面有没有这个id,有就拿过来,没有就用新的
      val hotCategory = hotCategoryMap.getOrElse(id, HotCategory(id, 0, 0, 0))

      // 行为类型之中有三种,用match匹配
      actionType match {
        case "click" => hotCategory.clickCount += 1 // 是哪种类型就加1
        case "order" => hotCategory.orderCount += 1
        case "pay" => hotCategory.payCount += 1
      }
      // 增加:从之前的map中取出旧的值,根据条件进行更新,更新后再放回去
      // 把这个map更新
      hotCategoryMap.update(id, hotCategory)
    }

    // merge 就是两个map的合并,合并当前和其他的
    override def merge(other: AccumulatorV2[(String, String), mutable.Map[String, HotCategory]]): Unit = {
      other.value.foreach { // 这个value是map:Map[String, HotCategory]
        case (id, otherHC) => {
          val thisHC = hotCategoryMap.getOrElse(id, HotCategory(id, 0, 0, 0))

          thisHC.clickCount += otherHC.clickCount
          thisHC.orderCount += otherHC.orderCount
          thisHC.payCount += otherHC.payCount

          // 更新一下
          hotCategoryMap.update(id, thisHC)

        }
      }
    }

    // 返回
    override def value: mutable.Map[String, HotCategory] = hotCategoryMap
  }

  // 样例类
  case class HotCategory(
                          var categoryId: String,
                          var clickCount: Long,
                          var orderCount: Long,
                          var payCount: Long,
                        )

}


测试数据百度网盘链接: 链接:https://pan.baidu.com/s/1t-4PeV8hiGpHYs_17LCFUw?pwd=aza1 提取码:aza1

上一篇:Spark教程-1.SparkCore

  • 5
    点赞
  • 86
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工藤-新二

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值