背景
网上有一些公开的数据集,可以供我们使用,做一些联系,本次使用的是常见的电影评分数据集,数据集比较容易获取,百度即可,这里只给出电影数据集的格式:
1.users.dat
UserID::Gender::Age::Occupatoin::Zip-Code
2.ratings.dat
UserID::MovieID::Rating::Timestamp
3.movies.dat
MovieID::Title::Genres
本次实践将使用RDD、DataFrame和Dataset三种方式进行实现,涉及的问题列表如下:
(1) 所有电影中平均得分最高(口碑最好)的电影及观看人数最高的电影(流行度最高)
(2)分析最受男性喜爱的电影Top10和最受女性喜爱的电影Top10
(3)某年龄阶段目标用户最爱电影TopN分析
在此说明spark和sql相关版本信息如下,如果版本不一致,api的使用方式会有所出入:
<dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.11</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-sql_2.11</artifactId> <version>2.1.0</version> <scope>compile</scope> </dependency>
正文
0.数据处理
该部分内容主要是rdd和dataframe、dataset之间的格式转换为了后面准备
a.读取rdd
val usersRDD = sc.textFile(dataPath + "users.dat") val moviesRDD = sc.textFile(dataPath + "movies.dat") val occupationsRDD = sc.textFile(dataPath + "occupations.dat") val ratingsRDD = sc.textFile(dataPath + "ratings.dat")
b.rdd转dataframe
拿users.dat里的数据举例,其他类型的数据类似:
//提供结构信息 val schemaforusers = StructType("UserID::Gender::Age::OccupationID::Zip_Code".split("::"). map(column => StructField(column, StringType, true))) //使用Struct方式把Users的数据格式化,即在RDD的基础上增加数据的元数据信息 //rdd->rdd[Row] val usersRDDRows = usersRDD.map(_.split("::")).map(line => Row(line(0).trim,line(1). trim,line(2).trim,line(3).trim,line(4).trim)) //把我们的每一条数据变成以Row为单位的数据 //to dataframe val usersDataFrame = spark.createDataFrame(usersRDDRows, schemaforusers)
如果有其他类型信息,比如DoubleType,我们可以单独添加,比如rating:
val schemaforratings = StructType("UserID::MovieID".split("::"). map(column => StructField(column, StringType, true))). add("Rating", DoubleType, true). add("Timestamp",StringType, true)
如果读入的数据本来就具有结构信息,只是读取成了Rdd,可以导入隐式转换方法,直接rdd.toDF()即可
如果没有列信息,是需要指定的:
import spark.implicits._ val testDF = rdd.map {line=> (line._1,line._2) }.toDF("col1","col2")
c.rdd转dataset
有了dataframe转dataset就方便许多了,分两步:
//1.定义一个case class case class User(UserID:String, Gender:String, Age:String, OccupationID:String, Zip_Code:String) //2.as val usersDataSet = usersDataFrame.as[User]
也可以直接从Rdd转dataset,但是需要引入隐式转换方法
//创建sparksession之后引入隐式转换方法 import sparkSession.implicits._ //定义一个case class case class User(UserID:String, Gender:String, Age:String, OccupationID:String, Zip_Code:String) //可以直接从RDD转换 val usersDataSet = usersRDD.as[User]或者使用隐式转换方法:
import spark.implicits._ case class Coltest(col1:String,col2:Int)extends Serializable //定义字段名和类型 val testDS = rdd.map {line=> Coltest(line._1,line._2) }.toDS
1.所有电影中平均得分最高(口碑最好)的电影及观看人数最高的电影(流行度最高)
a.RDD实现方式
rdd的实现方式倾向于批处理每一行的元素,相对于dataframe和dataset,rdd可以方便的编辑每行的每一个元素,平均分最高的实现思路,需要将ratings.dat中的Rating属性映射成(rating,1),然后构建key,value=>(movieID,(rating,1)),然后按照reduceBykey,即可得到某个电影的总评分和总观影数目,做个除法就获得了最高评分的电影ID,代码如下:
val ratings= ratingsRDD.map(_.split("::")).map(x => (x(0), x(1), x(2))).cache() //格式化出电影ID和评分 ratings.map(x => (x._2, (x._3.toDouble, 1))) //格式化成为Key-Value的方式 .reduceByKey((x, y) => (x._1 + y._1,x._2 + y._2)) //对Value进行reduce操作,分别得出每部电影的总的评分和总的点评人数 .map(x => (x._2._1.toDouble / x._2._2, x._1)) //求出电影平均分 .sortByKey(false) //降序排序 .take(10) //取Top10 .foreach(println) //打印到控制台
b.dataframe实现方式
dataframe提供的方法将为丰富,不需要我们单个元素去编辑,select选出需要的列,分组,求平均值,排序一气呵成,代码如下,评分最高的top10:
ratingsDataFrame.select("MovieID", "Rating").groupBy("MovieID"). avg("Rating").orderBy($"avg(Rating)".desc).show(10)
c.dataset实现方式
dataset和dataframe提供的api基本相同,实现方式相同,评分最高的top10:
ratingsDataSet.select("MovieID", "Rating").groupBy("MovieID"). avg("Rating").orderBy($"avg(Rating)".desc).show(10)
2.分析最受男性喜爱的电影Top10和最受女性喜爱的电影Top10
a.rdd实现
性别属性信息在users.dat中,评分观看信息在ratings.dat中,所以我们需要join操作,为了效率可能会cache一下当前数据,然后filter过滤出男性或者女性,剩下的就跟问题一中的一样了,代码如下:
val male = "M" val female = "F" val genderRatings = ratings.map(x => (x._1, (x._1, x._2, x._3))).join( usersRDD.map(_.split("::")).map(x => (x(0), x(1)))).cache() //因为后面需用多次用到,缓存一下 genderRatings.take(10).foreach(println) //过滤出新别信息的rating val maleFilteredRatings = genderRatings.filter(x => x._2._2.equals("M")).map(x => x._2._1) val femaleFilteredRatings = genderRatings.filter(x => x._2._2.equals("F")).map(x => x._2._1) maleFilteredRatings.map(x => (x._2, (x._3.toDouble, 1))) //格式化成为Key-Value的方式 .reduceByKey((x, y) => (x._1 + y._1,x._2 + y._2)) //对Value进行reduce操作,分别得出每部电影的总的评分和总的点评人数 .map(x => (x._2._1.toDouble / x._2._2, x._1)) //求出电影平均分 .sortByKey(false) //降序排序 .map(x => (x._2, x._1)) .take(10) //取Top10 .foreach(println) //打印到控制台 femaleFilteredRatings.map(x => (x._2, (x._3.toDouble, 1))) //格式化成为Key-Value的方式 .reduceByKey((x, y) => (x._1 + y._1,x._2 + y._2)) //对Value进行reduce操作,分别得出每部电影的总的评分和总的点评人数 .map(x => (x._2._1.toDouble / x._2._2, x._1)) //求出电影平均分 .sortByKey(false) //降序排序 .map(x => (x._2, x._1)) .take(10) //取Top10 .foreach(println) //打印到控制台
b.dataframe实现
实现思路差不多,只是函数使用方式不同,代码如下:
val male = "M" val female = "F" val genderRatingsDataFrame = ratingsDataFrame.join(usersDataFrame, "UserID").cache() //过滤出男女不同性别的打分信息 val maleFilteredRatingsDataFrame = genderRatingsDataFrame.filter("Gender= 'M'").select("MovieID", "Rating") val femaleFilteredRatingsDataFrame = genderRatingsDataFrame.filter("Gender= 'F'").select("MovieID", "Rating") //分组--计算平均分--排序 maleFilteredRatingsDataFrame.groupBy("MovieID").avg("Rating").orderBy($"avg(Rating)".desc).show(10) femaleFilteredRatingsDataFrame.groupBy("MovieID").avg("Rating").orderBy($"avg(Rating)".desc, $"MovieID".desc).show(10)
c.dataset实现
dataset实现方式与dataframe相似,这里就不再重复了。
3.某年龄阶段目标用户最爱电影TopN分析
a.RDD实现方式
特殊年龄阶段的用户,比如18<age<25,在spark中做筛选可能会导致大量的计算任务,比如我们可以在录入数据的时候,或者ETL清洗的时候,将18<age<25 阶段映射成18,25<age<40应设成25,这样我们只需要filter类型就可以了,将筛选任务放在spark任务之前,或者使用hive 内置函数来实现。
本次问题实现需要使用join操作,join操作很容易导致数据倾斜和大量计算任务,如果join双方有一方的数据量不大,可以放到内存中,那我们就可以使用mapjoin,mapjoin是hive的内置函数,spark中我们可以使用broadcase来实现,比如将符合年龄阶段的userID,广播出去,这样在每一个worker的executor中,是可以供task之间共享的,是executor级别,下面代码中就使用了map操作代替了一个join操作
//筛选出目标人群 val targetQQUsers = usersRDD.map(_.split("::")).map(x => (x(0), x(2))).filter(_._2.equals("18")) //为了广播,转换一下结构类型 val targetQQUsersSet = HashSet() ++ targetQQUsers.map(_._1).collect() 广播 val targetQQUsersBroadcast = sc.broadcast(targetQQUsersSet) //获取movieID到name的map结构 val movieID2Name = moviesRDD.map(_.split("::")).map(x => (x(0), x(1))).collect.toMap //1.切分行。2.选取userID、movieId 3.使用广播变量过滤用户 4.map成(movieId,1) 5.reduceByKey统计总数量。6.剩下就是排序了 ratingsRDD.map(_.split("::")).map(x => (x(0), x(1))).filter(x => targetQQUsersBroadcast.value.contains(x._1) ).map(x => (x._2, 1)).reduceByKey(_ + _).map(x => (x._2, x._1)). sortByKey(false).map(x => (x._2, x._1)).take(10). map(x => (movieID2Name.getOrElse(x._1, null), x._2)).foreach(println)
b.dataframe实现
dataframe实现较为简单,使用了join而没有使用mapjoin操作
ratingsDataFrame.join(usersDataFrame, "UserID").filter("Age = '18'").groupBy("MovieID"). count().orderBy($"count".desc).printSchema()
c.dataset实现
一 模一样有木有:
ratingsDataSet.join(usersDataSet, "UserID").filter("Age = '18'").groupBy("MovieID"). count().join(moviesDataSet, "MovieID").select("Title", "count").sort($"count".desc).show(10)
总结
1.rdd变成和dataframe、dataset的区别:
rdd编程使用较为简单的函数,编辑每一行的每一个元素,过程较为复杂,可控性高
dataframe和dataset编程较为简单,强调使用函数编程,而不是针对某一行的某一个元素,可控性较低
2.dataframe和dataset的区别:
dataframe->dataset[Row] 是dataset的特例,在我们的实现中,区别体现不是特别明显,但是dataset是强类型的,比如,在获取一行中的某个元素的时候:
dataframe: row.getString(0) 或 row.col("department") dataset: DataSet<Persono>。取得每条数据某个值时,person.getName()这样的API,可以保证类型安全。此外, 关于schema。DataFrame带有schema,而DataSet没有schema。schema定义了每行数据的“数据结构”,就像关系型数据库中的“列”,schema指定了某个DataFrame有多少列。