[译] 使用pandas构建IMDB Top 250

实际工作和生活中,不可避免地需要一些排序规则。这篇文章或多或少会有一些参考价值。

原文地址:Building an IMDB Top 250 Clone with Pandas

互联网电影数据库(IMDB)维护着一份名为IMDB Top 250的表格,该表格是一份根据某种评分原则生成的排名前250的电影。表格中的电影都是非纪录片,且为剧场版本,影片时长至少45分钟,影评数超过250000条:



这个表格可以看成最简单的推荐器。它没有考虑特定用户的喜好,也没有试图推断不同电影的相似度。它仅仅根据预定义的指标计算每部电影的评分,并以此输出一份排好序的电影列表。
本文包括以下内容:

简单的推荐器

构建一个简单的推荐器的第一步是建立自己的工作目录。新建一个文件夹,命名为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中熟悉的类似表格的结构出现。
构建简单的推荐器非常简单。步骤如下:

  1. 选择一个指标(或分数)来评价电影
  2. 确定要在表格上显示的电影的先决条件
  3. 计算满足条件的每部电影的分数
  4. 按照分数的降序输出电影列表

衡量准则

衡量准则是指对电影排名的定量标准。如果一部电影比另一部电影有更高的定量指标分,则认为该电影要优于另一部。因此,对于建立高质量的电影推荐器,一个鲁棒的可信赖的衡量准则非常重要。
衡量准则的选择是任意的。一种最简单的指标是电影评分。然后,这种方式有各种的缺点。首先,影片评分没有考虑电影的欢迎度。因此,一部被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:
<class 'str'>
<class 'list'>

现在已经有所有必要的工具,将类别特征转为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的值,因为不是每部电影都符合要求。换句话说,分为以下三步:

  1. 获取用户的偏好
  2. 过滤出符合用户条件的电影
  3. 根据以上,计算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的指标,您还可以观察到您的电影同时受到高度评价和欢迎。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容