电影推荐系统-整体总结(三)离线统计
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() } }
浙公网安备 33010602011771号