SparkCore之电商用户行为数据分析项目实战

SparkCore之电商用户行为数据分析项目实战

  • 1. 数据准备
    • 1)数据集
    • 2)数据格式说明
    • 3)数据详细字段说明
  • 2. 需求一:Top10热门品类
    • 1)需求说明
    • 2)需求分析
    • 3)代码实现
  • 3. 需求二:Top10热门品类中每个品类的Top10活跃Session统计
    • 1)需求说明
    • 2)需求分析
    • 3)代码实现
  • 4. 需求三:页面单跳转化率统计
    • 1)需求说明
    • 2)需求分析
    • 3)代码实现

1. 数据准备

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

1)数据集

SparkCore项目实战中用到的数据集

2)数据格式说明

SparkCore之电商用户行为数据分析项目实战_第1张图片
(1)数据采用 _ 分割字段;
(2)每一行表示用户的一个行为,所以每一行只能是四种行为中的一种;
(3)如果搜索关键字是null,表示这次不是搜索;
(4)如果点击的品类id和产品id是-1,表示这次不是点击;
(5)下单行为来说一次可以下单多个产品,所以品类id和产品id都是多个,id之间使用 ,(逗号)分割;如果本次不是下单行为,则他们相关数据用null来表示;
(6)支付行为和下单行为类似。

3)数据详细字段说明

SparkCore之电商用户行为数据分析项目实战_第2张图片

2. 需求一:Top10热门品类

1)需求说明

  • 品类是指产品的分类,大型电商网站品类分多级,咱们的项目中品类只有一级,不同的公司可能对热门的定义不一样。我们按照每个品类的点击、下单、支付的量来统计热门品类。
    SparkCore之电商用户行为数据分析项目实战_第3张图片
  • 例如,综合排名 = 点击数 * 20% + 下单数 * 30% + 支付数 * 50%
  • 本项目需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。

2)需求分析

SparkCore之电商用户行为数据分析项目实战_第4张图片

3)代码实现

  • 思路:分别统计每个品类点击的次数,下单的次数和支付的次数。
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ListBuffer


object Spark01_TopN_req1 {
  def main(args: Array[String]): Unit = {
    //创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

    //创建SparkContext,该对象是提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)

    //1.读取数据,创建RDD
    val dataRDD: RDD[String] = sc.textFile("D:\MyWork\IdeaProjects\spark0105_exer\src\main\input\user_visit_action.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)
    //(保健品,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))
        } else if (userAction.order_category_ids != "null") {
          //坑:读取的文件应该是null字符串,而不是null对象
          //判断是否为下单行为,如果是下单行为,需要对当前订单中涉及的所有品类Id进行切分
          val ids: Array[String] = userAction.order_category_ids.split(",")
          //定义一个集合,用于存放多个品类id封装的输出结果对象
          val categoryCountInfoList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          //对所有品类的id进行遍历
          for (id <- ids) {
            categoryCountInfoList.append(CategoryCountInfo(id, 0, 1, 0))
          }
          categoryCountInfoList
        } else if (userAction.pay_category_ids != "null") {
          //支付
          val ids: Array[String] = userAction.pay_category_ids.split(",")
          //定义一个集合,用于存放多个品类id封装的输出结果对象
          val categoryCountInfoList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          //对所有品类的id进行遍历
          for (id <- ids) {
            categoryCountInfoList.append(CategoryCountInfo(id, 0, 0, 1))
          }
          categoryCountInfoList
        } else {
          Nil
        }

      }
    }

    //4.将相同品类的放到一组
    val groupRDD: RDD[(String, Iterable[CategoryCountInfo])] = infoRDD.groupBy(_.categoryId)

    //5.将分组之后的数据进行聚合处理    (鞋,100,90,80)
    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
          }
        }
      }
    }
    //6.对上述RDD的结构进行转换,只保留value部分  ,得到聚合之后的RDD[CategoryCountInfo]
    val mapRDD: RDD[CategoryCountInfo] = reduceRDD.map(_._2)

    //7.对RDD中的数据排序,取前10
    val res: Array[CategoryCountInfo] = mapRDD.sortBy(info=>(info.clickCount,info.orderCount,info.payCount),false).take(10)

    //8.打印输出
    res.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)//支付次数

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

1)需求说明

  • 对于排名前10的品类,分别获取每个品类点击次数排名前10的sessionId。(注意: 这里我们只关注点击次数,不关心下单和支付次数)
  • 这个就是说,对于top10的品类,每一个都要获取对它点击次数排名前10的sessionId。这个功能,可以让我们看到,对某个用户群体最感兴趣的品类,各个品类最感兴趣最典型的用户的session的行为。

2)需求分析

  • 通过需求1,获取TopN热门品类的id
  • 将原始数据进行过滤(1.保留热门品类 2.只保留点击操作)
  • 对session的点击数进行转换 (category-session,1)
  • 对session的点击数进行统计 (category-session,sum)
  • 将统计聚合的结果进行转换 (category,(session,sum))
  • 将转换后的结构按照品类进行分组 (category,Iterator[(session,sum)])
  • 对分组后的数据降序 取前10
    SparkCore之电商用户行为数据分析项目实战_第5张图片

3)代码实现

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


object Spark02_TopN_req2 {
  def main(args: Array[String]): Unit = {
    //创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

    //创建SparkContext,该对象是提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)

    //1.读取数据,创建RDD
    val dataRDD: RDD[String] = sc.textFile("D:\MyWork\IdeaProjects\spark0105_exer\src\main\input\user_visit_action.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)
    //(保健品,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))
        } else if (userAction.order_category_ids != "null") {
          //坑:读取的文件应该是null字符串,而不是null对象
          //判断是否为下单行为,如果是下单行为,需要对当前订单中涉及的所有品类Id进行切分
          val ids: Array[String] = userAction.order_category_ids.split(",")
          //定义一个集合,用于存放多个品类id封装的输出结果对象
          val categoryCountInfoList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          //对所有品类的id进行遍历
          for (id <- ids) {
            categoryCountInfoList.append(CategoryCountInfo(id, 0, 1, 0))
          }
          categoryCountInfoList
        } else if (userAction.pay_category_ids != "null") {
          //支付
          val ids: Array[String] = userAction.pay_category_ids.split(",")
          //定义一个集合,用于存放多个品类id封装的输出结果对象
          val categoryCountInfoList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
          //对所有品类的id进行遍历
          for (id <- ids) {
            categoryCountInfoList.append(CategoryCountInfo(id, 0, 0, 1))
          }
          categoryCountInfoList
        } else {
          Nil
        }

      }
    }

    //4.将相同品类的放到一组
    val groupRDD: RDD[(String, Iterable[CategoryCountInfo])] = infoRDD.groupBy(_.categoryId)

    //5.将分组之后的数据进行聚合处理    (鞋,100,90,80)
    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
          }
        }
      }
    }
    //6.对上述RDD的结构进行转换,只保留value部分  ,得到聚合之后的RDD[CategoryCountInfo]
    val mapRDD: RDD[CategoryCountInfo] = reduceRDD.map(_._2)

    //7.对RDD中的数据排序,取前10
    val res: Array[CategoryCountInfo] = mapRDD.sortBy(info=>(info.clickCount,info.orderCount,info.payCount),false).take(10)

    //8.打印输出
    //res.foreach(println)

    //===============================需求二===================================
    //1.获取热门品类top10的品类id
    val ids: Array[String] = res.map(_.categoryId)

    //ids可以进行优化, 因为发送给Excutor中的Task使用,每一个Task都会创建一个副本,所以可以使用广播变量
    val broadcastIds: Broadcast[Array[String]] = sc.broadcast(ids)
    //2.将原始数据进行过滤(1.保留热门品类 2.只保留点击操作)
    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
        }
      }
    }

    //3.对session的点击数进行转换 (category_session,1)
    val mapRDD1: RDD[(String, Int)] = filterRDD.map {
      action => {
        (action.click_category_id + "_" + action.session_id, 1)
      }
    }

    //4.对session的点击数进行统计 (category_session,sum)
    val reduceRDD1: RDD[(String, Int)] = mapRDD1.reduceByKey(_+_)

    //5.将统计聚合的结果进行转换  (category,(session,sum))
    val mapRDD2: RDD[(String, (String, Int))] = reduceRDD1.map {
      case (categoryAndSession, sum) => {
        (categoryAndSession.split("_")(0), (categoryAndSession.split("_")(1), sum))
      }
    }
    //6.将转换后的结构按照品类进行分组 (category,Iterator[(session,sum)])
    val groupRDD2: RDD[(String, Iterable[(String, Int)])] = mapRDD2.groupByKey()

    //7.对分组后的数据降序 取前10
    val resRDD: RDD[(String, List[(String, Int)])] = groupRDD2.mapValues {
      datas => {
        datas.toList.sortWith {
          case (left, right) => {
            left._2 > right._2
          }
        }.take(10)
      }
    }
    resRDD.foreach(println)

    // 关闭连接
    sc.stop()
  }
}

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

1)需求说明

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

2)需求分析

  • 读取原始数据
  • 将原始数据映射为样例类
  • 将原始数据根据session进行分组
  • 将分组后的数据根据时间进行排序(升序)
  • 将排序后的数据进行结构的转换(pageId,1)
  • 计算分母-将相同的页面id进行聚合统计(pageId,sum)
  • 计算分子-将页面id进行拉链,形成连续的拉链效果,转换结构(pageId-pageId2,1)
  • 将转换结构后的数据进行聚合统计(pageId-pageId2,sum)
  • 计算页面单跳转换率
    SparkCore之电商用户行为数据分析项目实战_第6张图片

3)代码实现

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


object Spark03_TopN_req3 {
  def main(args: Array[String]): Unit = {
    //创建SparkConf并设置App名称
    val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

    //创建SparkContext,该对象是提交Spark App的入口
    val sc: SparkContext = new SparkContext(conf)

    //1.读取数据,创建RDD
    val dataRDD: RDD[String] = sc.textFile("E:\\Felix课程\\大数据\\大数据_200105\\Felix_02_尚硅谷大数据技术之Spark\\2.资料\\spark-core数据\\user_visit_action.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
        )
      }
    }

    //========================================需求三 ========================================
    //1.对当前日志中记录的访问页面进行计数
    val pageIdRDD: RDD[(Long, Long)] = actionRDD.map {
      action => {
        (action.page_id, 1L)
      }
    }
    //2.通过页面的计数,计算每一个页面出现的总次数    作为求单跳转换率的分母
    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  下单
    */
    //3.计算分子
    //3.1 将原始数据根据sessionId进行分组
    val sessionRDD: RDD[(String, Iterable[UserVisitAction])] = actionRDD.groupBy(_.session_id)
    //3.2 将分组后的数据按照时间进行升序排序
    val pageFlowRDD: RDD[(String, List[(String, Int)])] = sessionRDD.mapValues {
      datas => {
        //得到排序后的同一个session的用户访问行为
        val userActions: List[UserVisitAction] = datas.toList.sortWith {
          (left, right) => {
            left.action_time < right.action_time
          }
        }
        //3.3 对排序后的用户访问行为进行结构转换,只保留页面就可以
        val pageIdsList: List[Long] = userActions.map(_.page_id)
        //A->B->C->D->E->F
        //B->C->D->E->F
        //3.4 对当前会话用户访问页面 进行拉链  ,得到页面的流转情况 (页面A,页面B)
        val pageFlows: List[(Long, Long)] = pageIdsList.zip(pageIdsList.tail)
        //3.5 对拉链后的数据,进行结构的转换 (页面A-页面B,1)
        pageFlows.map {
          case (pageId1, pageId2) => {
            (pageId1 + "-" + pageId2, 1)
          }
        }

      }
    }
    // 3.6 将每一个会话的页面跳转统计完毕之后,没有必要保留会话信息了,所以对上述RDD的结构进行转换
    //只保留页面跳转以及计数
    val pageFlowMapRDD: RDD[(String, Int)] = pageFlowRDD.map(_._2).flatMap(list=>list)

    //3.7 对页面跳转情况进行聚合操作
    val pageAToPageBSumRDD: RDD[(String, Int)] = pageFlowMapRDD.reduceByKey(_+_)

    //4.页面单跳转换率计算
    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()
  }
}

你可能感兴趣的:(Spark)