实际工作和生活中,不可避免地需要一些排序规则。这篇文章或多或少会有一些参考价值。
原文地址:Building an IMDB Top 250 Clone with Pandas
互联网电影数据库(IMDB)维护着一份名为IMDB Top 250的表格,该表格是一份根据某种评分原则生成的排名前250的电影。表格中的电影都是非纪录片,且为剧场版本,影片时长至少45分钟,影评数超过250000条:
这个表格可以看成最简单的推荐器。它没有考虑特定用户的喜好,也没有试图推断不同电影的相似度。它仅仅根据预定义的指标计算每部电影的评分,并以此输出一份排好序的电影列表。
本文包括以下内容:
- 重构一张IMDB Top 250的表格(后面代指简单的推荐器)
- 进一步完善表格的功能,构建一个基于知识的推荐器。该模型考虑了用户的关于影片类型,年代,时长,语言的喜好,推荐满足所有条件的电影。
您需要在系统上安装Python。最后,为了使用Git仓库,你也需要安装Git。这篇文章的代码在Github的地址:https://github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python/tree/master/Chapter3。你还可以在http://bit.ly/2v7SZD4上查看代码视频。
简单的推荐器
构建一个简单的推荐器的第一步是建立自己的工作目录。新建一个文件夹,命名为IMDB。建立一个名为Simple Recommender的Jupyter Notebook,然后在浏览器里打开。
可用的数据集的地址:https://www.kaggle.com/rounakbanik/the-movies-dataset/downloads/movies_metadata.csv/7
import pandas as pd
import numpy as np
#Load the dataset into a pandas dataframe
df = pd.read_csv('../data/movies_')
#Display the first five movies in the dataframe
df.head()
在运行单元格时,你应该能看到notebook中熟悉的类似表格的结构出现。
构建简单的推荐器非常简单。步骤如下:
- 选择一个指标(或分数)来评价电影
- 确定要在表格上显示的电影的先决条件
- 计算满足条件的每部电影的分数
- 按照分数的降序输出电影列表
衡量准则
衡量准则是指对电影排名的定量标准。如果一部电影比另一部电影有更高的定量指标分,则认为该电影要优于另一部。因此,对于建立高质量的电影推荐器,一个鲁棒的可信赖的衡量准则非常重要。
衡量准则的选择是任意的。一种最简单的指标是电影评分。然后,这种方式有各种的缺点。首先,影片评分没有考虑电影的欢迎度。因此,一部被100,000位用户评为9分电影的评分会低于另一部只有100位用户评为9.5分的电影。这是不可取的,因为很可能这类只有100人观看和评分的电影迎合了一个非常特定的群体,并不像前者一样,受大众喜爱,吸引普通观众。
这也是一个事实,随着投票人数的增长,电影评分趋于正常化,并接近一个值,能反应电影质量和受欢迎度的价值。换而言之,只有少量评分的电影,其评分并不十分可信。一部只有5位用户评为10分的电影,并不意味着它是一部好电影。
因此,需要定义一个指标,某种程度上,将影片评分及其参与的投票数(代表人气)都考虑进来。这将使得一部轰动一时的电影更受青睐,这部电影的评分为8,用户数为100,000,而另一部电影的评分为9,用户数只有100。
幸运的是,您不必为指标集思广益。您可以使用IMDB的加权评级公式作为指标。在数学上,它可以表示如下:
加权评分(WR)=
(v/(v + m) * R)+ (m/(v+m) * C)
参数解释如下:
- v表示电影获得的票数
- m表示电表格中电影所需的最小票数(先决条件)
- R代指电影的平均评分
- C表示数据集中所有电影的评分分
v和R各自以电影的vote_count和vote_average的特征计算。计算C则非常简单。
先决条件
IMDB加权公式还有一个变量m,需要它计算得分。此变量用于确保仅考虑高于特定人气阈值的电影进行排名。因此,m的值确定有资格在表格中的电影,并且通过作为公式的一部分,确定得分的最终值。
正如衡量准则,m值的选择是任意的。换言之,m没有一个正确的值。最好尝试不同的m值,然后选择你(以及你的观众)认为最好的推荐值。唯一需要记住的是,m的值越高,对电影受欢迎程度的重视程度越高,因此选择性越高。
推荐而言,请使用第80百分位影片获得的投票数作为m的值。换句话说,对于要在排名中考虑的电影,它必须获得比数据集中至少80%的电影更多的选票。另外,在先前描述的加权公式中使用由第80百分位电影获得的投票数来得出分数的值。
现在,计算m的值:
#Calculate the number of votes garnered by the 80th percentile movie
m = df['vote_count'].quantile(0.80)
m
OUTPUT:
50.0
可以看到,只有百分之20的电影获得了超过50个的评分。因此,m的值取50.
另一个考虑的先决条件是影片时长。仅仅考虑时长在45分钟到300分钟的电影。定义一个Dataframe,q_movies,包含符合条件的所有电影。
#Only consider movies longer than 45 minutes and shorter than 300 minutes
q_movies = df[(df['runtime'] >= 45) & (df['runtime'] <= 300)]
#Only consider movies that have garnered more than m votes
q_movies = q_movies[q_movies['vote_count'] >= m]
#Inspect the number of movies that made the cut
q_movies.shape
OUTPUT:
(8963, 24)
数据集中45000部电影,大约9000(20%)部符合条件。
计算分值
在得到分值之前,最后需要计算的值就是C,数据集中所有电影的平均分:
# Calculate C
C = df['vote_average'].mean()
C
OUTPUT:
5.6182072151341851
电影的平均得分为5.6/10。IMDB似乎对电影的评分要求特别严格。 现在已经有C的值,可以对每部电影打分了。
首先,定义一个计算电影评分的函数,输入参数为电影的特征,m和C的值:
# Function to compute the IMDB weighted rating for each movie
def weighted_rating(x, m=m, C=C):
v = x['vote_count']
R = x['vote_average']
# Compute the weighted score
return (v/(v+m) * R) + (m/(m+v) * C)
然后,使用熟悉的apply函数作用在Dataframe q_movie上,构建一个新的得分特征列。因为,计算是作用在每一行的,设置axis为1,表示基于行的操作。
# Compute the score using the weighted_rating function defined above
q_movies['score'] = q_movies.apply(weighted_rating, axis=1)
排序及输出
只剩最后一步。现在需要基于计算出来的score,将Dataframe排序,输出一个top的电影列表:
嗯,这样,推荐器就建好了。
你可以看到宝莱坞电影Dilwale Dulhania Le Jayenge位居榜首。 它的票数明显少于其他前25部电影。 这有力地表明你应该探索更高的m值。 尝试不同的m值,观察图表中的电影如何变化。
基于知识的推荐器
接下来,你将构建一个基于知识的推荐器,方法类似于上面的IMDB Top 250。这将是一个简单函数,执行下面几个任务:
- 询问用户他/她正在寻找的电影类型
- 询问用户倾向的影片时长
- 询问用户倾向的影片的年代
- 使用收集的信息,向用户推荐具有高加权等级(根据IMDB公式)并满足上述条件的电影
您拥有的数据包含有关影片时长,流派和时间线的信息,但它目前不是可直接使用的形式。 在将数据用于构建此推荐程序之前,您的数据需要进行处理。
在您的IMDB文件夹中,创建一个名为Knowledge Recommender的新Jupyter Notebook。 此notebook将包含您在本节中编写的所有代码。
将依赖包和数据加载到notebook中。 另外,请查看已有的特征,并确定对此任务有用的特征:
import pandas as pd
import numpy as np
df = pd.read_csv('../data/movies_metadata.csv')
#Print all the features (or columns) of the DataFrame
df.columns
OUTPUT:
Index(['adult', 'belongs_to_collection', 'budget', 'genres', 'homepage', 'id',
'imdb_id', 'original_language', 'original_title', 'overview',
'popularity', 'poster_path', 'production_companies',
'production_countries', 'release_date', 'revenue', 'runtime',
'spoken_languages', 'status', 'tagline', 'title', 'video',
'vote_average', 'vote_count'],
dtype='object')
结果来看,很清晰地看到哪些特征需要,哪些不需要。接下来,简化你的Dataframe,只包含你模型需要的特征:
#Only keep those features that we require
df = df[['title','genres', 'release_date', 'runtime', 'vote_average', 'vote_count']]
df.head()
从release_date特征中提取发布年份:
#Convert release_date into pandas datetime format
df['release_date'] = pd.to_datetime(df['release_date'], errors='coerce')
#Extract year from the datetime
df['year'] = df['release_date'].apply(lambda x: str(x).split('-')[0] if x != np.nan else np.nan)
年份特征仍然是一个对象,并且充满了NaT值,这是一种由Pandas使用的空值。 将这些值转换为整数0,并将year特征的数据类型转换为int。
为此,定义辅助函数convert_int,并将其应用于年份特征:
#Helper function to convert NaT to 0 and all other years to integers.
def convert_int(x):
try:
return int(x)
except:
return 0
#Apply convert_int to the year feature
df['year'] = df['year'].apply(convert_int)
You do not require the release_date feature anymore. So, go ahead and remove it:
#Drop the release_date column
df = df.drop('release_date', axis=1)
#Display the dataframe
df.head()
影片时长特征已经是可用的形式。 它不需要任何额外的处理。 现在,把你的注意力转向影片的类别。
影片类别
你也许发现类别信息是以一种类似于Json对象(或Python字典)的格式呈现。瞄一眼电影的类别对象:
#Print genres of the first movie
df.iloc[0]['genres']
OUTPUT:
"[{'id': 16, 'name': 'Animation'}, {'id': 35, 'name': 'Comedy'}, {'id': 10751, 'name': 'Family'}]"
可以发现,输出的是一个字符串形式的字典。为了让该特征能用,有必要将其字符串转为原生的Python字典。幸运的是,Python中名为literal_eval的函数(在ast库里)可以准确地处理。literal_eval可以将任意的字符串转为相应的Python对象:
#Import the literal_eval function from ast
from ast import literal_eval
#Define a stringified list and output its type
a = "[1,2,3]"
print(type(a))
#Apply literal_eval and output type
b = literal_eval(a)
print(type(b))
OUTPUT:
现在已经有所有必要的工具,将类别特征转为Python字典格式。
同时,每个字典代表一个类别,存在两个键:id和name。然而,对于这个任务,只需要name。因此,将字典列表转换为字符串列表,其中每个字符串都是一个类别名称
#Convert all NaN into stringified empty lists
df['genres'] = df['genres'].fillna('[]')
#Apply literal_eval to convert to the list object
df['genres'] = df['genres'].apply(literal_eval)
#Convert list of dictionaries to a list of strings
df['genres'] = df['genres'].apply(lambda x: [i['name'] for i in x] if isinstance(x, list) else [])
df.head()
打印Dataframe的头部,将展示一个新的genres特征,其包含一个genre名字的列表。然而,事情还没完成。最后一步是,explode这个genres列。换言之,如果一部电影有多个类别,生成这个电影的多个备份,每个备份对应一个类别。
举例而言,假设一部名为Just Go With It的电影,有romance和comedy两个类别,将其explode成两行。一行是Just Go With I被标记为romance,另一行则是标记为comedy:
#Create a new feature by exploding genres
s = df.apply(lambda x: pd.Series(x['genres']),axis=1).stack().reset_index(level=1, drop=True)
#Name the new feature as 'genre'
s.name = 'genre'
#Create a new dataframe gen_df which by dropping the old 'genres' feature and adding the new 'genre'.
gen_df = df.drop('genres', axis=1).join(s)
#Print the head of the new gen_df
gen_df.head()
你应该能看到三行Toy Story,分别对应着animation, family, 和 comedy。这个gen_df的DataFrame对构建知识base的推荐器很重要。
build_chart函数
现在终于可以写一个函数,作为推荐器了。现在不能像之前一样计算m和C的值,因为不是每部电影都符合要求。换句话说,分为以下三步:
- 获取用户的偏好
- 过滤出符合用户条件的电影
- 根据以上,计算m和C的值,然后按照上一节中的步骤构建图表
因此,build_chart函数只接受两个输入:gen_df DataFrame和用于计算m值的百分位数。 默认情况下,百分位数设置为80%或0.8:
def build_chart(gen_df, percentile=0.8):
#Ask for preferred genres
print("Input preferred genre")
genre = input()
#Ask for lower limit of duration
print("Input shortest duration")
low_time = int(input())
#Ask for upper limit of duration
print("Input longest duration")
high_time = int(input())
#Ask for lower limit of timeline
print("Input earliest year")
low_year = int(input())
#Ask for upper limit of timeline
print("Input latest year")
high_year = int(input())
#Define a new movies variable to store the preferred movies. Copy the contents of gen_df to movies
movies = gen_df.copy()
#Filter based on the condition
movies = movies[(movies['genre'] == genre) &
(movies['runtime'] >= low_time) &
(movies['runtime'] <= high_time) &
(movies['year'] >= low_year) &
(movies['year'] <= high_year)]
#Compute the values of C and m for the filtered movies
C = movies['vote_average'].mean()
m = movies['vote_count'].quantile(percentile)
#Only consider movies that have higher than m votes. Save this in a new dataframe q_movies
q_movies = movies.copy().loc[movies['vote_count'] >= m]
#Calculate score using the IMDB formula
q_movies['score'] = q_movies.apply(lambda x: (x['vote_count']/(x['vote_count']+m) * x['vote_average'])
+ (m/(m+x['vote_count']) * C)
,axis=1)
#Sort movies in descending order of their scores
q_movies = q_movies.sort_values('score', ascending=False)
return q_movies
是时候把你的模型付诸行动了!
您可能需要推荐动画电影,并有以下要求:影片时长在30分钟到2小时之间的,发布时间在1990年到2005年。查看结果:
您可以看到它输出的电影满足您作为输入传递的所有条件。 由于您应用了IMDB的指标,您还可以观察到您的电影同时受到高度评价和欢迎。