15_spark_core_实战
一.数据准备
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 |
二.需求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也是不能累加的。