1.数据准备
本项目的数据是采集电商网站的用户行为数据,主要包含用户的4种行为:搜索、点击、下单和支付。
数据格式
- 数据采用_分割字段。
- 每一行表示用户的一个行为,所以每一行只能是四种行为中的一种。
- 如果搜索关键字是null,表示这次不是搜索。
- 如果点击的品类id和产品id是-1表示这次不是点击。
- 下单行为来说一次可以下单多个产品,所以品类id和产品id都是多个,id之间使用逗号,分割。如果本次不是下单行为,则他们相关数据用null来表示。
- 支付行为和下单行为类似。
数据集下载
链接:https://pan.baidu.com/s/1ZLhYdXz1Foi6MpeUBFGCnQ
提取码:12lt
需求1:Top10热门品类
需求说明
品类是指产品的分类,大型电商网站品类分多级,咱们的项目中品类只有一级,不同的公司可能对热门的定义不一样。我们按照每个品类的点击、下单、支付的量来统计热门品类。
本项目需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。
需求分析
- 判断操作是 “点击”、“下单”、“支付”三类中的哪一类。
- 将品类和操作类别组合为(类别,(点击数,下单数,支付数)),可以将元组分装为对象。
- 按照key类别聚合三种操作的总次数。
- 排序取前10。
代码实现:
创建样例类,用于封装数据信息
// 样例类可以自动生成apply方法和unapply()方法
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
// 定义样例类存储商品品类及对应的操作次数
// 样例类的属性默认使用val修饰,不可重新赋值
case class CategoryCountInfo(categoryId: String, //品类id
var clickCount: Long, //点击次数
var orderCount: Long, //订单次数
var payCount: Long) //支付次数
需求1代码
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
val sc = new SparkContext(conf)
val dataRDD: RDD[String] = sc.textFile("D:\\学习资料\\spark\\spark\\2.资料\\spark-core数据\\user_visit_action.txt")
// 封装对象
val userActionRDD: RDD[UserVisitAction] = dataRDD.map {
line => {
val lineSplit: Array[String] = line.split("_")
// 封装对象
UserVisitAction(
lineSplit(0),
lineSplit(1).toLong,
lineSplit(2),
lineSplit(3).toLong,
lineSplit(4),
lineSplit(5),
lineSplit(6).toLong,
lineSplit(7).toLong,
lineSplit(8),
lineSplit(9),
lineSplit(10),
lineSplit(11),
lineSplit(12).toLong
)
}
}
// 转换数据格式
val cateInfoRDD: RDD[(String, CategoryCountInfo)] = userActionRDD.flatMap {
userAction => {
// 判断操作类型
if (userAction.click_category_id != -1) {
// 转换数据格式 (品类, CategoryCountInfo对象)
List((userAction.click_category_id.toString,
CategoryCountInfo(userAction.click_category_id.toString, 1, 0, 0)))
} else if (userAction.order_category_ids != "null") {
// 处理order_category_ids
val cateIds: Array[String] = userAction.order_category_ids.split(",")
// ListBuffer存储多个对象
val list = new mutable.ListBuffer[(String, CategoryCountInfo)]()
for (cateId <- cateIds) {
list.append((cateId, CategoryCountInfo(cateId, 0, 1, 0)))
}
list
} else if (userAction.pay_category_ids != "null") {
val cateIds: Array[String] = userAction.pay_category_ids.split(",")
// ListBuffer存储多个对象
val list = new mutable.ListBuffer[(String, CategoryCountInfo)]()
for (cateId <- cateIds) {
list.append((cateId, CategoryCountInfo(cateId, 0, 0, 1)))
}
list
} else {
// 返回空集合
Nil
}
}
}
// 根据key聚合数据
val reduceRDD: RDD[(String, CategoryCountInfo)] = cateInfoRDD.reduceByKey(
(cate1, cate2) => {
cate1.clickCount = cate1.clickCount + cate2.clickCount
cate1.orderCount = cate1.orderCount + cate2.orderCount
cate1.payCount = cate1.payCount + cate2.payCount
cate1
}
)
// 去掉多余的key,转换格式
val cateCountRDD: RDD[CategoryCountInfo] = reduceRDD.map {
_._2
}
// 排序取前10
val resArr: Array[CategoryCountInfo] = cateCountRDD.sortBy(
// 元组可以按照元素的顺序来依次排序
cate =>{
(cate.clickCount,cate.orderCount,cate.payCount)},
// 倒序
false
).take(10)
sc.stop()
需求2:Top10热门品类中每个品类的Top10活跃Session统计
需求说明
对于排名前10的品类,分别获取每个品类点击次数排名前10的sessionId。(注意: 这里我们只关注点击次数,不关心下单和支付次数)
对于top10的品类,每一个都要获取对它点击次数排名前10的sessionId。这个功能,可以让我们看到,对某个用户群体最感兴趣的品类,各个品类最感兴趣最典型的用户的session的行为。
需求分析
- 从需求1获取top10热门品类中的id。
- 过滤数据,值保留top10热门品类id和对应的点击日志。
- 转换数据格式 (品类id_session,1)。
- 对如上数据做聚合,统计每个品类的session点击次数。
- 转换数据格式 (品类id,(session,count))。
- 按照品类分组。
- 倒序排序,每组取前10。
代码实现
- 需求2需要借助需求1的结果。
// =======================================上面是需求1=============================================
// 取出top10的商品id
val top10Ids: Array[String] = resArr.map(_.categoryId)
// top10Ids要发送到每个task,可以做广播变量优化
val broadcast: Broadcast[Array[String]] = sc.broadcast(top10Ids)
// 过滤数据,只保留top10 id对应的点击数据
val filterRDD: RDD[UserVisitAction] = userActionRDD.filter(
// 注意click_category_id的类型是Long类型,需要转换为String类型
datas => {
if (datas.click_category_id != -1) {
broadcast.value.contains(datas.click_category_id.toString)
} else {
false
}
}
)
// 转换格式 (品类id_session, 1)
val cateIdAndSession1: RDD[(String, Int)] = filterRDD.map(
datas => {
(datas.click_category_id + "_" + datas.session_id, 1)
}
)
// 按照相同的key聚合 (品类id_session, count)
val cateIdAndSessionCount: RDD[(String, Int)] = cateIdAndSession1.reduceByKey(_ + _)
// 转换数据格式 (品类id, (session, count))
val cateIdAndSessionCount2: RDD[(String, (String, Int))] = cateIdAndSessionCount.map {
case (idAndSession, count) => {
val split: Array[String] = idAndSession.split("_")
(split(0), (split(1), count))
}
}
// 按照品类分组
val cateGroupRDD: RDD[(String, Iterable[(String, Int)])] = cateIdAndSessionCount2.groupByKey()
// 倒序排序,取前10
val res2RDD: RDD[(String, List[(String, Int)])] = cateGroupRDD.mapValues(
datas => {
val list: List[(String, Int)] = datas.toList
// 排序
list.sortWith(_._2 > _._2)
}.take(10)
)
res2RDD.foreach(println)
sc.stop()
需求3:页面单跳转化率统计
需求说明
计算页面单跳转化率,什么是页面单跳转换率,比如一个用户在一次 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 的页面单跳转化率。
需求分析
- 可以先统计每个页面的访问次数
-->先转换数据格式:(页面id,1)
-->再根据页面id做聚合,求出每个页面的总访问量 (页面id,count)
-->再统计页面A->页面B->页面C->页面D…的单跳次数 - 根据sessionId和页面id,对用户访问时间做排序 (sessionid-页面id, 访问时间)
- 转换数据格式,按照sessionid分组
-->session1:页面A->页面B->页面C
-->session2:->页面A->页面B->页面C - 每个组中返回访问顺序的集合 List(每个用户的页面访问顺序)
-->页面访问顺序 : A -> B -> C -> D -> E
-->与集合tail做拉链: B -> C -> D -> E
-->(A,B) (B,C) (C,D) (D,E) - 求出拉链后组成的集合中每个元素出现的次数,即为每个页面的单跳次数 ((A,B),count)
- 最后用每个页面 单跳次数 / 每个页面的总次数 = 页面单跳转化率
val conf: SparkConf = new SparkConf().setAppName(this.getClass.getName).setMaster("local[*]")
val sc = new SparkContext(conf)
val dataRDD: RDD[String] = sc.textFile("D:\\学习资料\\spark\\spark\\2.资料\\spark-core数据\\user_visit_action.txt")
// 封装对象
val userVisitActionRDD: RDD[UserVisitAction] = dataRDD.map {
line => {
val lineSplit: Array[String] = line.split("_")
// 封装对象
UserVisitAction(
lineSplit(0),
lineSplit(1).toLong,
lineSplit(2),
lineSplit(3).toLong,
lineSplit(4),
lineSplit(5),
lineSplit(6).toLong,
lineSplit(7).toLong,
lineSplit(8),
lineSplit(9),
lineSplit(10),
lineSplit(11),
lineSplit(12).toLong
)
}
}
// 先统计每个页面的访问次数
// (页面id,1) -> (页面id,count)
val pageAnd1: RDD[(Long, Long)] = userVisitActionRDD.map(
action => {
(action.page_id, 1L)
}
)
// 按照pageId聚合,每个页面的访问次数, 转为map字典,方便使用
val pageAndCount: Map[Long, Long] = pageAnd1.reduceByKey(_ + _).collect().toMap
// 根据sessionid和pageid,按照时间排序
val sessionAndPage: RDD[(String, String)] = userVisitActionRDD.map(
action => {
// 转换格式 (sessionid_pageid, 时间)
(action.session_id + "_" + action.page_id, action.action_time)
}
)
// 按照sessionid分组,按照时间正序排序
// 转换格式 (sessionid, (pageid,时间))
val sessionPageTime: RDD[(String, (String, String))] = sessionAndPage.map {
case (session_page, time) => {
val split: Array[String] = session_page.split("_")
(split(0), (split(1), time))
}
}
// 按照sessionid分组,每个用户的访问页面和时间
val sessionGroup: RDD[(String, Iterable[(String, String)])] = sessionPageTime.groupByKey()
// 转换格式,舍弃sessionid Iterable(pageid,时间)
val pageTime: RDD[Iterable[(String, String)]] = sessionGroup.map(_._2)
// 按照时间正序排序 Iterable(pageid,时间)
val pageSortRDD: RDD[Iterable[(String, String)]] = pageTime.map(
data => {
data.toList.sortWith(_._2 < _._2)
}
)
//转换格式,舍弃时间 Iterable(pageid)
val pageRDD: RDD[List[String]] = pageSortRDD.map(
datas => {
datas.toList.map(_._1)
}
)
// 拉链操作,组成((A,B),1) 的形式,方便计算从A到B的跳转数
val zipRDD: RDD[((String, String), Long)] = pageRDD.flatMap(
pageList => {
pageList.zip(pageList.tail).map((_, 1L))
}
)
val breakCount: RDD[((String, String), Long)] = zipRDD.reduceByKey(_ + _)
// breakCount.take(3).foreach(println)
// 计算A到B的跳转数
// 跳转数 / 页面访问数
val resRDD: RDD[String] = breakCount.map {
case (breakPage, count) => {
val sum: Long = pageAndCount.getOrElse(breakPage._1.toLong, 1)
val res: Double = count * 1.0 / sum
breakPage + ":" + res
}
}
resRDD.foreach(println)
//breakRDD.take(3).foreach(println)
sc.stop()