电影推荐系统-整体总结(三)离线统计

Posted on 2020-10-16 20:40  MissRong  阅读(262)  评论(0)    收藏  举报

电影推荐系统-整体总结(三)离线统计

Scala代码实现

1.自定义数据类--Model.scala

package staticRecommender

/**
  * @Author : ASUS and xinrong
  * @Version : 2020/9/4
  *  数据格式转换类
  *  ---------------电影表------------------------
  *  1
  *  Toy Story (1995)
  *
  *  81 minutes
  *  March 20, 2001
  *  1995
  *  English
  *  Adventure|Animation|Children|Comedy|Fantasy
  *  Tom Hanks|Tim Allen|Don Rickles|Jim Varney|Wallace Shawn|John Ratzenberger|Annie Potts|John Morris|Erik von Detten|Laurie Metcalf|R. Lee Ermey|Sarah Freeman|Penn Jillette|Tom Hanks|Tim Allen|Don Rickles|Jim Varney|Wallace Shawn
  *  John Lasseter
  */
case class Movie(val mid:Int,val name:String,val descri:String,
                 val timelong:String,val cal_issue:String,val shoot:String,
                 val language:String,val genres :String,val actors:String,val directors:String)
/**
  * -----用户对电影的评分数据集--------
  * 1,31,2.5,1260759144
  */
case class Rating(val uid:Int,val mid:Int,val score:Double,val timestamp:Int)

/**
  * --------用户对电影的标签数据集--------
  * 15,339,sandra 'boring' bullock,1138537770
  */
case class Tag(val uid:Int,val mid:Int,val tag:String,val timestamp:Int)

/**
  *
  * MongoDB配置对象
  * @param uri
  * @param db
  */
case class MongoConfig(val uri:String,val db:String)

/**
  * ES配置对象
  * @param httpHosts
  * @param transportHosts:保存的是所有ES节点的信息
  * @param clusterName
  */
case class EsConfig(val httpHosts:String,val transportHosts:String,val index:String,val clusterName:String)
/**
  * recs的二次封装数据类
  * @param mid
  * @param res
  */
case class Recommendation(mid: Int ,res:Double)
/**
  *case class Recommender
  * Key-Value封装数据类
  * @param genres
  * @param recs
  */
case class GenresRecommendation(genres:String ,recs:Seq[Recommendation])

2.调用方法类--StatisticsApp

package staticRecommender

import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession

/**
  * @Author : ASUS and xinrong
  * @Version : 2020/9/18
  *         离线统计的main方法(只负责调用方法类)
  *         在工作中也是这样的,将入口程序和实现的程序分开,以免混淆
  *
  *         一、数据流程
  *         spark读取MongoDB中的数据,进行离线统计后再将结果写入MongoDB中
  *         二、统计值
  *         1.评分最多电影:
  *          获取所在历史数据中,评分个数最多的电影的集合,统计每个电影评分个数 -->存入的MongoDB表名:RateMoreMovies
  *         2.近期热门电影:
  *          按照月来统计,这个月中,评分最多的电影我们认为是热门电影,统计每个月中每个电影的评分总量 -->RateMoreRecentlyMovies
  *         3.电影平均得分:
  *          把每个电影所有的用户得分进行平均,计算出每个电影的平均得分--> AverageMovies
  *         4.每种类别电影的Top10:
  *          首先,将每种类别的电影中评分最高的10个电影拿出来-->GenresTopMovies
  *          注意:一个电影可能属于多个类别,而且他的结果还依赖于 3 的结果
  */
object StatisticsApp extends App {
  //声明表
  val MONGO_RATING_COLLECTION="Rating"
  val MONGO_MOVIE_COLLECTION="Movie"

  val params=scala.collection.mutable.Map[String,Any]()
  //保证CPU的核数大于等于2,"local[2]"表示开启两个线程
  //一个线程用于读取数据,一个线程用于计算处理数据
  params+="spark.core"->"local[2]"
  params+="mongodb.uri"->"mongodb://192.168.212.21:27017/recom"
  params+="mongodb.db"->"recom"


  //一、创建运行时的环境
  //spark环境
  val sparkConf = new SparkConf().setAppName("StatisticsApp").setMaster(params("spark.core").asInstanceOf[String])
  val sparkSession = SparkSession.builder().config(sparkConf).getOrCreate()

  //二、初始化MongoDB对象
    //传入uri、db--String类型
  implicit val mongoConfig = MongoConfig(params("mongodb.uri").asInstanceOf[String],params("mongodb.db").asInstanceOf[String])
    //之所以定义成隐式参数,是想便于后面的方法去调用

  //三、读表
  import sparkSession.implicits._
    // 1.读取mongoDB中的数据-ratings表的
  val ratingDS=sparkSession
    .read
    .option("uri",mongoConfig.uri)
    .option("collection",MONGO_RATING_COLLECTION)
    .format("com.mongodb.spark.sql")
    .load()
    .as[Rating]  //将DataFrame转换成DataSet,这样便于执行RDD操作
    .cache()  //将其随手存入缓存

    // 2.读取mongoDB中的数据-movies表的
  val movieDS=sparkSession
    .read
    .option("uri", mongoConfig.uri)
    .option("collection", MONGO_MOVIE_COLLECTION) //要读的表名
    .format("com.mongodb.spark.sql")
    .load()
    .as[Movie] //将DataFrame转换成DataSet,便于RDD操作
    .cache()

    //3.取出所有的电影类别
  val genres =movieDS.toDF().map{
    case row=>
      row.getAs[String]("genres")
  }.flatMap(_.split("\\|")).distinct().toDF().show()

  //四、将读到的数据信息注册成临时的视图
  ratingDS.createOrReplaceTempView("ratings")

  //五、统计评分最多的电影
  StatisticsRecommender.rateMore(sparkSession)
  //六、统计近期热门电影
  StatisticsRecommender.rateMoreRecently(sparkSession)
  //七、按类别统计平均分最高的10个电影(第4个目标包含第3个目标)
  StatisticsRecommender.genresTop10(sparkSession)(movieDS)

  //八、随手关闭
  ratingDS.unpersist()
  movieDS.unpersist()
  sparkSession.close()
}

3.离线统计方法的实际实现类--StatisticsRecommender

package staticRecommender

import java.text.SimpleDateFormat
import java.util.Date

import org.apache.spark.sql.{Dataset, SparkSession}

/**
  * @Author : ASUS and xinrong
  * @Version : 2020/9/18
  *         离线统计方法实现(存放所有离线统计实现代码)
  */
object StatisticsRecommender {
  //定义表名
  val RATE_MORE_MOVIES="RateMoreMovies"
  val RATE_MORE_MOVIES_RECENTLY="RateMoreMoviesRecently"
  val AVERAGE_MOVIES_SCORE="AverageMoviesScore"
  val GENRES_ROP_MOVIES="GenresTopMovies"

  //一、统计评分最多的电影
  //注意:没有要求取前几个电影,所以直接统计出来就好
  def rateMore(sparkSession: SparkSession)(implicit mongoConfig: MongoConfig)={
    val rateMoreMovie=sparkSession.sql("select mid ,count(1)as count from ratings group by mid order by count desc")
    //将统计好的数据写出去
    rateMoreMovie
      .write
      .option("uri",mongoConfig.uri)
      .option("collection",RATE_MORE_MOVIES)
      .mode("overwrite")
      .format("com.mongodb.spark.sql")
      .save()
  }

  //二、统计近期热门电影
  def rateMoreRecently(sparkSession: SparkSession)(implicit mongoConfig: MongoConfig)={
    //1.规定一种日期的格式
    /**
      * UDF--修改日期格式
      * 将数据中的长整形-1260759114类型的数据转换成规定形式"201912"
      */
    //这个线程不是很安全
    //建议使用这种写法(JDK 1.8版本以上新升级的):val dateTimeFormatter=DateTimeFormatter.ofPattern("yyyyMM")
    //因为SimpleDateFormat不是线程安全的,使用的时候,只能在方法内部创建新的局部变量。
    // 而DateTimeFormatter可以只创建一个实例,到处引用。
    val simpleDateFormat = new SimpleDateFormat("yyyyMM")
    //val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM")
    //2.写一个转换日期格式的方法
    //下面这行最后的转长整形(.toLong)是可有可无的,x*1000L是为了将时间精确到毫秒
    sparkSession.udf.register("changeDate",(x:Long)=>simpleDateFormat.format(new Date(x*1000L)).toLong)
    //3.将方法用到日期的转化上,获取数据
    val yearMonthOfRatings=sparkSession.sql("select mid,uid,score,changeDate(timestamp) as yearmonth from ratings ")
    //4.将读到的数据注册成临时的视图
    yearMonthOfRatings.createOrReplaceTempView("yearMonthOfRatings")
    //5.再根据获取的视图,进行排序
    //group by 月份和mid
    //排序的时候是按照月份和个数的降序进行排序的
    sparkSession.sql("select mid,yearmonth,count(1) as count from yearMonthOfRatings group by mid,yearmonth order by yearmonth desc,count desc" )
      .write
      .option("uri",mongoConfig.uri)
      .option("collection",RATE_MORE_MOVIES_RECENTLY)
      .mode("overwrite")
      .format("com.mongodb.spark.sql")
      .save()
  }

  //三、按类别统计平均分最高的10个电影(第4个目标包含第3个目标)

  //可以将Movie当作参数直接传进来,也可以将movies当成视图去写
  //在学闭包柯里化的时候(柯里化解决了多个参数的问题)--可以将多个参数拆成多个括号的形式,调用的时候需要注意-要写多个括号
  def genresTop10(sparkSession: SparkSession)(movie:Dataset[Movie])(implicit mongoConfig: MongoConfig)= {
    //1.定义所有的电影类别(需要自己统计所有的类别)
      //类别直接写出来就可以了,不然数据量太大,统计费时
    val genres = List("Action", "Adventure", "Animation", "Comedy", "Ccrime", "Documentary", "Drama", "Family", "Fantasy", "Foreign", "History", "Horror", "Music", "Mystery"
      , "Romance", "Science", "Tv", "Thriller", "War", "Western")
    //2.统计电影平均分-averageMovieScore(DF类型的)
    val averageMovieScore = sparkSession.sql("select mid , avg(score) as avg from ratings group by mid").cache()
    //3.统计类别中分数最高的10部电影
    //先将电影平均分、电影两个集合进行Join得到一个合并后的数据集
    val moviesWithScoreDF = movie.join(averageMovieScore, Seq("mid", "mid")).select("mid", "avg", "genres").cache()
    //4.做笛卡儿积、filter、模式匹配+GroupBy、排序+take
    //1)genres一开始是一个List,将其转成RDD才能使用
    val genresRDD = sparkSession.sparkContext.makeRDD(genres)
    //2)操作笛卡儿积注意引入(需要的时候再引入)
    import sparkSession.implicits._
    //3)进行Filter(将名字转换成小写)
    //注意要将合并后的集合-moviesWithScoreDF转换成RDD形式
    val genresTopMovies=genresRDD.cartesian(moviesWithScoreDF.rdd).filter {
      //genres:是指做笛卡儿积之后所得数据集的genres列;row则是指此数据集中属于原合并数据集(moviesWithScoreDF)的每行数据
      case (genres, row) => {
        //如果genres在row中geners包括的电影种类里,就返回true,它就被保留了
        row.getAs[String]("genres").toLowerCase.contains(genres.toLowerCase)
        //否则此行数据就会被清除(不删除结构,只是删除数据)
      }
    }.map {
      //4)GroupBy
      case (genres, row) => {
        //genres作为Key,(Int,Double)作为value
        //按键分组
        (genres, (row.getAs[Int]("mid"), row.getAs[Double]("avg")))
      }
    }.groupByKey()
      .map {
        //5)排序+take
        case (genres, items) =>
          //将items从Iterable转换成List,然后进行遍历
          GenresRecommendation(genres, items.toList.sortWith(_._2 > _._2).take(10).map(x => Recommendation(x._1, x._2)))  //取前十位
        }.toDF //注意:调用toDF,需要引入:import spark.implicits._

    //5.将统计好的top10电影数据写入到MongoDB里面(注意转换成DataFrame的类型)
    genresTopMovies
      .write
      .option("uri",mongoConfig.uri)
      .option("collection",GENRES_ROP_MOVIES)
      .mode("overwrite")
      .format("com.mongodb.spark.sql")
      .save()
    //6.将统计好的电影平均分写入MongoDB里面
    averageMovieScore
      .write
      .option("uri",mongoConfig.uri)
      .option("collection",AVERAGE_MOVIES_SCORE)
      .mode("overwrite")
      .format("com.mongodb.spark.sql")
      .save()
  }
}

 

博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3