前言
本文将利用kaggle上的电影数据tmdb-5000(已上传,可免费下载;若收费,请联系作者修改),实现一个电影推荐系统。由于数据集并不涉及到用户行为数据,所以我们只能用基于统计或者基于内容的推荐方法。
我们将整个项目分为三个部分。
- 数据读取
- 数据预处理和数据可视化
- 搭建推荐系统
在第一部分,我们看到tmdb-5000的两个csv文件里面实际上包含着json格式的数据,我们需要对其进行提取。
在第二部分,我们对缺失值和关键词信息进行处理,提高数据质量;我们还对部分信息进行可视化展示,方便直观了解数据。
在第三部分,我们将整个推荐系统分为召回和过滤两个阶段。
- 在召回阶段,我们通过提取关键词、导演、演员、类别等特征,计算其他电影与当前电影的相似度,并进而取出最相似的40部电影;
- 在过滤阶段,我们将进一步考虑其他特征,如年代、投票等,对召回结果进行进一步的过滤和打分;
- 最终,根据最后过滤阶段的打分,我们推荐打分最高的4部电影
下面,我们将详细介绍每一部分。
Part 1. 数据读取
Step 1
点开tmdb_5000_movies.csv文件,可以发现某些数据是字典,如下
特征"genres"下第一行
'[{"id": 28, "name": "Action"}, {"id": 12, "name": "Adventure"}, {"id": 14, "name": "Fantasy"}, {"id": 878, "name": "Science Fiction"}]'
用json.load()函数来读取此类字典数据,读取后为
[{'id': 28, 'name': 'Action'},
{'id': 12, 'name': 'Adventure'},
{'id': 14, 'name': 'Fantasy'},
{'id': 878, 'name': 'Science Fiction'}]
Step 2
对于特征"genres",我们不需要其中的id,仅仅需要类别名称,因此,从"genres"中抽取类别名称。
具体的,我们在tmdb_5000_movies.csv文件中,
- 从“genres”中抽取电影类别名称
- 从“keywords”中抽取关键词名称
- 从“production_companies”中抽取出公司名称
- 从“production_countries”中抽取出国家名称
- 从“spoken_languages”中抽取出语言名称
在tmdb_5000_credits.csv文件中,
- 从“cast”中抽取演员名称
- 从“crew”中抽取导演名称(剧组中我们通常只看导演)
Step 3
这样抽取完成之后,我们将两个数据集根据电影id合并起来,形成一个dataframe,方便后面数据处理。
此时,dataframe看起来是这样的
Part 2. 数据清洗以及可视化
经过初步处理之后的数据,其实是有些问题的,对于推荐系统而言,
- 特征重复,如"title_x"和“title_y”
- 无用特征,如“original_language”,“budget”,“revenue”等
- 特征提取,如“release_date”可以简单为年份或者时代
- 缺失值处理
数据经过处理之后,我们可以进行可视化,对数据有一个直观的了解,具体的,
- 电影年份分布
- 电影关键词
- 电影类别趋势
Step 1. 数据清洗
Step 1.1. 舍去无用特征
由于我们关心的是电影的推荐,所以,我们将以下特征舍去
- 电影投资收益信息:‘budget’,‘revenue’
- 电影内容信息:‘homepage’, ‘tagline’
- 电影制作信息:‘production_companies’, ‘production_countries’,‘status’,‘runtime’
- 电影重复信息:‘id’,‘original_language’, ‘original_title’,‘title_y’
舍去无用特征之后,我们剩下如下特征,
- ‘id’:电影唯一标识
- ‘title’:电影名称
- ‘actors’:电影所有演员名字
- ‘director’:电影导演名字
- ‘genres’:电影类别
- ‘keywords’:电影关键词
- ‘popularity’:基于某种方法算出来的电影流行度
- ‘date’:电影发布日期
- ‘vote_average’:电影平均评分
- ‘vote_count’:给电影打分人数
- ‘overview’:电影简介
Step 1.2. 缺失值
为了直观看当前数据的缺失值情况,我们给出缺失值表格
从上面表格可以看出,除了keywords之外,其余特征缺失值占总体数据比例非常低(均在1%之下),我们对这些缺失值直接做舍弃处理。
我们主要考虑keywords缺失值。
- keywords缺失值占总体数据8.58%,如果能够填充,当然最好
- 直观上说,keywords与overview和genres很接近,我们或许能从overview和genres中提出信息对keywords进行填充
- 对overview进行tf-idf处理,提取出有代表性的单词,作为keywords
然而实际情况是,我们有4000多个数据,当仅用100个数据对overview进行tf-idf处理时,得到的结果已经很不理想。以第一行数据为例,
- 实际keywords:[‘culture clash’, ‘future’, ‘space war’, ‘space colony’, ‘society’, ‘space travel’, ‘futuristic’, ‘romance’, ‘space’, ‘alien’, ‘tribe’, ‘alien planet’, ‘cgi’, ‘marine’, ‘soldier’, ‘battle’, ‘love affair’, ‘anti war’, ‘power relations’, ‘mind and soul’, ‘3d’]
- overview的tf-idf处理结果:[‘22nd’, ‘dispatched’, ‘orders’, ‘pandora’, ‘paraplegic’, ‘protecting’]
- genres:[‘Action’, ‘Adventure’, ‘Fantasy’, ‘Science Fiction’]
可以看到,实际keywords与overview和genres的处理结果相差很大,为了避免影响数据质量,我们决定不对keywords进行填充,对缺失值所在行直接舍去。
此时,我们一同舍去的还有overview,因为我们只需要用genres和keywords对电影内容进行刻画就行,而相似电影的overview可能完全不同。因此,我们
- 舍去keywords缺失值
- 舍去overview特征
Step 1.3. 特征处理
我们主要对date和keywords进行处理,具体来说,
- 从date中取出年份信息
- 根据年份信息得到年代信息
- 从keywords中取出每个关键词的词根(考虑到很多关键词都是同一个词根的动名词变化等等)
- 对出现次数较低的关键词予以删除,比如低于3次
- (可选项)将同义的关键词用同一个关键词代替
之所以对date这样处理,是因为电影往往和年代有关,比如90年代的电影相比于80年代的电影,有更大的区分度。
之所以对关键词这样处理,是因为后面我们将关键词作为特征来计算相似度,我们需要尽量保证特征之间的独立性,以及特征方差较大。
对date的处理结果如下
而对keywords的处理,我们首先用nltk的PorterStemmer()找出一个关键词的词根,然后用nltk的wordnet给出关键词的同义词。但是,这样比较耗费时间,同时效果并不明显。这是因为,经过删除出现频率较低的关键词之后,我们的关键词已经从9000多降低到2000多,再进行同义词处理效果不明显。
另外,我们还要对actors进行处理。这是因为观众往往仅关注少数的几个主演,对于其他演员并不敏感。
Step 2. 可视化
-
第一张图:演员词云展示
从词云图中可以明显看出,Robert De Niro(罗伯特·德尼罗)、Nicolas Cage(尼古拉斯凯奇)、Bruce Willis(布鲁斯威利斯)、Morgan Freeman等人出演电影较多。实际上,这些人也都是非常著名和成功的演员。 -
第二张图:导演词云展示
通过词云,可以明显找到steven spielberg(斯蒂芬 斯皮尔伯格)、Clint Eastwood(克林特·伊斯特伍德)等众多名导演。 -
第三张图:电影类型的时间趋势图
上面实际上是从1916年到2015年间,美国电影的类别趋势图。可以较为明显的看到,从95年左右电影数量开始大量增长,同时,较为流行的电影类型始终是 drama(剧情片)、thriller(惊悚)、action(动作片)、romance(爱情片)和crime(犯罪片)等。 -
第四张图:不同年代的电影占比
从上图中可以明显看出,90年代至今的电影占据了大多数,其中相比于90年代,00年代的电影数量显著增加,而10年代之所以数量较少,是因为10年代的仅仅统计到2015年。 -
第五张图:不同流行度电影的数量
可以看到,大部分电影的流行度都较低,流行度超过200以上的非常少。 -
第六张图:不同打分下的电影数量
从上面可以看出来,大部分打分都集中在5-8分,但相比于低分段0-5分的其他分数,0分的电影数量反而更多。
Part 3. 推荐引擎
经过上面的数据清洗,我们基本上得到想要的数据。现在,根据这些数据,对于给定的一部电影,我们推荐5部电影。
我们将过程分为 召回 和过滤部分。在召回阶段,我们从所有电影中选出40部电影,在过滤部分,我们从这40部电影中,进一步筛选出5部电影作为最终推荐。
在召回阶段,通过考虑电影的类别信息、导演信息、主演信息和内容的关键词信息,构建特征,通过计算电影之间在给定特征下的相似度,我们给出最相似的40部电影。
在过滤阶段,我们进一步考虑电影的年份信息、评分信息、流行度信息等,对电影进行进一步打分。具体的,
- 对于年份越接近的电影,我们给的分越高
- 对于评分越高的电影,我们给的分越高
- 对于流行度较低但是评分较高的电影,我们给的分越高
基于上述想法,我们构建了推荐系统。
Part 4. 结果测试
我们给出两个例子,测试推荐结果是否吸引人。
第一个例子,对于电影 阿凡达(Avatar),电影id为19995,推荐结果为
['Captain America: The Winter Soldier',
'Treasure Planet',
'Star Trek Into Darkness',
'Titan A.E.',
"Ender's Game"]
对于电影阿凡达,我们是清楚的,我们看看系统推荐了什么,
- Captain America: The Winter Soldier(美国队长2),豆瓣评分8.0,看过的同学应该比较清楚,都是很经典的科幻冒险动作片
- Treasure Planet(星银岛,2002),豆瓣评分7.9,其实阅读评论发现对这部电影评价很高,同样是一部很好的科幻冒险片
- Star Trek Into Darkness(星际迷航2,2013),豆瓣评分8.0,同样是一部经典的科幻冒险动作片
- Titan A.E.(冰冻星球,2000),豆瓣评分6.8,是一部科幻冒险动画片,获过多个大奖,应该是一部不错的电影
- Ender’s Game(安德的游戏,2013),豆瓣评分7.0,个人很喜欢这部电影,是属于科幻冒险动作类大片
可以看到,当一个人看完阿凡达之后,系统将会推荐上面5部电影,就我看过的美国队长2、星际迷航2和安德的游戏而言,是非常恰当的,再就是星银岛和冰冻星球,年份相对较早,不过属于经典之作,应该也值得一看。
第二个例子,对于电影 海底总动员(Finding Nemo),电影id为12,推荐结果为
['The Secret of Kells',
"VeggieTales: The Pirates Who Don't Do Anything",
'Winnie the Pooh',
'Tangled',
'Arthur Christmas']
从上到下,电影名称依次是,凯尔经的秘密(2009,豆瓣评分8.5,奇幻冒险动画),无所事事的海盗(2008,豆瓣评分6.8,冒险家庭喜剧动画),小熊维尼(2011,豆瓣评分8.0,冒险喜剧动画),魔法奇缘(2010,豆瓣评分8.2,奇幻冒险动画),亚瑟圣诞(2011,豆瓣评分7.6,喜剧动画)。
从推荐的内容来看,评分都较高,大部分和海底总动员一样,属于适合家庭一起观看的喜剧经典动画。
评价:总体来看是达到预期效果的,期望系统推荐经典的高评分的相似电影,有些电影属于冷门好看的,这正是我们期待的效果。
不足:对于原始信息中,有些特征,比如电影简介、电影投入、电影收益之类的特征,我们没有使用,如果能够有效利用,或许能提供其他种类的推荐;再就是我们在设计特征的时候,实际上是人工设置的,可能换一种设置方法就能推荐不同的电影,这个也是值得商榷的。
附:github代码