1 数据准备
- 本项目的数据是采集电商网站的用户行为数据,主要包含用户的4种行为:搜索、点击、下单和支付。
1.1 数据格式
- 数据采用_分割字段
- 每一行表示用户的一个行为,所以每一行只能是四种行为中的一种。
- 如果搜索关键字是null,表示这次不是搜索
- 如果点击的品类id和产品id是-1表示这次不是点击
- 下单行为来说一次可以下单多个产品,所以品类id和产品id都是多个,id之间使用逗号,分割。如果本次不是下单行为,则他们相关数据用null来表示
- 支付行为和下单行为类似
1.2 数据详细字段说明
编号 | 字段名称 | 字段类型 | 字段含义 |
1 | date | String | 用户点击行为的日期 |
2 | user_id | Long | 用户的ID |
3 | session_id | String | Session的ID |
4 | page_id | Long | 某个页面的ID |
5 | action_time | String | 动作的时间点 |
6 | search_keyword | String | 用户搜索的关键词 |
7 | click_category_id | Long | 某一个商品品类的ID |
8 | click_product_id | Long | 某一个商品的ID |
9 | order_category_ids | String | 一次订单中所有品类的ID集合 |
10 | order_product_ids | String | 一次订单中所有商品的ID集合 |
11 | pay_category_ids | String | 一次支付中所有品类的ID集合 |
12 | pay_product_ids | String | 一次支付中所有商品的ID集合 |
13 | city_id | Long | 城市 id |
2 需求一:Top10热门品类
- 需求说明:品类是指产品的分类,大型电商网站品类分多级,咱们的项目中品类只有一级,不同的公司可能对热门的定义不一样。我们按照每个品类的点击、下单、支付的量来统计热门品类。
-
本项目需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。
2.1 需求一分析
- 思路一:分别统计每个品类点击的次数,下单的次数和支付的次数
2.2 代码实现一
-
核心代码实现
package com.spark.day07
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ListBuffer
/*
* 需求一、统计热门品类TopN
*
* */
object Spark01_TopN_req1 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Spark Project").setMaster("local[*]")
val sc = new SparkContext(conf)
// 读取数据创建RDD
val dataRDD: RDD[String] = sc.textFile("/Users/tiger/Desktop/study/001大数据学习/14,Spark/2.资料/spark-core数据/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 infoRDD: RDD[CategoryCountInfo] = actionRDD.flatMap {
userAction => {
// 判断是否为点击行为
if (userAction.click_category_id != -1) {
// 封装输出结果对象
List(CategoryCountInfo(userAction.click_category_id.toString, 1, 0, 0))
} else if (userAction.order_category_ids != "null") { // 坑:读取的文件应该是null字符串,而不是null关键字
// 判断是否是为下单行为,如果是下单行为,需要对当前订单中设计的所有品类id进行切分
val ids: Array[String] = userAction.order_category_ids.split(",")
// 定义一个集合,用于存放多个品类id封装的输出结果对象
val categoryCountInfoeList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
// 对所有品类的id进行遍历
for (id <- ids) {
categoryCountInfoeList.append(CategoryCountInfo(id, 0, 1, 0))
}
categoryCountInfoeList
} else if (userAction.pay_category_ids != "null") {
// 支付
val ids: Array[String] = userAction.pay_category_ids.split(",")
// 定义一个集合,用于存放多个品类id封装的输出结果对象
val categoryCountInfoeList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
// 对所有品类的id进行遍历
for (id <- ids) {
categoryCountInfoeList.append(CategoryCountInfo(id, 0, 0, 1))
}
categoryCountInfoeList
} else {
Nil
}
}
}
// 将相同品类的放到一组
val groupRDD: RDD[(String, Iterable[CategoryCountInfo])] = infoRDD.groupBy(_.categoryId)
// 将分组之后的数据进行处理
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
}
}
}
}
// 对上述RDD的结构进行转换,只保留value部分,得到聚合之后的CategoryCountInfo
val mapRDD: RDD[CategoryCountInfo] = reduceRDD.map(_._2)
// 对RDD中数据进行排序取前10
val res: Array[CategoryCountInfo] = mapRDD.sortBy(info => (info.clickCount, info.orderCount, info.payCount), false).take(10)
// 打印输出
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(var categoryId: String,//品类id
var clickCount: Long,//点击次数
var orderCount: Long,//订单次数
var payCount: Long)//支付次数
3 需求2:Top10热门品类中每个品类的Top10活跃Session统计
3.1 需求分析
- 对于排名前10的品类,分别获取每个品类点击次数排名前10的sessionId。(注意: 这里我们只关注点击次数,不关心下单和支付次数)这个就是说,对于top10的品类,每一个都要获取对它点击次数排名前10的sessionId。这个功能,可以让我们看到,对某个用户群体最感兴趣的品类,各个品类最感兴趣最典型的用户的session的行为。
- 分析思路
- 通过需求1,获取TopN热门品类的id
- 将原始数据进行过滤(1.保留热门品类 2.只保留点击操作)
- 对session的点击数进行转换 (category-session,1)
- 对session的点击数进行统计 (category-session,sum)
- 将统计聚合的结果进行转换 (category,(session,sum))
- 将转换后的结构按照品类进行分组 (category,Iterator[(session,sum)])
- 对分组后的数据降序 取前10
3.2 代码实现
package com.spark.day07
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable.ListBuffer
/*
* 需求一、统计热门品类TopN
*
* */
object Spark02_TopN_req2 {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Spark Project").setMaster("local[*]")
val sc = new SparkContext(conf)
// 读取数据创建RDD
val dataRDD: RDD[String] = sc.textFile("/Users/tiger/Desktop/study/001大数据学习/14,Spark/2.资料/spark-core数据/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 infoRDD: RDD[CategoryCountInfo] = actionRDD.flatMap {
userAction => {
// 判断是否为点击行为
if (userAction.click_category_id != -1) {
// 封装输出结果对象
List(CategoryCountInfo(userAction.click_category_id.toString, 1, 0, 0))
} else if (userAction.order_category_ids != "null") { // 坑:读取的文件应该是null字符串,而不是null关键字
// 判断是否是为下单行为,如果是下单行为,需要对当前订单中设计的所有品类id进行切分
val ids: Array[String] = userAction.order_category_ids.split(",")
// 定义一个集合,用于存放多个品类id封装的输出结果对象
val categoryCountInfoeList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
// 对所有品类的id进行遍历
for (id <- ids) {
categoryCountInfoeList.append(CategoryCountInfo(id, 0, 1, 0))
}
categoryCountInfoeList
} else if (userAction.pay_category_ids != "null") {
// 支付
val ids: Array[String] = userAction.pay_category_ids.split(",")
// 定义一个集合,用于存放多个品类id封装的输出结果对象
val categoryCountInfoeList: ListBuffer[CategoryCountInfo] = ListBuffer[CategoryCountInfo]()
// 对所有品类的id进行遍历
for (id <- ids) {
categoryCountInfoeList.append(CategoryCountInfo(id, 0, 0, 1))
}
categoryCountInfoeList
} else {
Nil
}
}
}
// 将相同品类的放到一组
val groupRDD: RDD[(String, Iterable[CategoryCountInfo])] = infoRDD.groupBy(_.categoryId)
// 将分组之后的数据进行处理
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
}
}
}
}
// 对上述RDD的结构进行转换,只保留value部分,得到聚合之后的CategoryCountInfo
val mapRDD: RDD[CategoryCountInfo] = reduceRDD.map(_._2)
// 对RDD中数据进行排序取前10
val res: Array[CategoryCountInfo] = mapRDD.sortBy(info => (info.clickCount, info.orderCount, info.payCount), false).take(10)
//需求二实现//
// 获取热门品类Top10的品类id
val ids: Array[String] = res.map(_.categoryId)
println(ids.mkString(","))
// 通过需求1,获取TopN热门品类的id
// ids可以进行优化,因为发送给Excutor中的Task使用,每一个Task都会创建一个副本,所以可以使用广播变量
val broadcastIds: Broadcast[Array[String]] = sc.broadcast(ids)
// 将原始数据进行过滤(1.保留热门品类 2.只保留点击操作)
val filterRDD: RDD[UserVisitAction] = actionRDD .filter {
action => {
// 只保留点击行为
if (action.click_category_id != -1) {
// 同时确定是热门品类的点击
// 坑,集合数据是String类型,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 reduceRDD1: RDD[(String, Int)] = mapRDD1.reduceByKey(_ + _)
// 将统计聚合的结果进行转换 (category,(session,sum))
val mapRDD2: RDD[(String, (String, Int))] = reduceRDD1.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 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 需求3:页面单跳转化率统计
4.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 的页面单跳转化率.
-
在该模块中,需要根据查询对象中设置的 Session 过滤条件,先将对应得 Session 过滤出来,然后根据查询对象中设置的页面路径,计算页面单跳转化率,比如查询的页面路径为:3、5、7、8,那么就要计算 3-5、5-7、7-8 的页面单跳转化率。需要注意的一点是,页面的访问时有先后的,要做好排序。
-
思路分析
-
读取原始数据
- 将原始数据映射为样例类
- 将原始数据根据session进行分组
- 将分组后的数据根据时间进行排序(升序)
- 将排序后的数据进行结构的转换(pageId,1)
- 计算分母-将相同的页面id进行聚合统计(pageId,sum)
- 计算分子-将页面id进行拉链,形成连续的拉链效果,转换结构(pageId-pageId2,1)
- 将转换结构后的数据进行聚合统计(pageId-pageId2,sum)
- 计算页面单跳转换率
-
4.2 代码实现
package com.spark.day07
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 = {
val conf: SparkConf = new SparkConf().setAppName("Spark Project").setMaster("local[*]")
val sc = new SparkContext(conf)
// 读取数据创建RDD
val dataRDD: RDD[String] = sc.textFile("/Users/tiger/Desktop/study/001大数据学习/14,Spark/2.资料/spark-core数据/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 pageIdRDD: RDD[(Long, Long)] = actionRDD.map {
action => {
(action.page_id, 1L)
}
}
// 通过页面计数,计算每一个页面出现的总的次数,作为单跳转换率的分母
val fmIdsMap: Map[Long, Long] = pageIdRDD.reduceByKey(_ + _).collect().toMap
// 计算分子
//将原始数据根据session进行分组
val sessionRDD: RDD[(String, Iterable[UserVisitAction])] = actionRDD.groupBy(_.session_id)
// 把分组之后的数据按照时间进行升序排序
val pageFlowRDD: RDD[(String, List[(String, Int)])] = sessionRDD.mapValues {
datas => {
// 得到排序后的同一个session的用户访问行为
val userActions: List[UserVisitAction] = datas.toList.sortWith {
(lefe, right) => {
lefe.action_time < right.action_time
}
}
// 对排序后的用户访问行为进行结构转换,只保留页面就可以
val pageIdList: List[Long] = userActions.map(_.page_id)
// 对当前绘画用户访问页面进行拉链,得到页面的流转情况(页面A,页面B)
val pageFlows: List[(Long, Long)] = pageIdList.zip(pageIdList.tail)
// 对拉链后的数据,进行结构的转换(页面A-页面B, 1)
pageFlows.map {
case (pageId1, pageId2) => {
(pageId1 + "-" + pageId2, 1)
}
}
}
}
// 将每一个会话的页面跳转完毕之后,没有必要保留会话信息了,所以对上述RDD的结构进行转换
// 只保留页面跳转及计数
val pageFlowMapRDD: RDD[(String, Int)] = pageFlowRDD.map(_._2).flatMap(list => list)
// 对页面跳转情况进行聚合操作
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()
}
}