spark - 电影信息挖掘小实践(1)

背景

       网上有一些公开的数据集,可以供我们使用,做一些联系,本次使用的是常见的电影评分数据集,数据集比较容易获取,百度即可,这里只给出电影数据集的格式:

 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有多少列。

猜你喜欢

转载自blog.csdn.net/u013560925/article/details/79826783