Spark大数据-TMDB电影数据分析(spark-scala版)

TMDB电影数据分析(spark-scala版,pyspark版本

基于kaggle的TMDB电影数据集的数据分析,该数据集包含大约 5000 部电影的相关数据,TMDB数据下载。分析电影关键词的分布、电影投入以及收益评价等之间的关系,并使用Python web进行可视化。

一.环境要求(仅供参考)

centos7、hadoop、spark、bottle(一种基于Python的web框架)。

二.数据预处理

tmdb_5000_movies.csv 数据包含以下字段:
字段名称 解释 例子
budget 预算 10000000
genres 体裁 “[{“”id””: 18, “”name””: “”Drama””}]”
homepage 主页 “”
id id 268238
keywords 关键词 “[{“”id””: 14636, “”name””: “”india””}]”
original_language 原始语言 en
original_title 原标题 The Second Best Exotic Marigold Hotel
overview 概览 As the Best Exotic Marigold Hotel …
popularity 流行度 17.592299
production_companies 生产公司 “[{“”name””: “”Fox Searchlight Pictures””, “”id””: 43}, …]”
production_countries 生产国家 “[{“”iso31661″”: “”GB””, “”name””: “”United Kingdom””}, …]”
release_date 发行日期 2015-02-26
revenue 盈收 85978266
runtime 片长 122
spoken_languages 语言 “[{“”iso6391″”: “”en””, “”name””: “”English””}]”
status 状态 Released
tagline 宣传语 “”
title 标题 The Second Best Exotic Marigold Hotel
vote_average 平均分 6.3
vote_count 投票人数 272

  • 对数据去除标题行。数据中某些字段包含 json 数据,直接使用 DataFrame 进行读取会出现分割错误,如果要创建 DataFrame,先读取文件生成 RDD,再将 RDD 转为 DataFrame。
  • 为了便于处理上传数据至hdfs文件系统:
hdfs dfs -put tmdb_5000_movies.csv

三、使用 Spark 将数据转为 DataFrame

为了创建 DataFrame,首先需要将 HDFS 上的数据加载成 RDD,再将 RDD 转化为 DataFrame。下面代码段完成从文件到 RDD 再到 DataFrame 的转化:

// 创建sparksession
import org.apache.spark.sql.SparkSession
val spark=SparkSession.builder().getOrCreate()
import spark.implicits._
// 使用编程方式定义RDD模式
import org.apache.spark.sql.types._
import org.apache.spark.sql.Row
// 定义一个模式字符串
val schemaString="budget,genres,homepage,id,keywords,original_language,original_title,overview,popularity,production_companies,production_countries,release_date,revenue,runtime,spoken_languages,status,tagline,title,vote_average,vote_count"
// 根据字符串生成模式
val fields=schemaString.split(",").map(fieldName => StructField(fieldName,StringType,nullable=true))
val schema=StructType(fields)
val path = "hdfs://192.168.1.30:9000/user/root/tmdb_5000_movies.csv"

// 由于tmdb的csv数据某些字段中包含
val mdf=spark.read.format("com.databricks.spark.csv")
      .schema(schema)
      .option("inferSchema", value = false)
      .option("header", value = true)
      .option("nullValue", "\\N")
      .option("escape", "\"") // 设置用于在已引用的值内转义引号的单个字符。详情见 spark 读取 csv 官网介绍 https://spark.apache.org/docs/2.0.2/api/java/org/apache/spark/sql/DataFrameReader.html#option(java.lang.String,%20boolean)
      .option("quoteAll","true")
      .option("sep", ",")
      .csv(path)
mdf.select("genres").show(2,false)

四、使用 Spark 进行数据分析

Spark 处理得到的 DataFrame mdf 进行数据分析,首先对数据中的主要字段单独进行分析(概览小节),然后再分析不同字段间的关系(关系小节)。为了方便进行数据可视化,每个不同的分析,都将分析结果导出为 json 文件存储到static目录下,由 web 页面读取并进行可视化。

1.概览

这个部分对数据进行整体的分析。

1.1.TMDb 电影中的体裁分布

从上面的数据字典描述可以看出,电影的体裁字段是一个 json 格式的数据,因此,为了统计不同体裁的电影的数量,需要首先解析 json 数据,从中取出每个电影对应的体裁数组,然后使用词频统计的方法统计不同体裁出现的频率,即可得到电影的体裁分布。
首先实现一个函数 countByJson(field) ,该函数实现解析 json 格式字段从中提取出 name 并进行词频统计的功能:

// (1)TMDb 电影中的体裁分布
import org.apache.spark.sql.functions._
import org.apache.spark.rdd.RDD 
import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.Row

import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods._

import java.io.PrintWriter //这行是Scala解释器执行上面语句后返回的结果
// // 寻找tmdb中最常见的10中预算数,对电影预算频率进行统计
// // 使用sql语句分析
// 或者使用spark的转换函数进行分析
def countByJson(field:String):org.apache.spark.rdd.RDD[(String,Int)]  ={
    val jsonSchema =ArrayType(new StructType().add("id", IntegerType).add("name",StringType)) 
    mdf.select(mdf.col(field))
        .filter(mdf.col(field).isNotNull)
//     此处是单条中包含多个数据的json,按照jsonSchema的格式进行解析,并生成多条单个数据,explode是将数组组生成为列。
        .select(explode(from_json(mdf.col(field), jsonSchema)).as(field))
//     解决$"genres.name"的变量问题
        .select(field.concat(".name"))
        .rdd
        .map(name=>(name.toString(),1))
        .repartition(1)
        .reduceByKey((x,y) => x + y)
}

该函数返回一个 RDD,整个过程如下图所示。
在这里插入图片描述
基于这个函数实现 countByGenres 用来生成不同体裁的电影数统计结果,接着,使用下面代码进行数据导出至 genres.json 方便之后进行可视化:

def countByGenres():String={
    val genresRDD=countByJson("genres")
    val jsonString =genresRDD.collect().toList.map { case(genre,count) =>
    (("genre" ->genre.replace("[","").replace("]",""))~("count" ->count))
    }
    val mdfJson=compact(render(jsonString))
    mdfJson
}
def save(str:String,path:String,hdfspath:String):Unit={
//     写入本地文件
    val out = new PrintWriter(path)
    out.println(str)
    out.close()
//     写入hdfs文件系统
    val str_rdd=spark.sparkContext.parallelize(List(str))
    str_rdd.saveAsTextFile(hdfspath)
}
val str=countByGenres()
println(str)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_genres_word_count.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_genres_word_count.txt"
save(str,path,hdfspath)
1.2. 前 100 个常见关键词

该项分析电影关键词中出现频率最高的前一百个。由于关键词字段也是 json 格式数据,因此调用 countByJson 进行频率统计,同时对于统计结果进行降序排序并取前 100 项即可:

// 2. 前 100 个常见关键词
def countByKeywords():String={
//     对rdd排序
    val keywordsRDD=countByJson("keywords").sortBy(x=>(-x._2))
    val jsonString =keywordsRDD.take(100).toList.map { case(keywords,count) =>
    (("keywords" ->keywords.replace("[","").replace("]",""))~("count" ->count))
    }
    val mdfJson=compact(render(jsonString))
    mdfJson
}
val str=countByKeywords()
println(str)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_keywords_word_count.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_keywords_word_count.txt"
save(str,path,hdfspath)
1.3. TMDb 中最常见的 10 种预算数

这一项探究电影常见的预算数是多少,因此需要对电影预算进行频率统计。首先,需要对预算字段进行过滤,去除预算为 0 的项目,然后根据预算聚合并计数,接着根据计数进行排序,并将结果导出为 json 字符串,为了统一输出,这里将 json 字符串转为 python 对象,最后取前 10 项作为最终的结果。

// 3. TMDb 中最常见的 10 种预算数
def countByBudget(order:String,ascending:Boolean):Array[String]={
    if (ascending){
        mdf.filter(!$"budget".equalTo(0)).groupBy("budget").count().orderBy(order).toJSON.take(10)
    }else{
        mdf.filter(!$"budget".equalTo(0)).groupBy("budget").count().orderBy(desc(order)).toJSON.take(10)
    }
}
val budgetTop10Arr=countByBudget("count",false)

val movie_budgetTop10 = new StringBuilder
movie_budgetTop10 ++= "["
for (v <- budgetTop10Arr){
    movie_budgetTop10 ++= v
}
movie_budgetTop10 ++= "]"
println(movie_budgetTop10.toString)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_budgetTop10.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_budgetTop10.txt"
save(movie_budgetTop10.toString,path,hdfspath)
1.4. TMDb 中最常见电影时长 (只展示电影数大于 100 的时长)

这一项统计 TMDb 中最常见的电影时长,首先,需要过滤时长为 0 的电影,然后根据时长字段聚合并计数,接着过滤掉出现频率小于 100 的时长 (这一步是为了方便可视化,避免过多冗余信息)得到最终的结果。

// 4. TMDb 中最常见电影时长 (只展示电影数大于 100 的时长)
def distrbutionOfRuntime(order:String,ascending:Boolean):Array[String]={
//     后一个filter之前是dataset有两列一列runtime,一列为count
    mdf.filter(!$"runtime".equalTo(0)).groupBy("runtime").count().filter("count>=100").toJSON.collect()
}
val runtimeOfCountOver100Arr=distrbutionOfRuntime("count",false)
val movie_runtimeOfCountOver100 = new StringBuilder
movie_runtimeOfCountOver100 ++= "["
for (v <- runtimeOfCountOver100Arr){
    movie_runtimeOfCountOver100 ++= v
}
movie_runtimeOfCountOver100 ++= "]"
println(movie_runtimeOfCountOver100.toString)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_runtimeOfCountOver100.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_runtimeOfCountOver100.txt"
save(movie_runtimeOfCountOver100.toString,path,hdfspath)
1.5. 生产电影最多的 10 大公司

这一项统计电影产出最多的 10 个公司,同样使用 countByJson 对 JSON 数据进行频率统计,然后进行降序排列取前 10 项即可。

// 5. 生产电影最多的 10 大公司
def countByCompanies():String={
    val production_companiesRDD=countByJson("production_companies").sortBy(x=>(-x._2))
    val jsonString =production_companiesRDD.take(10).toList.map { case(company,count) =>
        (("company" ->company.replace("[","").replace("]",""))~("count" ->count))
        }
    val mdfJson=compact(render(jsonString))
    mdfJson
}
val movie_countByCompanies=countByCompanies()
println(movie_countByCompanies)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_countByCompanies.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_countByCompanies.txt"
save(str,path,hdfspath)
1.6. TMDb 中的 10 大电影语言

该项统计 TMDb 中出现最多的语言,与前面类似,该字段也是 JSON 数据,因此首先对每个项目进行词频统计,然后过滤掉语言为空的项目,最后排序取前十即可。

// 6. TMDb 中的 10 大电影语言
def countByLanguageRDD():String={
    val countByLanguageRDD=countByJson("spoken_languages").sortBy(x=>(-x._2))
    val jsonString =countByLanguageRDD.take(10).toList.map { case(language,count) =>
        (("language" ->language.replace("[","").replace("]",""))~("count" ->count))
        }
    val mdfJson=compact(render(jsonString))
    mdfJson
}
val movie_countByLanguage=countByLanguageRDD()
println(movie_countByLanguage)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_countByLanguage.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_countByLanguage.txt"
save(movie_countByLanguage,path,hdfspath)

2.关系

这个部分考虑数据之间的关系。

2.1.预算与评价的关系

这部分考虑预算与评价之间的关系,因此对于每个电影,需要导出如下的数据:

  • [电影标题,预算,评价]
// 2.关系
// 这个部分考虑数据之间的关系。
// 1. 预算与评价的关系
def budgetVote():Array[String]={
    mdf.select($"title",$"budget",$"vote_average").filter(!$"budget".equalTo(0)).filter(mdf.col("vote_count")>100).toJSON.collect()
}
val budgetVoteArr=budgetVote()
// println(budgetVoteArr.length)
val budgetVoteSB = new StringBuilder
budgetVoteSB ++= "["
for (v <- budgetVoteArr){
    budgetVoteSB ++= v
}
budgetVoteSB ++= "]"
println(budgetVoteSB.toString)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_budgetVote.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_budgetVote.txt"
save(budgetVoteSB.toString,path,hdfspath)
2.2. 发行时间与评价的关系

这部分考虑发行时间与评价之间的关系,因此对于每个电影,需要导出如下的数据:

  • [电影标题,发行时间,评价]
// 2. 发行时间与评价的关系
def dateVote():Array[String]={
    mdf.select($"release_date",$"vote_average",$"title") .filter(mdf.col("release_date").isNotNull).filter(mdf.col("vote_count")>100).toJSON.collect()
}
val dateVoteArr=dateVote()
// println(budgetVoteArr.length)
val dateVoteSB = new StringBuilder
dateVoteSB ++= "["
for (v <- dateVoteArr){
    dateVoteSB ++= v
}
dateVoteSB ++= "]"
println(dateVoteSB.toString)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_dateVote.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_dateVote.txt"
save(dateVoteSB.toString,path,hdfspath)
2.3. 流行度和评价的关系

这部分考虑流行度与评价之间的关系,因此对于每个电影,需要导出如下的数据:

  • [电影标题,流行度,评价]
// 3. 流行度和评价的关系
def popVote():Array[String]={
    mdf.select($"title",$"popularity",$"vote_average").filter(!$"popularity".equalTo(0)).filter(mdf.col("vote_count")>100).toJSON.collect()
}
val popVoteArr=popVote()
println(budgetVoteArr.length)
val popVoteSB = new StringBuilder
popVoteSB ++= "["
for (v <- popVoteArr){
    popVoteSB ++= v
}
popVoteSB ++= "]"
println(popVoteSB.toString)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_popVote.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_popVote.txt"
save(popVoteSB.toString,path,hdfspath)
2.4. 公司生产的电影平均分和数量的关系

这部分计算每个公司生产的电影数量及这些电影的平均分分布。首先,需要对数据进行过滤,去掉生产公司字段为空和评价人数小于 100 的电影,然后对于每一条记录,得到一条如下形式的记录:

  • [公司名,(评分,1)]
    在这里插入图片描述
// 4. 公司生产的电影平均分和数量的关系
def moviesVote():String={
    val jsonSchema =ArrayType(new StructType().add("id", IntegerType).add("name",StringType)) 
    val jsonString=mdf.filter(mdf.col("production_companies").isNotNull)
    .filter(mdf.col("vote_count")>100)
    .select(explode(from_json(mdf.col("production_companies"), jsonSchema)).as("production_companies"),$"vote_average")
    .select($"production_companies.name",$"vote_average")
    .rdd
    .map(v => (v(0).toString,(v(1).toString.toFloat,1)))
    .repartition(1)
    .reduceByKey((x,y) => (x._1+y._1 , x._2+y._2))
    .mapValues(x => (x._1/x._2,x._2))
    .collect()
    .toList.map { case(company,(average,count)) =>
    (("company" ->company.replace("[","").replace("]",""))
     ~("average" ->average)
     ~("count" ->count)
    )}
    val mdfJson=compact(render(jsonString))
    mdfJson
}

val str=moviesVote()
println(str)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_moviesVote.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_moviesVote.txt"
save(str,path,hdfspath)
2.5. 电影预算和营收的关系

这部分考虑电影的营收情况,因此对于每个电影,需要导出如下的数据:

  • [电影标题,预算,收入]
    基于 DataFrame 对数据进行字段过滤即可,过滤掉预算,收入为 0 的数据。:
// 5. 电影预算和营收的关系
// 3. 流行度和评价的关系
def budgetRevenue():Array[String]={
    mdf.select($"title",$"budget",$"revenue").filter(!$"budget".equalTo(0)).filter(!$"revenue".equalTo(0)).toJSON.collect()
}
val budgetRevenueArr=budgetRevenue()
println(budgetVoteArr.length)
val budgetRevenueSB = new StringBuilder
budgetRevenueSB ++= "["
for (v <- budgetRevenueArr){
    budgetRevenueSB ++= v
}
budgetRevenueSB ++= "]"
println(budgetRevenueSB.toString)
val path="/home/chenbengang/ziyu_bigdata/quick_learn_spark/movie_budgetRevenue.txt"
val hdfspath="hdfs://192.168.1.30:9000/user/root/movie_budgetRevenue.txt"
save(budgetRevenueSB.toString,path,hdfspath)

五、数据可视化方法

数据可视化基于阿里开源的数据可视化工具 G2 实现。G2 是一套基于可视化编码的图形语法,以数据驱动,具有高度的易用性和扩展性,用户无需关注各种繁琐的实现细节,一条语句即可构建出各种各样的可交互的统计图表。下面以 TMDb 中电影的体裁分布为例说明可视化过程。

  • 1.首先使用 python Web 框架 bottle 访问可视化页面方便进行 json 数据的读取。使用下面代码web.py 可以实现一个简单的静态文件读取。
  • 2.bottle 对于接收到的请求进行路由
  1. 对于 web 服务启动目录中 static 文件夹下的文件,直接返回对应文件名的文件;
  2. 对于启动目录下的 html 文件,也返回对应的页面。
  3. 直接访问本机的 9999 端口,则返回主页。
  • 3.最后,将 web 服务绑定到本机的 9999 端口。根据上面的实现,对于 web 页面 (html 文件),直接放在服务启动的目录下,对于 Spark 分析的结果,则保存在 static 目录下。
    接下来实现主页文件 index.html。
    (可视化代码见pyspark代码中的可视化部分):百度网盘地址:https://pan.baidu.com/s/1lt7PHF17-gHieOU0B0zJ3A 提取码:cui7

六、数据图表

(一)概览

1.TMDb 电影中的体裁分布:

从图中可以看出,Drama 的电影在 TMDb 中占比较大,其次 Science Fiction、Action 和 Thriller 的数量也较多。
在这里插入图片描述

2.前 100 个常见关键词

TMDb 中最常见的关键词是 Woman Director,其次还有 independent film 等。
在这里插入图片描述

3. TMDb 中最常见的 10 种预算数

有 144 部电影的预算为 20,000,000,是最常见的预算值。
在这里插入图片描述

4. TMDb 中最常见电影时长 (只展示电影数大于 100 的时长)

多数电影的时长是90分钟或100分钟。
在这里插入图片描述

5. 生产电影最多的 10 大公司

生产电影较多的公司是 Warner Bros.、Universal Pictures等。
在这里插入图片描述

6. TMDb 中的 10 大电影语言

大多数电影中的语言是英语。
在这里插入图片描述

(二)关系

1.预算与评价的关系

预算高的电影不见得能取得更好的评价,例如预算高达 380,000,000 美元的 Pirates of the Caribbean: On Stranger Tides(加勒比海盗)评价只有6.4分。
在这里插入图片描述

2.发行时间与评价的关系

早期的电影评价都比较高,例如发行于1936年的 Modern Times(摩登时代)评价高达8.1分。
在这里插入图片描述

3.流行度和评价的关系

流行度较高的话一般能取得平均水平以上的评价,例如 Interstellar(星际穿越)流行度很高,评价为8.1分。
在这里插入图片描述

4.公司生产的电影平均分和数量的关系

从图中可以看出,一个公司生产的电影越多,其电影平均分越接近整体的平均水平。
在这里插入图片描述

5.电影预算和营收的关系

从图中可以看出,多数电影都能实现正收入,而预算为 237,000,000 美元的 Avatar(阿凡达)最终收入为2787,965,087美元
在这里插入图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值