實際工作和生活中,不可避免地需要一些排序規(guī)則。這篇文章或多或少會有一些參考價值。
原文地址:Building an IMDB Top 250 Clone with Pandas
互聯(lián)網(wǎng)電影數(shù)據(jù)庫(IMDB)維護(hù)著一份名為IMDB Top 250的表格,該表格是一份根據(jù)某種評分原則生成的排名前250的電影。表格中的電影都是非紀(jì)錄片,且為劇場版本,影片時長至少45分鐘,影評數(shù)超過250000條:

這個表格可以看成最簡單的推薦器。它沒有考慮特定用戶的喜好,也沒有試圖推斷不同電影的相似度。它僅僅根據(jù)預(yù)定義的指標(biāo)計算每部電影的評分,并以此輸出一份排好序的電影列表。
本文包括以下內(nèi)容:
- 重構(gòu)一張IMDB Top 250的表格(后面代指簡單的推薦器)
- 進(jìn)一步完善表格的功能,構(gòu)建一個基于知識的推薦器。該模型考慮了用戶的關(guān)于影片類型,年代,時長,語言的喜好,推薦滿足所有條件的電影。
您需要在系統(tǒng)上安裝Python。最后,為了使用Git倉庫,你也需要安裝Git。這篇文章的代碼在Github的地址:https://github.com/PacktPublishing/Hands-On-Recommendation-Systems-with-Python/tree/master/Chapter3。你還可以在http://bit.ly/2v7SZD4上查看代碼視頻。
簡單的推薦器
構(gòu)建一個簡單的推薦器的第一步是建立自己的工作目錄。新建一個文件夾,命名為IMDB。建立一個名為Simple Recommender的Jupyter Notebook,然后在瀏覽器里打開。
可用的數(shù)據(jù)集的地址: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()
在運行單元格時,你應(yīng)該能看到notebook中熟悉的類似表格的結(jié)構(gòu)出現(xiàn)。
構(gòu)建簡單的推薦器非常簡單。步驟如下:
- 選擇一個指標(biāo)(或分?jǐn)?shù))來評價電影
- 確定要在表格上顯示的電影的先決條件
- 計算滿足條件的每部電影的分?jǐn)?shù)
- 按照分?jǐn)?shù)的降序輸出電影列表
衡量準(zhǔn)則
衡量準(zhǔn)則是指對電影排名的定量標(biāo)準(zhǔn)。如果一部電影比另一部電影有更高的定量指標(biāo)分,則認(rèn)為該電影要優(yōu)于另一部。因此,對于建立高質(zhì)量的電影推薦器,一個魯棒的可信賴的衡量準(zhǔn)則非常重要。
衡量準(zhǔn)則的選擇是任意的。一種最簡單的指標(biāo)是電影評分。然后,這種方式有各種的缺點。首先,影片評分沒有考慮電影的歡迎度。因此,一部被100,000位用戶評為9分電影的評分會低于另一部只有100位用戶評為9.5分的電影。這是不可取的,因為很可能這類只有100人觀看和評分的電影迎合了一個非常特定的群體,并不像前者一樣,受大眾喜愛,吸引普通觀眾。
這也是一個事實,隨著投票人數(shù)的增長,電影評分趨于正?;?,并接近一個值,能反應(yīng)電影質(zhì)量和受歡迎度的價值。換而言之,只有少量評分的電影,其評分并不十分可信。一部只有5位用戶評為10分的電影,并不意味著它是一部好電影。
因此,需要定義一個指標(biāo),某種程度上,將影片評分及其參與的投票數(shù)(代表人氣)都考慮進(jìn)來。這將使得一部轟動一時的電影更受青睞,這部電影的評分為8,用戶數(shù)為100,000,而另一部電影的評分為9,用戶數(shù)只有100。
幸運的是,您不必為指標(biāo)集思廣益。您可以使用IMDB的加權(quán)評級公式作為指標(biāo)。在數(shù)學(xué)上,它可以表示如下:
加權(quán)評分(WR)=
(v/(v + m) * R)+ (m/(v+m) * C)
參數(shù)解釋如下:
- v表示電影獲得的票數(shù)
- m表示電表格中電影所需的最小票數(shù)(先決條件)
- R代指電影的平均評分
- C表示數(shù)據(jù)集中所有電影的評分分
v和R各自以電影的vote_count和vote_average的特征計算。計算C則非常簡單。
先決條件
IMDB加權(quán)公式還有一個變量m,需要它計算得分。此變量用于確保僅考慮高于特定人氣閾值的電影進(jìn)行排名。因此,m的值確定有資格在表格中的電影,并且通過作為公式的一部分,確定得分的最終值。
正如衡量準(zhǔn)則,m值的選擇是任意的。換言之,m沒有一個正確的值。最好嘗試不同的m值,然后選擇你(以及你的觀眾)認(rèn)為最好的推薦值。唯一需要記住的是,m的值越高,對電影受歡迎程度的重視程度越高,因此選擇性越高。
推薦而言,請使用第80百分位影片獲得的投票數(shù)作為m的值。換句話說,對于要在排名中考慮的電影,它必須獲得比數(shù)據(jù)集中至少80%的電影更多的選票。另外,在先前描述的加權(quán)公式中使用由第80百分位電影獲得的投票數(shù)來得出分?jǐn)?shù)的值。
現(xiàn)在,計算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)
數(shù)據(jù)集中45000部電影,大約9000(20%)部符合條件。
計算分值
在得到分值之前,最后需要計算的值就是C,數(shù)據(jù)集中所有電影的平均分:
# Calculate C
C = df['vote_average'].mean()
C
OUTPUT:
5.6182072151341851
電影的平均得分為5.6/10。IMDB似乎對電影的評分要求特別嚴(yán)格。 現(xiàn)在已經(jīng)有C的值,可以對每部電影打分了。
首先,定義一個計算電影評分的函數(shù),輸入?yún)?shù)為電影的特征,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函數(shù)作用在Dataframe q_movie上,構(gòu)建一個新的得分特征列。因為,計算是作用在每一行的,設(shè)置axis為1,表示基于行的操作。
# Compute the score using the weighted_rating function defined above
q_movies['score'] = q_movies.apply(weighted_rating, axis=1)
排序及輸出
只剩最后一步?,F(xiàn)在需要基于計算出來的score,將Dataframe排序,輸出一個top的電影列表:

嗯,這樣,推薦器就建好了。
你可以看到寶萊塢電影Dilwale Dulhania Le Jayenge位居榜首。 它的票數(shù)明顯少于其他前25部電影。 這有力地表明你應(yīng)該探索更高的m值。 嘗試不同的m值,觀察圖表中的電影如何變化。
基于知識的推薦器
接下來,你將構(gòu)建一個基于知識的推薦器,方法類似于上面的IMDB Top 250。這將是一個簡單函數(shù),執(zhí)行下面幾個任務(wù):
- 詢問用戶他/她正在尋找的電影類型
- 詢問用戶傾向的影片時長
- 詢問用戶傾向的影片的年代
- 使用收集的信息,向用戶推薦具有高加權(quán)等級(根據(jù)IMDB公式)并滿足上述條件的電影
您擁有的數(shù)據(jù)包含有關(guān)影片時長,流派和時間線的信息,但它目前不是可直接使用的形式。 在將數(shù)據(jù)用于構(gòu)建此推薦程序之前,您的數(shù)據(jù)需要進(jìn)行處理。
在您的IMDB文件夾中,創(chuàng)建一個名為Knowledge Recommender的新Jupyter Notebook。 此notebook將包含您在本節(jié)中編寫的所有代碼。
將依賴包和數(shù)據(jù)加載到notebook中。 另外,請查看已有的特征,并確定對此任務(wù)有用的特征:
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')
結(jié)果來看,很清晰地看到哪些特征需要,哪些不需要。接下來,簡化你的Dataframe,只包含你模型需要的特征:
#Only keep those features that we require
df = df[['title','genres', 'release_date', 'runtime', 'vote_average', 'vote_count']]
df.head()
從release_date特征中提取發(fā)布年份:
#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使用的空值。 將這些值轉(zhuǎn)換為整數(shù)0,并將year特征的數(shù)據(jù)類型轉(zhuǎn)換為int。
為此,定義輔助函數(shù)convert_int,并將其應(yīng)用于年份特征:
#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()
影片時長特征已經(jīng)是可用的形式。 它不需要任何額外的處理。 現(xiàn)在,把你的注意力轉(zhuǎn)向影片的類別。
影片類別
你也許發(fā)現(xiàn)類別信息是以一種類似于Json對象(或Python字典)的格式呈現(xiàn)。瞄一眼電影的類別對象:
#Print genres of the first movie
df.iloc[0]['genres']
OUTPUT:
"[{'id': 16, 'name': 'Animation'}, {'id': 35, 'name': 'Comedy'}, {'id': 10751, 'name': 'Family'}]"
可以發(fā)現(xiàn),輸出的是一個字符串形式的字典。為了讓該特征能用,有必要將其字符串轉(zhuǎn)為原生的Python字典。幸運的是,Python中名為literal_eval的函數(shù)(在ast庫里)可以準(zhǔn)確地處理。literal_eval可以將任意的字符串轉(zhuǎn)為相應(yīng)的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'>

現(xiàn)在已經(jīng)有所有必要的工具,將類別特征轉(zhuǎn)為Python字典格式。
同時,每個字典代表一個類別,存在兩個鍵:id和name。然而,對于這個任務(wù),只需要name。因此,將字典列表轉(zhuǎn)換為字符串列表,其中每個字符串都是一個類別名稱
#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列。換言之,如果一部電影有多個類別,生成這個電影的多個備份,每個備份對應(yīng)一個類別。
舉例而言,假設(shè)一部名為Just Go With It的電影,有romance和comedy兩個類別,將其explode成兩行。一行是Just Go With I被標(biāo)記為romance,另一行則是標(biāo)記為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()
你應(yīng)該能看到三行Toy Story,分別對應(yīng)著animation, family, 和 comedy。這個gen_df的DataFrame對構(gòu)建知識base的推薦器很重要。
build_chart函數(shù)
現(xiàn)在終于可以寫一個函數(shù),作為推薦器了?,F(xiàn)在不能像之前一樣計算m和C的值,因為不是每部電影都符合要求。換句話說,分為以下三步:
- 獲取用戶的偏好
- 過濾出符合用戶條件的電影
- 根據(jù)以上,計算m和C的值,然后按照上一節(jié)中的步驟構(gòu)建圖表
因此,build_chart函數(shù)只接受兩個輸入:gen_df DataFrame和用于計算m值的百分位數(shù)。 默認(rèn)情況下,百分位數(shù)設(shè)置為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小時之間的,發(fā)布時間在1990年到2005年。查看結(jié)果:

您可以看到它輸出的電影滿足您作為輸入傳遞的所有條件。 由于您應(yīng)用了IMDB的指標(biāo),您還可以觀察到您的電影同時受到高度評價和歡迎。